diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..631ae16d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,42 @@ +--- +name: Bug report +about: Create a bug report for a script (please do not report security issues here) +title: "script_name.py: " +labels: bug + +--- + + + +## Script / WeeChat + +- Name of script:  +- Script version:  +- WeeChat version:  + +This bug has been reported to the script author? Yes/No +Response or reason:  + +## Bug summary + + + +## Steps to reproduce + +1.  +2.  +3.  + +## Current behavior + + + +## Expected behavior + + + +## Suggested solutions + + + +## Additional information diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..f6d7291c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2020-2025 Sébastien Helleu +# +# SPDX-License-Identifier: GPL-3.0-or-later + +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..cfb8c4f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,13 @@ +--- +name: Feature request +about: Request a new feature for a script +title: "script_name.py: " +labels: feature + +--- + +## Script + +- Name of script:  + +## Feature description diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..f58d76ca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,13 @@ +--- +name: Question +about: Ask a question about a script +title: "script_name.py: " +labels: question + +--- + +## Script + +- Name of script:  + +## Question diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..1df46a9c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,63 @@ +## Script info + + + +- Script name:  +- Version:  + + +- Requirements:  + + +- Min WeeChat version:  + + +- Script tags:  + +## Description + + + + + +## Checklist (new script) + + + + + +- [ ] Single commit, single file added +- [ ] Commit message: `New script name.py: short description…` +- [ ] No similar script already exists +- [ ] Name: max 32 chars, only lower case letters, digits and underscores +- [ ] Unique name, does not already exist in repository +- [ ] No shebang on the first line +- [ ] Comment in script with name/pseudo, e-mail and license using [SPDX](https://spdx.dev/) tags (see [Contributing guide](https://github.com/weechat/scripts/blob/main/CONTRIBUTING.md#copyright-and-license)) +- [ ] Only English in code/comments +- [ ] Pure WeeChat API used, no extra API +- [ ] Function `hook_url`, `hook_process` or `hook_process_hashtable` is used for any blocking call +- [ ] For Python script: works with Python 3 (Python 2 support is optional) +- [ ] Score 100 / 100 displayed by [weechat-script-lint](https://github.com/weechat/weechat-script-lint) + +## Checklist (script update) + + + + + +- [ ] Author has been contacted +- [ ] Single commit, single file added +- [ ] Commit message format: `script_name.py X.Y: …` +- [ ] Script version and Changelog have been updated +- [ ] For Python script: works with Python 3 (Python 2 support is optional) +- [ ] Score 100 / 100 displayed by [weechat-script-lint](https://github.com/weechat/weechat-script-lint) + +## Checklist (script deletion) + + + + + +- [ ] Author has been contacted +- [ ] Single commit, single file deleted +- [ ] Commit message format: `Remove script name.py` with reasons in description diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..b3014305 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2021-2025 Sébastien Helleu +# +# SPDX-License-Identifier: GPL-3.0-or-later + +name: CI + +on: + - push + - pull_request + +jobs: + check-scripts: + runs-on: ubuntu-24.04 + name: Check scripts + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: pip install weechat-script-lint + - name: Check scripts + run: make check diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml new file mode 100644 index 00000000..98fb6481 --- /dev/null +++ b/.github/workflows/reuse.yml @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2025 Sébastien Helleu +# +# SPDX-License-Identifier: GPL-3.0-or-later + +name: REUSE Compliance Check + +on: + - push + - pull_request + +jobs: + + test: + + runs-on: ubuntu-24.04 + + steps: + + - uses: actions/checkout@v4 + + - name: REUSE Compliance Check + uses: fsfe/reuse-action@v4 diff --git a/.mailmap b/.mailmap index bf4f93c3..83b6e852 100644 --- a/.mailmap +++ b/.mailmap @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2014-2025 Sébastien Helleu +# +# SPDX-License-Identifier: GPL-3.0-or-later +# # Map author and committer names and email addresses to canonical real names # and email addresses. # diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..370c7408 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,125 @@ + + +# Contributing to WeeChat scripts + +## Reporting an issue + +Issue must be reported to upstream repository, not this one (most of times, the upstream repository is mentioned in the script itself).\ +If you don't know where is the upstream repository, please contact directly the author by e-mail.\ +If you have no answer, or if the author has no time to fix the problem, then you can report the issue in the tracker or send an update of the script if you are able to fix yourself. + +## Testing pending scripts + +Your can help WeeChat team by testing the pending scripts (new scripts or updates): this makes script approbation faster and this ensures there are no major bugs in the script itself. + +To do that, you can download the script in the pull request and load it manually in WeeChat (for example: `/script load /path/to/new_script.py`).\ +Whether the script is working or not, please comment the pull request accordingly. + +Thank you for your help! + +## Adding a new script + +New scripts are added with pull requests against master branch of this repository, using the pull request template called `Add script`. + +### Guidelines + +**Important:** please fill the pull request template and follow **all** these rules, otherwise your new script will be rejected: + +- pull request: + - fill the pull request template + - make only one commit with one new file (the new script) in the appropriate directory, for example `python/` if you add a new Python script + - use this commit message: `New script name.py: short description…` +- script feature: + - check that no script or [pending script](https://github.com/weechat/scripts/pulls) does exactly same thing as your script +- script name: + - use max 32 chars, only lower case letters, digits and underscores + - use a unique name, not used by any other script, even in a different language + - use the script name (without extension) in the call to the `register` function + - do **NOT** use the word "weechat" in the script name: for example prefer `notify.py` to `weechat_notify.py` (the script is only for WeeChat) +- script content: + - do **NOT** use a shebang on the first line (like `#!/usr/bin/perl`), this is not needed + - write a comment at the beginning with your name (or pseudo), your e-mail and the chosen license, which must be free (see [Copyright and license](#copyright-and-license)) + - consider using [Semantic versioning](https://semver.org/) (recommended, not mandatory); only digits and dots are allowed in version + - use only English for code and comments + - do **NOT** use an extra API between WeeChat and your script (like Ruby gem "WeeChat"), use the standard WeeChat API only + - use function [hook_url](https://weechat.org/files/doc/stable/weechat_plugin_api.en.html#_hook_url) (WeeChat ≥ 4.1.0), [hook_process](https://weechat.org/files/doc/stable/weechat_plugin_api.en.html#_hook_process) or [hook_process_hashtable](https://weechat.org/files/doc/stable/weechat_plugin_api.en.html#_hook_process_hashtable) if your script is doing something blocking (like fetching URL), to not block WeeChat + - make your Python script compatible with Python 3.x, the support of Python 2.x is now optional + - use the official WeeChat URL: [https://weechat.org](https://weechat.org) (`https` and no `www.`) in any link to the WeeChat website. + +Your script is automatically checked in CI, see [Automatic checks on scripts](#automatic-checks-on-scripts). + +### Copyright and license + +The copyright and license must be present in header, using [SPDX](https://spdx.dev/) tags, see the [list of licenses](https://spdx.org/licenses/). + +Example of header in a Python script: + + + +```python +# SPDX-FileCopyrightText: 2025 Your Name +# +# SPDX-License-Identifier: GPL-3.0-or-later +``` + + + +## Updating a script + +### Contacting the author + +Before updating a script, if you are not the author of the script, you **must** contact the author of script directly, and discuss about your changes to check if it's OK, especially if you are adding new features or if you are changing the behavior of the script. + +For example, if the author uses a GitHub repository for the script, you can send a pull request to the author instead of sending directly to this repository.\ +Then the author will send a pull request on this repository. + +### Reporting a vulnerability + +Please **DO NOT** file a GitHub issue for security related problems, but send an email to [security@weechat.org](mailto:security@weechat.org) instead. + +### Sending the new release + +Scripts updates are made with pull requests against master branch of this repository, using the pull request template called `Fix script` or `Improve script`. + +**Important:** please fill the pull request template and follow **all** these rules, otherwise your script update will be rejected: + +- pull request: + - fill the pull request template + - make only one commit on one file in the pull request (the script actually updated); exceptions are allowed if motivated in the pull request description + - use this commit message: `name.py 1.3: fix some bug…` (`1.3` being the new version of script `name.py`) +- script content: + - update the version number in the script (used in `register` function) and the ChangeLog, if there is one + - do **NOT** update the author name in script (used in `register` function), it must always contain the original script author, even if you are doing large updates in the script + - make any Python script compatible with Python 3.x, the support of Python 2.x is now optional. + +The script is automatically checked in CI, see [Automatic checks on scripts](#automatic-checks-on-scripts). + +## Deleting a script + +Deleting a script must be done for a justified decision, for example such reasons are valid: + +- the feature implemented by the script has been implemented in WeeChat itself, so the script is no longer of interest +- the web service used by the script has been discontinued, so the script can not work at all any more +- the script uses dependencies that are not maintained any more or subject to vulnerabilities not fixed, thus impacting WeeChat itself. + +If you are not the author of the script, you must first contact the author to discuss about the deletion: the author could have a different opinion or could make changes to keep the script. + +**Important:** please fill the pull request template and follow **all** these rules, otherwise your script deletion will be rejected: + +- pull request: + - fill the pull request template + - make only one commit to delete only one script + - use this commit message: `Remove script name.py`, and it is recommended to explain the reasons in the commit description + +## Automatic checks on scripts + +Whenever a script is added or updated, the script `weechat-script-lint` is executed by CI in GitHub Actions and looks for errors in the script. + +You must run this script prior to submit the pull request, and the score displayed must be 100 / 100. +If errors or warnings are detected in the script, you must fix them before the script is manually tested/merged by the WeeChat team. + +See the [weechat-script-lint repository](https://github.com/weechat/weechat-script-lint) for more information about the checks performed and how to use the script. diff --git a/Contributing.adoc b/Contributing.adoc deleted file mode 100644 index 0f3b6399..00000000 --- a/Contributing.adoc +++ /dev/null @@ -1,65 +0,0 @@ -= Contributing to WeeChat scripts -:author: Sébastien Helleu -:email: flashcode@flashtux.org -:lang: en - - -== Add a script - -To submit a new script in this repository, please use the form at: -https://weechat.org/scripts/add/ - -There are strict rules for new scripts, please read them carefully, otherwise -your script will be rejected. - -Pending scripts are visible at: https://weechat.org/scripts/pending/ - -== Update a script - -=== Contact the author - -Before updating a script, you *must* contact the author of script directly, -and discuss about your changes (to check if it's OK), especially if you are -adding new features or if you are changing the behavior of the script. - -For example, if the author uses a GitHub repository for the script, you can -send a pull request to the author (instead of sending directly to this -repository). + -Then the author will send a pull request on this repository. - -=== Send new release - -There are two ways to send a new release for a script: - -* Send a pull request, the commit message must have the name of script, - version, and changes, like that: + - `script.py 0.3: fix...` + - Please submit only one script per pull request. -* Use the form at: . - -When sending a new version : - -* Don't forget to update the version number in the script (used in `register` - function) and the ChangeLog (if there is one). -* Do *not* update the author name in script (used in `register` function); - it must always contain the original script author, even if you are doing - large updates in the script. -* If the script is tagged `py3k-ok` (script running fine with Python 3.x), - please ensure that your update is still compatible with both - Python 2.x *AND* 3.x. - -== Report an issue - -Before reporting an issue, it's better to contact the author of script -directly. - -If you have no answer, or if the author has no time to fix the problem, then -you can report the issue in the tracker (or send an update of script if you are -able to fix yourself). - -When reporting an issue, please give the following info: - -* Your *WeeChat version*: the output of `/v` in WeeChat, for example: - _WeeChat 1.7-dev (git: v1.6-6-g997f47f)_. -* If possible, please include a reproducible example: explain the steps which - led you to the problem. diff --git a/LICENSES/0BSD.txt b/LICENSES/0BSD.txt new file mode 100644 index 00000000..0b8ae762 --- /dev/null +++ b/LICENSES/0BSD.txt @@ -0,0 +1,5 @@ +Copyright (C) YEAR by AUTHOR EMAIL + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/LICENSES/AGPL-3.0-or-later.txt b/LICENSES/AGPL-3.0-or-later.txt new file mode 100644 index 00000000..0c97efd2 --- /dev/null +++ b/LICENSES/AGPL-3.0-or-later.txt @@ -0,0 +1,235 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. + +A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. + +The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. + +An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. + +The precise terms and conditions for copying, distribution and modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the Program. + +To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 00000000..137069b8 --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSES/Artistic-2.0.txt b/LICENSES/Artistic-2.0.txt new file mode 100644 index 00000000..eb2e968e --- /dev/null +++ b/LICENSES/Artistic-2.0.txt @@ -0,0 +1,85 @@ +The Artistic License 2.0 + +Copyright (c) 2000-2006, The Perl Foundation. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +This license establishes the terms under which a given free software Package may be copied, modified, distributed, and/or redistributed. The intent is that the Copyright Holder maintains some artistic control over the development of that Package while still keeping the Package available as open source and free software. + +You are always permitted to make arrangements wholly outside of this license directly with the Copyright Holder of a given Package. If the terms of this license do not permit the full use that you propose to make of the Package, you should contact the Copyright Holder and seek a different licensing arrangement. + +Definitions + + "Copyright Holder" means the individual(s) or organization(s) named in the copyright notice for the entire Package. + + "Contributor" means any party that has contributed code or other material to the Package, in accordance with the Copyright Holder's procedures. + + "You" and "your" means any person who would like to copy, distribute, or modify the Package. + + "Package" means the collection of files distributed by the Copyright Holder, and derivatives of that collection and/or of those files. A given Package may consist of either the Standard Version, or a Modified Version. + + "Distribute" means providing a copy of the Package or making it accessible to anyone else, or in the case of a company or organization, to others outside of your company or organization. + + "Distributor Fee" means any fee that you charge for Distributing this Package or providing support for this Package to another party. It does not mean licensing fees. + + "Standard Version" refers to the Package if it has not been modified, or has been modified only in ways explicitly requested by the Copyright Holder. + + "Modified Version" means the Package, if it has been changed, and such changes were not explicitly requested by the Copyright Holder. + + "Original License" means this Artistic License as Distributed with the Standard Version of the Package, in its current version or as it may be modified by The Perl Foundation in the future. + + "Source" form means the source code, documentation source, and configuration files for the Package. + + "Compiled" form means the compiled bytecode, object code, binary, or any other form resulting from mechanical transformation or translation of the Source form. + +Permission for Use and Modification Without Distribution + +(1) You are permitted to use the Standard Version and create and use Modified Versions for any purpose without restriction, provided that you do not Distribute the Modified Version. + +Permissions for Redistribution of the Standard Version + +(2) You may Distribute verbatim copies of the Source form of the Standard Version of this Package in any medium without restriction, either gratis or for a Distributor Fee, provided that you duplicate all of the original copyright notices and associated disclaimers. At your discretion, such verbatim copies may or may not include a Compiled form of the Package. + +(3) You may apply any bug fixes, portability changes, and other modifications made available from the Copyright Holder. The resulting Package will still be considered the Standard Version, and as such will be subject to the Original License. + +Distribution of Modified Versions of the Package as Source + +(4) You may Distribute your Modified Version as Source (either gratis or for a Distributor Fee, and with or without a Compiled form of the Modified Version) provided that you clearly document how it differs from the Standard Version, including, but not limited to, documenting any non-standard features, executables, or modules, and provided that you do at least ONE of the following: + + (a) make the Modified Version available to the Copyright Holder of the Standard Version, under the Original License, so that the Copyright Holder may include your modifications in the Standard Version. + (b) ensure that installation of your Modified Version does not prevent the user installing or running the Standard Version. In addition, the Modified Version must bear a name that is different from the name of the Standard Version. + (c) allow anyone who receives a copy of the Modified Version to make the Source form of the Modified Version available to others under + + (i) the Original License or + (ii) a license that permits the licensee to freely copy, modify and redistribute the Modified Version using the same licensing terms that apply to the copy that the licensee received, and requires that the Source form of the Modified Version, and of any works derived from it, be made freely available in that license fees are prohibited but Distributor Fees are allowed. + +Distribution of Compiled Forms of the Standard Version or Modified Versions without the Source + +(5) You may Distribute Compiled forms of the Standard Version without the Source, provided that you include complete instructions on how to get the Source of the Standard Version. Such instructions must be valid at the time of your distribution. If these instructions, at any time while you are carrying out such distribution, become invalid, you must provide new instructions on demand or cease further distribution. If you provide valid instructions or cease distribution within thirty days after you become aware that the instructions are invalid, then you do not forfeit any of your rights under this license. + +(6) You may Distribute a Modified Version in Compiled form without the Source, provided that you comply with Section 4 with respect to the Source of the Modified Version. + +Aggregating or Linking the Package + +(7) You may aggregate the Package (either the Standard Version or Modified Version) with other packages and Distribute the resulting aggregation provided that you do not charge a licensing fee for the Package. Distributor Fees are permitted, and licensing fees for other components in the aggregation are permitted. The terms of this license apply to the use and Distribution of the Standard or Modified Versions as included in the aggregation. + +(8) You are permitted to link Modified and Standard Versions with other works, to embed the Package in a larger work of your own, or to build stand-alone binary or bytecode versions of applications that include the Package, and Distribute the result without restriction, provided the result does not expose a direct interface to the Package. + +Items That are Not Considered Part of a Modified Version + +(9) Works (including, but not limited to, modules and scripts) that merely extend or make use of the Package, do not, by themselves, cause the Package to be a Modified Version. In addition, such works are not considered parts of the Package itself, and are not subject to the terms of this license. + +General Provisions + +(10) Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License. By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license. + +(11) If your Modified Version has been derived from a Modified Version made by someone other than you, you are nevertheless required to ensure that your Modified Version complies with the requirements of this license. + +(12) This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder. + +(13) This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement, then this Artistic License to you shall terminate on the date that such litigation is filed. + +(14) Disclaimer of Warranty: +THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/BSD-2-Clause.txt b/LICENSES/BSD-2-Clause.txt new file mode 100644 index 00000000..eb3c575b --- /dev/null +++ b/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,9 @@ +Copyright (c) + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt new file mode 100644 index 00000000..086d3992 --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,11 @@ +Copyright (c) . + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/Beerware.txt b/LICENSES/Beerware.txt new file mode 100644 index 00000000..c7ffc1a0 --- /dev/null +++ b/LICENSES/Beerware.txt @@ -0,0 +1 @@ +"THE BEER-WARE LICENSE" (Revision 42): wrote this file. As long as you retain this notice you can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp diff --git a/LICENSES/CC-BY-SA-3.0.txt b/LICENSES/CC-BY-SA-3.0.txt new file mode 100644 index 00000000..604209a8 --- /dev/null +++ b/LICENSES/CC-BY-SA-3.0.txt @@ -0,0 +1,359 @@ +Creative Commons Legal Code + +Attribution-ShareAlike 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR + DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY +BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and + other pre-existing works, such as a translation, adaptation, + derivative work, arrangement of music or other alterations of a + literary or artistic work, or phonogram or performance and includes + cinematographic adaptations or any other form in which the Work may be + recast, transformed, or adapted including in any form recognizably + derived from the original, except that a work that constitutes a + Collection will not be considered an Adaptation for the purpose of + this License. For the avoidance of doubt, where the Work is a musical + work, performance or phonogram, the synchronization of the Work in + timed-relation with a moving image ("synching") will be considered an + Adaptation for the purpose of this License. + b. "Collection" means a collection of literary or artistic works, such as + encyclopedias and anthologies, or performances, phonograms or + broadcasts, or other works or subject matter other than works listed + in Section 1(f) below, which, by reason of the selection and + arrangement of their contents, constitute intellectual creations, in + which the Work is included in its entirety in unmodified form along + with one or more other contributions, each constituting separate and + independent works in themselves, which together are assembled into a + collective whole. A work that constitutes a Collection will not be + considered an Adaptation (as defined below) for the purposes of this + License. + c. "Creative Commons Compatible License" means a license that is listed + at https://creativecommons.org/compatiblelicenses that has been + approved by Creative Commons as being essentially equivalent to this + License, including, at a minimum, because that license: (i) contains + terms that have the same purpose, meaning and effect as the License + Elements of this License; and, (ii) explicitly permits the relicensing + of adaptations of works made available under that license under this + License or a Creative Commons jurisdiction license with the same + License Elements as this License. + d. "Distribute" means to make available to the public the original and + copies of the Work or Adaptation, as appropriate, through sale or + other transfer of ownership. + e. "License Elements" means the following high-level license attributes + as selected by Licensor and indicated in the title of this License: + Attribution, ShareAlike. + f. "Licensor" means the individual, individuals, entity or entities that + offer(s) the Work under the terms of this License. + g. "Original Author" means, in the case of a literary or artistic work, + the individual, individuals, entity or entities who created the Work + or if no individual or entity can be identified, the publisher; and in + addition (i) in the case of a performance the actors, singers, + musicians, dancers, and other persons who act, sing, deliver, declaim, + play in, interpret or otherwise perform literary or artistic works or + expressions of folklore; (ii) in the case of a phonogram the producer + being the person or legal entity who first fixes the sounds of a + performance or other sounds; and, (iii) in the case of broadcasts, the + organization that transmits the broadcast. + h. "Work" means the literary and/or artistic work offered under the terms + of this License including without limitation any production in the + literary, scientific and artistic domain, whatever may be the mode or + form of its expression including digital form, such as a book, + pamphlet and other writing; a lecture, address, sermon or other work + of the same nature; a dramatic or dramatico-musical work; a + choreographic work or entertainment in dumb show; a musical + composition with or without words; a cinematographic work to which are + assimilated works expressed by a process analogous to cinematography; + a work of drawing, painting, architecture, sculpture, engraving or + lithography; a photographic work to which are assimilated works + expressed by a process analogous to photography; a work of applied + art; an illustration, map, plan, sketch or three-dimensional work + relative to geography, topography, architecture or science; a + performance; a broadcast; a phonogram; a compilation of data to the + extent it is protected as a copyrightable work; or a work performed by + a variety or circus performer to the extent it is not otherwise + considered a literary or artistic work. + i. "You" means an individual or entity exercising rights under this + License who has not previously violated the terms of this License with + respect to the Work, or who has received express permission from the + Licensor to exercise rights under this License despite a previous + violation. + j. "Publicly Perform" means to perform public recitations of the Work and + to communicate to the public those public recitations, by any means or + process, including by wire or wireless means or public digital + performances; to make available to the public Works in such a way that + members of the public may access these Works from a place and at a + place individually chosen by them; to perform the Work to the public + by any means or process and the communication to the public of the + performances of the Work, including by public digital performance; to + broadcast and rebroadcast the Work by any means including signs, + sounds or images. + k. "Reproduce" means to make copies of the Work by any means including + without limitation by sound or visual recordings and the right of + fixation and reproducing fixations of the Work, including storage of a + protected performance or phonogram in digital form or other electronic + medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, +limit, or restrict any uses free from copyright or rights arising from +limitations or exceptions that are provided for in connection with the +copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +perpetual (for the duration of the applicable copyright) license to +exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more + Collections, and to Reproduce the Work as incorporated in the + Collections; + b. to create and Reproduce Adaptations provided that any such Adaptation, + including any translation in any medium, takes reasonable steps to + clearly label, demarcate or otherwise identify that changes were made + to the original Work. For example, a translation could be marked "The + original work was translated from English to Spanish," or a + modification could indicate "The original work has been modified."; + c. to Distribute and Publicly Perform the Work including as incorporated + in Collections; and, + d. to Distribute and Publicly Perform Adaptations. + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor + reserves the exclusive right to collect such royalties for any + exercise by You of the rights granted under this License; + ii. Waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor waives the + exclusive right to collect such royalties for any exercise by You + of the rights granted under this License; and, + iii. Voluntary License Schemes. The Licensor waives the right to + collect royalties, whether individually or, in the event that the + Licensor is a member of a collecting society that administers + voluntary licensing schemes, via that society, from any exercise + by You of the rights granted under this License. + +The above rights may be exercised in all media and formats whether now +known or hereafter devised. The above rights include the right to make +such modifications as are technically necessary to exercise the rights in +other media and formats. Subject to Section 8(f), all rights not expressly +granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms + of this License. You must include a copy of, or the Uniform Resource + Identifier (URI) for, this License with every copy of the Work You + Distribute or Publicly Perform. You may not offer or impose any terms + on the Work that restrict the terms of this License or the ability of + the recipient of the Work to exercise the rights granted to that + recipient under the terms of the License. You may not sublicense the + Work. You must keep intact all notices that refer to this License and + to the disclaimer of warranties with every copy of the Work You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Work, You may not impose any effective technological + measures on the Work that restrict the ability of a recipient of the + Work from You to exercise the rights granted to that recipient under + the terms of the License. This Section 4(a) applies to the Work as + incorporated in a Collection, but this does not require the Collection + apart from the Work itself to be made subject to the terms of this + License. If You create a Collection, upon notice from any Licensor You + must, to the extent practicable, remove from the Collection any credit + as required by Section 4(c), as requested. If You create an + Adaptation, upon notice from any Licensor You must, to the extent + practicable, remove from the Adaptation any credit as required by + Section 4(c), as requested. + b. You may Distribute or Publicly Perform an Adaptation only under the + terms of: (i) this License; (ii) a later version of this License with + the same License Elements as this License; (iii) a Creative Commons + jurisdiction license (either this or a later license version) that + contains the same License Elements as this License (e.g., + Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible + License. If you license the Adaptation under one of the licenses + mentioned in (iv), you must comply with the terms of that license. If + you license the Adaptation under the terms of any of the licenses + mentioned in (i), (ii) or (iii) (the "Applicable License"), you must + comply with the terms of the Applicable License generally and the + following provisions: (I) You must include a copy of, or the URI for, + the Applicable License with every copy of each Adaptation You + Distribute or Publicly Perform; (II) You may not offer or impose any + terms on the Adaptation that restrict the terms of the Applicable + License or the ability of the recipient of the Adaptation to exercise + the rights granted to that recipient under the terms of the Applicable + License; (III) You must keep intact all notices that refer to the + Applicable License and to the disclaimer of warranties with every copy + of the Work as included in the Adaptation You Distribute or Publicly + Perform; (IV) when You Distribute or Publicly Perform the Adaptation, + You may not impose any effective technological measures on the + Adaptation that restrict the ability of a recipient of the Adaptation + from You to exercise the rights granted to that recipient under the + terms of the Applicable License. This Section 4(b) applies to the + Adaptation as incorporated in a Collection, but this does not require + the Collection apart from the Adaptation itself to be made subject to + the terms of the Applicable License. + c. If You Distribute, or Publicly Perform the Work or any Adaptations or + Collections, You must, unless a request has been made pursuant to + Section 4(a), keep intact all copyright notices for the Work and + provide, reasonable to the medium or means You are utilizing: (i) the + name of the Original Author (or pseudonym, if applicable) if supplied, + and/or if the Original Author and/or Licensor designate another party + or parties (e.g., a sponsor institute, publishing entity, journal) for + attribution ("Attribution Parties") in Licensor's copyright notice, + terms of service or by other reasonable means, the name of such party + or parties; (ii) the title of the Work if supplied; (iii) to the + extent reasonably practicable, the URI, if any, that Licensor + specifies to be associated with the Work, unless such URI does not + refer to the copyright notice or licensing information for the Work; + and (iv) , consistent with Ssection 3(b), in the case of an + Adaptation, a credit identifying the use of the Work in the Adaptation + (e.g., "French translation of the Work by Original Author," or + "Screenplay based on original Work by Original Author"). The credit + required by this Section 4(c) may be implemented in any reasonable + manner; provided, however, that in the case of a Adaptation or + Collection, at a minimum such credit will appear, if a credit for all + contributing authors of the Adaptation or Collection appears, then as + part of these credits and in a manner at least as prominent as the + credits for the other contributing authors. For the avoidance of + doubt, You may only use the credit required by this Section for the + purpose of attribution in the manner set out above and, by exercising + Your rights under this License, You may not implicitly or explicitly + assert or imply any connection with, sponsorship or endorsement by the + Original Author, Licensor and/or Attribution Parties, as appropriate, + of You or Your use of the Work, without the separate, express prior + written permission of the Original Author, Licensor and/or Attribution + Parties. + d. Except as otherwise agreed in writing by the Licensor or as may be + otherwise permitted by applicable law, if You Reproduce, Distribute or + Publicly Perform the Work either by itself or as part of any + Adaptations or Collections, You must not distort, mutilate, modify or + take other derogatory action in relation to the Work which would be + prejudicial to the Original Author's honor or reputation. Licensor + agrees that in those jurisdictions (e.g. Japan), in which any exercise + of the right granted in Section 3(b) of this License (the right to + make Adaptations) would be deemed to be a distortion, mutilation, + modification or other derogatory action prejudicial to the Original + Author's honor and reputation, the Licensor will waive or not assert, + as appropriate, this Section, to the fullest extent permitted by the + applicable national law, to enable You to reasonably exercise Your + right under Section 3(b) of this License (right to make Adaptations) + but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR +OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY +KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, +INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, +FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF +LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, +WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION +OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE +LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR +ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES +ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this License. + Individuals or entities who have received Adaptations or Collections + from You under this License, however, will not have their licenses + terminated provided such individuals or entities remain in full + compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will + survive any termination of this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the + Work under different license terms or to stop distributing the Work at + any time; provided, however that any such election will not serve to + withdraw this License (or any other license that has been, or is + required to be, granted under the terms of this License), and this + License will continue in full force and effect unless terminated as + stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, + the Licensor offers to the recipient a license to the Work on the same + terms and conditions as the license granted to You under this License. + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor + offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this License, and without further action + by the parties to this agreement, such provision shall be reformed to + the minimum extent necessary to make such provision valid and + enforceable. + d. No term or provision of this License shall be deemed waived and no + breach consented to unless such waiver or consent shall be in writing + and signed by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, + agreements or representations with respect to the Work not specified + here. Licensor shall not be bound by any additional provisions that + may appear in any communication from You. This License may not be + modified without the mutual written agreement of the Licensor and You. + f. The rights granted under, and the subject matter referenced, in this + License were drafted utilizing the terminology of the Berne Convention + for the Protection of Literary and Artistic Works (as amended on + September 28, 1979), the Rome Convention of 1961, the WIPO Copyright + Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 + and the Universal Copyright Convention (as revised on July 24, 1971). + These rights and subject matter take effect in the relevant + jurisdiction in which the License terms are sought to be enforced + according to the corresponding provisions of the implementation of + those treaty provisions in the applicable national law. If the + standard suite of rights granted under applicable copyright law + includes additional rights not granted under this License, such + additional rights are deemed to be included in the License; this + License is not intended to restrict the license of any rights under + applicable law. + + +Creative Commons Notice + + Creative Commons is not a party to this License, and makes no warranty + whatsoever in connection with the Work. Creative Commons will not be + liable to You or any party on any legal theory for any damages + whatsoever, including without limitation any general, special, + incidental or consequential damages arising in connection to this + license. Notwithstanding the foregoing two (2) sentences, if Creative + Commons has expressly identified itself as the Licensor hereunder, it + shall have all rights and obligations of Licensor. + + Except for the limited purpose of indicating to the public that the + Work is licensed under the CCPL, Creative Commons does not authorize + the use by either party of the trademark "Creative Commons" or any + related trademark or logo of Creative Commons without the prior + written consent of Creative Commons. Any permitted use will be in + compliance with Creative Commons' then-current trademark usage + guidelines, as may be published on its website or otherwise made + available upon request from time to time. For the avoidance of doubt, + this trademark restriction does not form part of the License. + + Creative Commons may be contacted at https://creativecommons.org/. diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/LICENSES/GPL-2.0-only.txt b/LICENSES/GPL-2.0-only.txt new file mode 100644 index 00000000..17cb2864 --- /dev/null +++ b/LICENSES/GPL-2.0-only.txt @@ -0,0 +1,117 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. + + c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author + + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/LICENSES/GPL-2.0-or-later.txt b/LICENSES/GPL-2.0-or-later.txt new file mode 100644 index 00000000..17cb2864 --- /dev/null +++ b/LICENSES/GPL-2.0-or-later.txt @@ -0,0 +1,117 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. + + c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author + + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/LICENSES/GPL-3.0-only.txt b/LICENSES/GPL-3.0-only.txt new file mode 100644 index 00000000..f6cdd22a --- /dev/null +++ b/LICENSES/GPL-3.0-only.txt @@ -0,0 +1,232 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/LICENSES/GPL-3.0-or-later.txt b/LICENSES/GPL-3.0-or-later.txt new file mode 100644 index 00000000..f6cdd22a --- /dev/null +++ b/LICENSES/GPL-3.0-or-later.txt @@ -0,0 +1,232 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/LICENSES/ISC.txt b/LICENSES/ISC.txt new file mode 100644 index 00000000..b9c199c9 --- /dev/null +++ b/LICENSES/ISC.txt @@ -0,0 +1,8 @@ +ISC License: + +Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC") +Copyright (c) 1995-2003 by Internet Software Consortium + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/LICENSES/MIT-0.txt b/LICENSES/MIT-0.txt new file mode 100644 index 00000000..a4e9dc90 --- /dev/null +++ b/LICENSES/MIT-0.txt @@ -0,0 +1,16 @@ +MIT No Attribution + +Copyright + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 00000000..d817195d --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSES/WTFPL.txt b/LICENSES/WTFPL.txt new file mode 100644 index 00000000..7a3094a8 --- /dev/null +++ b/LICENSES/WTFPL.txt @@ -0,0 +1,11 @@ +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..cc091696 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# +# SPDX-FileCopyrightText: 2021-2025 Sébastien Helleu +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +CHECKER ?= weechat-script-lint + +.PHONY: all check + +all: check + +check: + "$(CHECKER)" --recursive . diff --git a/README.adoc b/README.adoc deleted file mode 100644 index 52731e73..00000000 --- a/README.adoc +++ /dev/null @@ -1,13 +0,0 @@ -= WeeChat official scripts -:author: Sébastien Helleu -:email: flashcode@flashtux.org -:lang: en - -These official scripts can be installed with command `/script install ` -in WeeChat. - -They are also displayed on https://weechat.org/scripts/ - -== Add/update a script - -See the file https://github.com/weechat/scripts/blob/master/Contributing.adoc[Contributing.adoc]. diff --git a/README.md b/README.md new file mode 100644 index 00000000..40991ccb --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ + + +# WeeChat official scripts + +[![CI Status](https://github.com/weechat/scripts/workflows/CI/badge.svg)](https://github.com/weechat/scripts/actions?query=workflow%3A%22CI%22) +[![REUSE status](https://api.reuse.software/badge/github.com/weechat/scripts)](https://api.reuse.software/info/github.com/weechat/scripts) + +These official scripts can be installed with command `/script install ` in WeeChat. + +They are also displayed on [https://weechat.org/scripts/](https://weechat.org/scripts/). + +## Contributing + +To add/update a script or report an issue, see the [Contributing guide](CONTRIBUTING.md). diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 00000000..83735da3 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,2310 @@ +# SPDX-FileCopyrightText: 2025 Sébastien Helleu +# +# SPDX-License-Identifier: GPL-3.0-or-later + +version = 1 + +[[annotations]] +path = [ + ".github/ISSUE_TEMPLATE/*.md", + ".github/pull_request_template.md", +] +precedence = "override" +SPDX-FileCopyrightText = "2006-2025 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "guile/emote.scm" +precedence = "override" +SPDX-FileCopyrightText = "2014 csmith " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "guile/gateway_rename.scm" +precedence = "override" +SPDX-FileCopyrightText = "2016-2017 Zephyr Pellerin " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "guile/karmastorm.scm" +precedence = "override" +SPDX-FileCopyrightText = "2014-2016 msoucy " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "guile/weechataboo.scm" +precedence = "override" +SPDX-FileCopyrightText = "2017-2018 Alvar " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "javascript/autospurdo.js" +precedence = "override" +SPDX-FileCopyrightText = "2015-2016 installgen2 " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "javascript/opall.js" +precedence = "override" +SPDX-FileCopyrightText = "2019 gagz " +SPDX-License-Identifier = "WTFPL" + +[[annotations]] +path = "lua/cmus_announce.lua" +precedence = "override" +SPDX-FileCopyrightText = "2024 lan Nomar " +SPDX-License-Identifier = "0BSD" + +[[annotations]] +path = "lua/emoji.lua" +precedence = "override" +SPDX-FileCopyrightText = "2016 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "lua/http_item.lua" +precedence = "override" +SPDX-FileCopyrightText = "2013 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "lua/mpdbitl.lua" +precedence = "override" +SPDX-FileCopyrightText = "2012 rumia " +SPDX-License-Identifier = "WTFPL" + +[[annotations]] +path = "lua/nick_complete_wrapper.lua" +precedence = "override" +SPDX-FileCopyrightText = "2016 singalaut " +SPDX-License-Identifier = "WTFPL" + +[[annotations]] +path = "lua/oldswarner.lua" +precedence = "override" +SPDX-FileCopyrightText = "2013 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "lua/pastebuf.lua" +precedence = "override" +SPDX-FileCopyrightText = "2014 tomoe-mami " +SPDX-License-Identifier = "WTFPL" + +[[annotations]] +path = "lua/text_effects.lua" +precedence = "override" +SPDX-FileCopyrightText = "2010 Vaughan Newton " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "lua/urlselect.lua" +precedence = "override" +SPDX-FileCopyrightText = "2014 tomoe-mami/singalaut " +SPDX-License-Identifier = "WTFPL" + +[[annotations]] +path = "perl/amarok2.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009-2012 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/audacious.pl" +precedence = "override" +SPDX-FileCopyrightText = "2006 DeltaS4 " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "perl/autonickprefix.pl" +precedence = "override" +SPDX-FileCopyrightText = "2018 Juerd <#####@juerd.nl>" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "perl/awaylog.pl" +precedence = "override" +SPDX-FileCopyrightText = "2008 Jiri Golembiovsky " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "perl/bashorg.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011 Trashlord " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "perl/beat.pl" +precedence = "override" +SPDX-FileCopyrightText = "2010 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/buddylist.pl" +precedence = "override" +SPDX-FileCopyrightText = [ + "2009-2023 Nils Görs ", + "2025 Kamil Wiśniewski ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/chanmon.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009-2014 KenjiE20 " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/chatters.pl" +precedence = "override" +SPDX-FileCopyrightText = "2012 Arvydas Sidorenko " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/cmdind.pl" +precedence = "override" +SPDX-FileCopyrightText = "2013 Nei " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/colorize_lines.pl" +precedence = "override" +SPDX-FileCopyrightText = [ + "2010-2019 Nils Görs ", + "2013 oakkitten", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/color_popup.pl" +precedence = "override" +SPDX-FileCopyrightText = "2013-2015 Nei " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/commorkers.pl" +precedence = "override" +SPDX-FileCopyrightText = "2015 Al-Caveman " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/coords.pl" +precedence = "override" +SPDX-FileCopyrightText = "2012-2014 Nei " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/ctrl_w.pl" +precedence = "override" +SPDX-FileCopyrightText = "2018-2019 Juerd <#####@juerd.nl>" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "perl/curiousignore.pl" +precedence = "override" +SPDX-FileCopyrightText = "2010-2013 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/dellog.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009-2010 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/dzen_notifier.pl" +precedence = "override" +SPDX-FileCopyrightText = "2015 apendragon " +SPDX-License-Identifier = "Artistic-2.0" + +[[annotations]] +path = "perl/expand_url.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011-2019 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/filter_ext.pl" +precedence = "override" +SPDX-FileCopyrightText = "2010-2011 rettub " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/foo.pl" +precedence = "override" +SPDX-FileCopyrightText = "2018 Juerd <#####@juerd.nl>" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "perl/foo_spam.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009-2010 Diogo Franco " +SPDX-License-Identifier = "ISC" + +[[annotations]] +path = "perl/format_lines.pl" +precedence = "override" +SPDX-FileCopyrightText = "2013 R1cochet " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/fortune.pl" +precedence = "override" +SPDX-FileCopyrightText = [ + "2006 Julien Louis ", + "2009 Sid Vicious (Trashlord) ", + "2004 Ivo Marino ", +] +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "perl/growl_net_notify.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009 kinabalu " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "perl/highmon.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009-2014 KenjiE20 " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/hl_here.pl" +precedence = "override" +SPDX-FileCopyrightText = "2013 Sascha Ohms " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/hotlist2extern.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009-2018 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/isgd.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011-2014 stfn " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/jnotify.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009-2012 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/join2fast.pl" +precedence = "override" +SPDX-FileCopyrightText = "2012-2013 Stefan Wold " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/jump_smart_closest.pl" +precedence = "override" +SPDX-FileCopyrightText = "2018 arza " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/kernel.pl" +precedence = "override" +SPDX-FileCopyrightText = "2006 Julien Louis " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "perl/kickban.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011-2014 arza " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/kikoo.pl" +precedence = "override" +SPDX-FileCopyrightText = "2006 DeltaS4 " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "perl/launcher.pl" +precedence = "override" +SPDX-FileCopyrightText = [ + "2009-2011 Sébastien Helleu ", + "2010 James Campos ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/listsort.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011 arza " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/luanma.pl" +precedence = "override" +SPDX-FileCopyrightText = "2013 Nei " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/maildir.pl" +precedence = "override" +SPDX-FileCopyrightText = "2012 Yoran Heling " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "perl/mass_hl_blocker.pl" +precedence = "override" +SPDX-FileCopyrightText = "2013 arza " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/mastermind.pl" +precedence = "override" +SPDX-FileCopyrightText = "2008-2009 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/mnick.pl" +precedence = "override" +SPDX-FileCopyrightText = "2014-2019 CrazyCat " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/moc.pl" +precedence = "override" +SPDX-FileCopyrightText = "2006-2009 Jiri Golembiovsky " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "perl/mplex.pl" +precedence = "override" +SPDX-FileCopyrightText = "2010 rettub " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/ncmpcpp.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011 stfn " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/newsbar.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009-2014 rettub " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/nickregain.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009 KenjiE20 " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/notifym.pl" +precedence = "override" +SPDX-FileCopyrightText = [ + "2016-2019 Mitescu George Dan ", + "2019 Silvan Mosberger ", + "2016 Berechet Mihai ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/notify_send.pl" +precedence = "override" +SPDX-FileCopyrightText = "2013-2017 shmibs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/parse_relayed_msg.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011-2019 w8rabbit " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/perlexec.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011 arza " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/pop3_mail.pl" +precedence = "override" +SPDX-FileCopyrightText = "2010-2013 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/pushover.pl" +precedence = "override" +SPDX-FileCopyrightText = "2013-2017 stfn " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/pv_info.pl" +precedence = "override" +SPDX-FileCopyrightText = "2018 Max Wölfing " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/query_blocker.pl" +precedence = "override" +SPDX-FileCopyrightText = [ + "2009-2014 rettub ", + "2011-2025 Nils Görs ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/ragefaces.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011 stfn " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/recoverop.pl" +precedence = "override" +SPDX-FileCopyrightText = "2012 Ryuunosuke AYANOKOUZI " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/rhythmbox.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009 jnbek " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "perl/rhythmbus.pl" +precedence = "override" +SPDX-FileCopyrightText = "2012 R1cochet " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/rslap.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009-2010 KenjiE20 " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/rssagg.pl" +precedence = "override" +SPDX-FileCopyrightText = "2012-2013 R1cochet " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/seeks.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011 Fabien Dupont " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/shuffle.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009 Sid Vicious (Trashlord) " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "perl/snake.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011 arza " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/snarl_net_notify.pl" +precedence = "override" +SPDX-FileCopyrightText = [ + "2010 Eric Harmon", + "2009 kinabalu ", +] +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "perl/sort_arza.pl" +precedence = "override" +SPDX-FileCopyrightText = "2015 arza " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/sort_buffers.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011-2013 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/sound.pl" +precedence = "override" +SPDX-FileCopyrightText = "2006-2009 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/spacer.pl" +precedence = "override" +SPDX-FileCopyrightText = "2013 Biohazard " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/spell_menu.pl" +precedence = "override" +SPDX-FileCopyrightText = "2013 Nei " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/stalker.pl" +precedence = "override" +SPDX-FileCopyrightText = [ + "2013-2018 Nils Görs ", + "2013-2014 Stefan Wold ", + "2010-2013 Kaitlyn Parkhurst (SymKat) ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/stats_bar.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009 wishbone " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/strmon.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009-2017 Stravy " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/thinklight_blink.pl" +precedence = "override" +SPDX-FileCopyrightText = "2010-2011 trenki " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/topicsed.pl" +precedence = "override" +SPDX-FileCopyrightText = [ + "2011 Nils Görs ", + "2011 stfn ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/ubus.pl" +precedence = "override" +SPDX-FileCopyrightText = "2010-2011 Arvid Picciani " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/unset_unused.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011-2019 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/url_arza.pl" +precedence = "override" +SPDX-FileCopyrightText = "2013-2019 arza " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/volumeter.pl" +precedence = "override" +SPDX-FileCopyrightText = "2009 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/weerock.pl" +precedence = "override" +SPDX-FileCopyrightText = "2010 Sebastian Köhler " +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "perl/wmiibar.pl" +precedence = "override" +SPDX-FileCopyrightText = "2010 Sebastian Köhler " +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "perl/xclip.pl" +precedence = "override" +SPDX-FileCopyrightText = "2011 stfn " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/xterm_paste.pl" +precedence = "override" +SPDX-FileCopyrightText = "2012 Nei " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "perl/yaurls.pl" +precedence = "override" +SPDX-FileCopyrightText = "2012 R1cochet " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/aesthetic.py" +precedence = "override" +SPDX-FileCopyrightText = "2019 Wojciech Siewierski " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/aformat.py" +precedence = "override" +SPDX-FileCopyrightText = "2016-2018 Hairo R. Carela " +SPDX-License-Identifier = "WTFPL" + +[[annotations]] +path = "python/alternatetz.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2010 Chmouel Boudjnah ", + "2012-2013 bwidawsk ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/announce_url_title.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2012 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/anotify.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2012 magnific0 ", + "2011 Sorin Ionescu ", +] +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/anti_password.py" +precedence = "override" +SPDX-FileCopyrightText = "2021 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/apply_corrections.py" +precedence = "override" +SPDX-FileCopyrightText = "2012 Chris Johnson " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/arespond.py" +precedence = "override" +SPDX-FileCopyrightText = "2011 Stephan Huebner " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/autoauth.py" +precedence = "override" +SPDX-FileCopyrightText = "2005 Emmanuel Bouthenot " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "python/auto_away.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 Specimen " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/autobump.py" +precedence = "override" +SPDX-FileCopyrightText = "2018-2019 Daniel Kessler " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/autoconf.py" +precedence = "override" +SPDX-FileCopyrightText = "2017 Manu Koell " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/autoconnect.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2011-2012 Arnaud Renevier ", + "2016 kbdkode ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/autojoin_on_invite.py" +precedence = "override" +SPDX-FileCopyrightText = "2009 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/automarkbuffer.py" +precedence = "override" +SPDX-FileCopyrightText = "2015 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/automerge.py" +precedence = "override" +SPDX-FileCopyrightText = "2017 Ricky Brent " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "python/automode.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2010 Elián Hanisch " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/autosavekey.py" +precedence = "override" +SPDX-FileCopyrightText = "2013-2019 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/autosort.py" +precedence = "override" +SPDX-FileCopyrightText = "2013-2023 Maarten de Vries " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/away_action.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2010 Tor Hveem ", + "2010 Elián Hanisch ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/axolotl.py" +precedence = "override" +SPDX-FileCopyrightText = "2014-2016 David R. Andersen " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/bandwidth.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2009 Tor Hveem ", + "2011 quazgaa ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/beinc.py" +precedence = "override" +SPDX-FileCopyrightText = "2013-2024 Simeon Simeonov " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/biditext.py" +precedence = "override" +SPDX-FileCopyrightText = "2012 Oscar Morante " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/bitlbee_completion.py" +precedence = "override" +SPDX-FileCopyrightText = "2015 Roger Duran " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/bitlbee_typing_notice.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2010 Alexander Schremmer ", + "2013, 2020 Corey Halpin ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/buffer_autoclose.py" +precedence = "override" +SPDX-FileCopyrightText = "2009 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/buffer_autohide.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2017-2019 Matthias Adamczyk ", + "2019 Marco Trevisan ", +] +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/buffer_autoset.py" +precedence = "override" +SPDX-FileCopyrightText = "2010-2021 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/buffer_bind.py" +precedence = "override" +SPDX-FileCopyrightText = "2017 Trevor 'tee' Slocum " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/buffer_dmenu.py" +precedence = "override" +SPDX-FileCopyrightText = "2017-2020 Ferus Castor " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/buffer_open.py" +precedence = "override" +SPDX-FileCopyrightText = "2019 Simmo Saan " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/bufsave.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2009 Tor Hveem ", + "2012-2021 Sébastien Helleu ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/bufsize.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2012-2017 Nils Görs ", + "2012-2017 nesthib ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/chanact.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2009-2010 Tor Hveem ", + "2011 mythmon ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/chancomp.py" +precedence = "override" +SPDX-FileCopyrightText = "2015-2016 Jos Ahrens " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/chanop.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2013 Elián Hanisch " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/chanotify.py" +precedence = "override" +SPDX-FileCopyrightText = "2018 manzerbredes " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/chanpriority.py" +precedence = "override" +SPDX-FileCopyrightText = "2011-2012 Barbu Paul - Gheorghe " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/clemenshow.py" +precedence = "override" +SPDX-FileCopyrightText = "2011 Leigh MacDonald " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/clemy.py" +precedence = "override" +SPDX-FileCopyrightText = "2014 Darth-O-Ring " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/clone_scanner.py" +precedence = "override" +SPDX-FileCopyrightText = "2011-2015 Filip H.F. 'FiXato' Slagter " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/cmd_help.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2011-2018 Sébastien Helleu ", + "2012 arza ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/cmdqueue.py" +precedence = "override" +SPDX-FileCopyrightText = "2011 walk " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/cmus.py" +precedence = "override" +SPDX-FileCopyrightText = "2013 Isaac Ross " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "python/collapse_channel.py" +precedence = "override" +SPDX-FileCopyrightText = "2019-2024 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/completion.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 Elián Hanisch " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/confversion.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 David Rubin " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/correction_completion.py" +precedence = "override" +SPDX-FileCopyrightText = "2011-2020 Pascal Wittmann " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/cron.py" +precedence = "override" +SPDX-FileCopyrightText = "2010-2021 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/crypt.py" +precedence = "override" +SPDX-FileCopyrightText = "2008-2014 Nicolai Lissner " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/customize_bar.py" +precedence = "override" +SPDX-FileCopyrightText = "2012-2018 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/dcc_antispam.py" +precedence = "override" +SPDX-FileCopyrightText = "2010-2012 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/deadbeef_np.py" +precedence = "override" +SPDX-FileCopyrightText = "2014 mwgg " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/detach_away.py" +precedence = "override" +SPDX-FileCopyrightText = "2017 p3lim " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/digraph.py" +precedence = "override" +SPDX-FileCopyrightText = "2023 narodnik " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/emoji2alias.py" +precedence = "override" +SPDX-FileCopyrightText = "2016 Wil Clouser " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/emoji2ascii.py" +precedence = "override" +SPDX-FileCopyrightText = "2019-2021 eyJhb " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/emoji_aliases.py" +precedence = "override" +SPDX-FileCopyrightText = "2016 Mike Reinhardt " +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "python/emojis.py" +precedence = "override" +SPDX-FileCopyrightText = "2017 jmui " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/emojize.py" +precedence = "override" +SPDX-FileCopyrightText = "2021 Thom Wiggers " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "python/execbot.py" +precedence = "override" +SPDX-FileCopyrightText = "2018 Giap Tran " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/fileaway.py" +precedence = "override" +SPDX-FileCopyrightText = "2011 Richard A Hofer (javagamer) " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/fish.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2011-2023 David Flatz ", + "2017-2020 Marcin Kurczewski ", + "2017 Ricardo Ferreira ", + "2014 Charles Franklin ", + "2012 Markus Näsman ", + "2009 Bjorn Edstrom ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/floodit.py" +precedence = "override" +SPDX-FileCopyrightText = "2011-2012 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/force_nick.py" +precedence = "override" +SPDX-FileCopyrightText = "2015-2018 Simmo Saan " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/fullwidth.py" +precedence = "override" +SPDX-FileCopyrightText = "2013-2014 Germain Z. " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/giphy.py" +precedence = "override" +SPDX-FileCopyrightText = "2017-2018 butlerx " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/glitter.py" +precedence = "override" +SPDX-FileCopyrightText = "2019 jotham " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/gnome_screensaver_away.py" +precedence = "override" +SPDX-FileCopyrightText = "2016-2021 Gerard Ryan " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/gnotify.py" +precedence = "override" +SPDX-FileCopyrightText = "2009 tobypadilla " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "python/gntpnotify.py" +precedence = "override" +SPDX-FileCopyrightText = "2012 Ryan Feng " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/go.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2009-2024 Sébastien Helleu ", + "2010 m4v ", + "2011 stfn ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/greentext.py" +precedence = "override" +SPDX-FileCopyrightText = "2023 AGVXOV " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "python/grep_filter.py" +precedence = "override" +SPDX-FileCopyrightText = "2015-2016 Simmo Saan " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/grep.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2011 Elián Hanisch " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/gribble.py" +precedence = "override" +SPDX-FileCopyrightText = "2017 Alex Fluter " +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "python/growl.py" +precedence = "override" +SPDX-FileCopyrightText = "2011 Sorin Ionescu " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/hatwidget.py" +precedence = "override" +SPDX-FileCopyrightText = "2013-2014 Germain Z. " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/highlightxmpp.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2015 Jacob Peddicord " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/himan.py" +precedence = "override" +SPDX-FileCopyrightText = "2019 pX " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/histman.py" +precedence = "override" +SPDX-FileCopyrightText = "2012-2018 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/histsearch.py" +precedence = "override" +SPDX-FileCopyrightText = "2009 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/hl2file.py" +precedence = "override" +SPDX-FileCopyrightText = "2012 nesthib " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/hl_nicks.py" +precedence = "override" +SPDX-FileCopyrightText = "2012 nesthib " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/hlpvitem.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2014 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/i3lock_away.py" +precedence = "override" +SPDX-FileCopyrightText = "2017 Bertrand Ciroux " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/identica.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2010 Nicolas Reynolds " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/imap_status.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2015 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/im_kayac_com_notify.py" +precedence = "override" +SPDX-FileCopyrightText = "2012-2013 Gosuke Miyashita " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/im_rename.py" +precedence = "override" +SPDX-FileCopyrightText = "2014 Jaakko Lintula " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "python/infolist.py" +precedence = "override" +SPDX-FileCopyrightText = "2008-2018 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/infos.py" +precedence = "override" +SPDX-FileCopyrightText = "2011 Elián Hanisch " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/irccloud_avatar_link.py" +precedence = "override" +SPDX-FileCopyrightText = "2024 Jesse McDowell " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/irssi_awaylog.py" +precedence = "override" +SPDX-FileCopyrightText = "2013 henrik " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/irssinotifier.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2013 Caspar Clemens Mierau ", + "2013 ochameau ", + "2013 lavaramano ", + "2010 Yoshiyasu SAEKI ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/jabber.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2009-2013 Sébastien Helleu ", + "2010 Tor Hveem ", + "2010 Aleksey V. Zapparov ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/jisearch.py" +precedence = "override" +SPDX-FileCopyrightText = "2016 ark " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/kbtimeout.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2018 kinabalu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/keepnick.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2012-2017 Nils Görs ", + "2006 EgS ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/kiloseconds.py" +precedence = "override" +SPDX-FileCopyrightText = "2009 Wraithan " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/kitty_notifications.py" +precedence = "override" +SPDX-FileCopyrightText = "2024 Emma Eilefsen Glenna " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/lastfm2.py" +precedence = "override" +SPDX-FileCopyrightText = "2015 timss " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/lastfm.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 Adam Saponara " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/last_written.py" +precedence = "override" +SPDX-FileCopyrightText = "2019 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/listbuffer.py" +precedence = "override" +SPDX-FileCopyrightText = "2011-2013 Filip H.F. 'FiXato' Slagter " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/lnotify.py" +precedence = "override" +SPDX-FileCopyrightText = "2010-2019 kevr " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/log.py" +precedence = "override" +SPDX-FileCopyrightText = "2019 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/logsize.py" +precedence = "override" +SPDX-FileCopyrightText = "2012-2019 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/lossage.py" +precedence = "override" +SPDX-FileCopyrightText = "2021 Germain Z. " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/maskmatch.py" +precedence = "override" +SPDX-FileCopyrightText = "2015-2016 Jos Ahrens " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/maze.py" +precedence = "override" +SPDX-FileCopyrightText = "2012-2022 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/memon.py" +precedence = "override" +SPDX-FileCopyrightText = "2012 Barbu Paul - Gheorghe " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/minesweeper.py" +precedence = "override" +SPDX-FileCopyrightText = "2011-2012 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/mnotify.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2013 maker ", + "2012 magnific0 ", + "2011 Sorin Ionescu ", +] +SPDX-License-Identifier = "Beerware" + +[[annotations]] +path = "python/moc_control.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2019 Benjamin Neff " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "python/mop.py" +precedence = "override" +SPDX-FileCopyrightText = "2016 Adam Saponara " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/mpc.py" +precedence = "override" +SPDX-FileCopyrightText = "2011 P Hargrave " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/mpdnp.py" +precedence = "override" +SPDX-FileCopyrightText = "2006 Henning Hasemann " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "python/mpris2_np.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2011 Johannes Nixdorf ", + "2014 Mantas Mikulėnas ", +] +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "python/mpris_np.py" +precedence = "override" +SPDX-FileCopyrightText = "2011 Johannes Nixdorf " +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "python/mpv.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2016 teraflops ", + "2016 nashgul ", +] +SPDX-License-Identifier = "Beerware" + +[[annotations]] +path = "python/mqtt_notify.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2016 Guillaume Subiron ", + "2018 Serge van Ginderachter ", +] +SPDX-License-Identifier = "WTFPL" + +[[annotations]] +path = "python/msg_command.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/nameday.py" +precedence = "override" +SPDX-FileCopyrightText = "2003-2012 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/nma.py" +precedence = "override" +SPDX-FileCopyrightText = "2011-2013 sitaktif " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "python/noirccolors.py" +precedence = "override" +SPDX-FileCopyrightText = "2012 Fredrick Brennan " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "python/notification_center.py" +precedence = "override" +SPDX-FileCopyrightText = "2012-2020 Sindre Sorhus " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/notification.py" +precedence = "override" +SPDX-FileCopyrightText = "2014 Guido Berhoerster " +SPDX-License-Identifier = "GPL-3.0-only" + +[[annotations]] +path = "python/notifo_notify.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 Yoshiyasu SAEKI " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/notify.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2007 lavaramano ", + "2008 BaSh ", +] +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "python/openbsd_privdrop.py" +precedence = "override" +SPDX-FileCopyrightText = "2022-2024 Alvar Penning " +SPDX-License-Identifier = "ISC" + +[[annotations]] +path = "python/otr.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2012-2015 Matthew M. Boedicker ", + "2012-2015 Nils Görs ", + "2012-2015 Daniel 'koolfy' Faucon ", + "2012-2015 Felix Eckhofer ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/pagetitle.py" +precedence = "override" +SPDX-FileCopyrightText = "2008 xororand " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "python/postpone.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 Alexander Schremmer " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/prism.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 Alex Barrett " +SPDX-License-Identifier = "WTFPL" + +[[annotations]] +path = "python/prowl_notify.py" +precedence = "override" +SPDX-FileCopyrightText = "2013-2014 kidchunks " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/purgelogs.py" +precedence = "override" +SPDX-FileCopyrightText = "2011-2013 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/pushjet.py" +precedence = "override" +SPDX-FileCopyrightText = "2017 p3lim " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/pybuffer.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 Elián Hanisch " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/quick_force_color.py" +precedence = "override" +SPDX-FileCopyrightText = "2012-2017 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/quodnp.py" +precedence = "override" +SPDX-FileCopyrightText = "2009 Brandon Hartshorn " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "python/read_marker.py" +precedence = "override" +SPDX-FileCopyrightText = "2022-2023 Simon Ser " +SPDX-License-Identifier = "AGPL-3.0-or-later" + +[[annotations]] +path = "python/reop.py" +precedence = "override" +SPDX-FileCopyrightText = "2022 Jeroen F.J. Laros " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/responsive_layout.py" +precedence = "override" +SPDX-FileCopyrightText = "2014-2019 Stefan Wold " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/samegame.py" +precedence = "override" +SPDX-FileCopyrightText = "2012 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/sarcasm.py" +precedence = "override" +SPDX-FileCopyrightText = "2023 Fsaev " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/screen_away.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2009 Tor Hveem ", + "2009 penryu ", + "2010 Blake Winton ", + "2010 Aron Griffis ", + "2010 Jani Kesänen ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/selfcensor.py" +precedence = "override" +SPDX-FileCopyrightText = "2018 tx " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/sensors.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 0x1cedd1ce <0x1cedd1ce@freeunix.net>" +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/server_autoswitch.py" +precedence = "override" +SPDX-FileCopyrightText = "2012-2013 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/shortenurl.py" +precedence = "override" +SPDX-FileCopyrightText = "2010-2014 John Anderson " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/shutup.py" +precedence = "override" +SPDX-FileCopyrightText = "2014 Filip H.F. 'FiXato' Slagter " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/slack.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2014-2016 Ryan Huber ", + "2015-2018 Tollef Fog Heen ", + "2015-2024 Trygve Aaberge ", +] +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/slock_away.py" +precedence = "override" +SPDX-FileCopyrightText = "2011 Peter A. Shevtsov " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/smile.py" +precedence = "override" +SPDX-FileCopyrightText = "2014 Vlad Stoica " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/snotify.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 Stephan Huebner " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/soju.py" +precedence = "override" +SPDX-FileCopyrightText = "2021-2023 Simon Ser " +SPDX-License-Identifier = "AGPL-3.0-or-later" + +[[annotations]] +path = "python/sort_servers.py" +precedence = "override" +SPDX-FileCopyrightText = "2013 KokaKiwi " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/spell_correction.py" +precedence = "override" +SPDX-FileCopyrightText = "2013-2024 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/spotify_nowplaying.py" +precedence = "override" +SPDX-FileCopyrightText = "2011 agreeabledragon " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/spotify.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2010 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/sshnotify.py" +precedence = "override" +SPDX-FileCopyrightText = "2011 delwin " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/stick_buffer.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2013-2017 Nils Görs ", + "2015 Damien Bargiacchi ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/styurl.py" +precedence = "override" +SPDX-FileCopyrightText = "2019 Cole Helbling " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/tailer.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/teknik.py" +precedence = "override" +SPDX-FileCopyrightText = "2017 Uncled1023 " +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "python/telnot.py" +precedence = "override" +SPDX-FileCopyrightText = "2018 Frantisek Kolacek " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/terminal_title.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 Guido Berhoerster " +SPDX-License-Identifier = "GPL-3.0-only" + +[[annotations]] +path = "python/text_replace.py" +precedence = "override" +SPDX-FileCopyrightText = "2009 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/tictactoe.py" +precedence = "override" +SPDX-FileCopyrightText = "2016 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/title.py" +precedence = "override" +SPDX-FileCopyrightText = "2009 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/tmux_env.py" +precedence = "override" +SPDX-FileCopyrightText = "2013-2014 Aron Griffis " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/toggle_highlight.py" +precedence = "override" +SPDX-FileCopyrightText = "2013 Adam Spiers " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/toggle_nicklist.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2009 Tor Hveem ", + "2009-2012 Sébastien Helleu ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/topicdiff_alt.py" +precedence = "override" +SPDX-FileCopyrightText = "2018 Juerd <#####@juerd.nl>" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "python/topicdiff.py" +precedence = "override" +SPDX-FileCopyrightText = "2011 Dafydd Harries " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/translate.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2013 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/triggerreply.py" +precedence = "override" +SPDX-FileCopyrightText = "2014-2018 Vlad Stoica " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/tts.py" +precedence = "override" +SPDX-FileCopyrightText = "2017 raspbeguy " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/twmn_notify.py" +precedence = "override" +SPDX-FileCopyrightText = "2012 epegzz " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/undernet_totp.py" +precedence = "override" +SPDX-FileCopyrightText = "2013-2019 Stefan Wold " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/unhighlight.py" +precedence = "override" +SPDX-FileCopyrightText = "2016 Andrew Rodgers-Schatz " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/unread_buffer.py" +precedence = "override" +SPDX-FileCopyrightText = "2015-2018 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/unwanted_msg.py" +precedence = "override" +SPDX-FileCopyrightText = "2012-2018 nesthib " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/update_notifier.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2010 David Rubin ", + "2017-2023 Sébastien Helleu ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/uppercase.py" +precedence = "override" +SPDX-FileCopyrightText = "2009 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/upside_down.py" +precedence = "override" +SPDX-FileCopyrightText = "2009 Tor Hveem " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/urlbar.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2009-2020 Sébastien Helleu ", + "2009 Tor Hveem ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/urlbuf.py" +precedence = "override" +SPDX-FileCopyrightText = "2011-2014 Jani Kesänen " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/urlgrab.py" +precedence = "override" +SPDX-FileCopyrightText = "2005 David Rubin " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "python/url_hint.py" +precedence = "override" +SPDX-FileCopyrightText = "2017-2019 oakkitten " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/url_olde.py" +precedence = "override" +SPDX-FileCopyrightText = "2016-2017 Charlie Allom " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/urlserver.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2011-2021 Sébastien Helleu ", + "2011 Tor Hveem ", + "2012 Filip H.F. 'FiXato' Slagter ", + "2012 WillyKaze ", + "2013 Thomas Kindler ", + "2013 Felix Eckhofer ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/vdm.py" +precedence = "override" +SPDX-FileCopyrightText = "2009-2012 Sébastien Helleu " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/vimode.py" +precedence = "override" +SPDX-FileCopyrightText = "2013-2018 Germain Z. " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/weefusables.py" +precedence = "override" +SPDX-FileCopyrightText = "2021 Nils Görs " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/weejoin.py" +precedence = "override" +SPDX-FileCopyrightText = "2020 KittyKatt " +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "python/wee_most.py" +precedence = "override" +SPDX-FileCopyrightText = "2022-2023 Damien Tardy-Panis " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/weemoticons.py" +precedence = "override" +SPDX-FileCopyrightText = "2013 Stefan Wold " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/weempd.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2007 Apprentice ", + "2007 Pablo Escobar ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/weemustfeed.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2013 Bit Shift ", + "2016-2018 Pol Van Aubel ", +] +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/weenetsoul.py" +precedence = "override" +SPDX-FileCopyrightText = "2011-2014 godric " +SPDX-License-Identifier = "WTFPL" + +[[annotations]] +path = "python/weenotifier.py" +precedence = "override" +SPDX-FileCopyrightText = "2021-2024 Mohan Raman " +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "python/weenotify.py" +precedence = "override" +SPDX-FileCopyrightText = "2018 Elia El Lazkani " +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "python/weeprowl.py" +precedence = "override" +SPDX-FileCopyrightText = "2013 Josh Dick " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "python/weepushover.py" +precedence = "override" +SPDX-FileCopyrightText = "2018 adtac " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/weestats.py" +precedence = "override" +SPDX-FileCopyrightText = "2012 Filip H.F. 'FiXato' Slagter " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/weestreamer.py" +precedence = "override" +SPDX-FileCopyrightText = "2015 Miblo " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/weetext.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2014 David R. Andersen ", + "2014 Tycho Andersen ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/weetweet.py" +precedence = "override" +SPDX-FileCopyrightText = "2013-2014 DarkDefender " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/whatismyip.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 John Anderson " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/whatsapp.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2015 Jochen Sprickerhof ", + "2009-2013 Sébastien Helleu ", + "2010 Tor Hveem ", + "2010 Aleksey V. Zapparov ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/whois_on_query.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2009-2012 Sébastien Helleu ", + "2011 Elián Hanisch ", + "2011 arza ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/whowas_timeago.py" +precedence = "override" +SPDX-FileCopyrightText = "2015-2016 Jos Ahrens " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/windicate.py" +precedence = "override" +SPDX-FileCopyrightText = [ + "2010 Leon Bogaert ", + "2010-2013 Stacey Sheldon ", +] +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "python/win_scroll_screen.py" +precedence = "override" +SPDX-FileCopyrightText = "2015 Paul L " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/xdccq.py" +precedence = "override" +SPDX-FileCopyrightText = "2018 Randall Flagg " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/xfer_run_command.py" +precedence = "override" +SPDX-FileCopyrightText = "2016 Michael Kebe " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/xfer_scp.py" +precedence = "override" +SPDX-FileCopyrightText = "2014-2017 Grant Bacon " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/xfer_setip.py" +precedence = "override" +SPDX-FileCopyrightText = "2010-2012 Stephan Huebner " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/zerotab.py" +precedence = "override" +SPDX-FileCopyrightText = "2010 Lucian Adamson " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "python/zncnotice.py" +precedence = "override" +SPDX-FileCopyrightText = "2016 Hugo Landau " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "python/zncplayback.py" +precedence = "override" +SPDX-FileCopyrightText = "2016 Jasper v. Blanckenburg " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "ruby/amqp_notify.rb" +precedence = "override" +SPDX-FileCopyrightText = "2011 Mahlon E. Smith " +SPDX-License-Identifier = "BSD-3-Clause" + +[[annotations]] +path = "ruby/auth.rb" +precedence = "override" +SPDX-FileCopyrightText = "2013-2014 Shawn Smith " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "ruby/buffzilla.rb" +precedence = "override" +SPDX-FileCopyrightText = "2016 Dave Williams " +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "ruby/challengeauth.rb" +precedence = "override" +SPDX-FileCopyrightText = "2013 Dominik Honnef " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "ruby/cleanbuffer.rb" +precedence = "override" +SPDX-FileCopyrightText = "2016 Ewa Baumgarten " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "ruby/colorizer.rb" +precedence = "override" +SPDX-FileCopyrightText = "2015 Michael B. Hix " +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "ruby/countdown.rb" +precedence = "override" +SPDX-FileCopyrightText = "2014 Rylee Fowler " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "ruby/dcc_send_relay.rb" +precedence = "override" +SPDX-FileCopyrightText = "2012 Dominik Honnef " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "ruby/gntp_notify.rb" +precedence = "override" +SPDX-FileCopyrightText = "2011-2012 Justin Anderson " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "ruby/hilites.rb" +precedence = "override" +SPDX-FileCopyrightText = "2011 Christian Brassat " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "ruby/input_lock.rb" +precedence = "override" +SPDX-FileCopyrightText = "2011-2022 Mahlon E. Smith " +SPDX-License-Identifier = "BSD-3-Clause" + +[[annotations]] +path = "ruby/itunes.rb" +precedence = "override" +SPDX-FileCopyrightText = "2012 mdszy " +SPDX-License-Identifier = "CC-BY-SA-3.0" + +[[annotations]] +path = "ruby/minbif_typing_notice.rb" +precedence = "override" +SPDX-FileCopyrightText = "2010 CissWit " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "ruby/mpdspam.rb" +precedence = "override" +SPDX-FileCopyrightText = "2009 Benedikt 'linopolus' Mueller " +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "ruby/myuptime.rb" +precedence = "override" +SPDX-FileCopyrightText = "2006 David DEMONCHY " +SPDX-License-Identifier = "GPL-2.0-or-later" + +[[annotations]] +path = "ruby/pushbullet.rb" +precedence = "override" +SPDX-FileCopyrightText = "2018 yazgoo " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "ruby/pushsafer.rb" +precedence = "override" +SPDX-FileCopyrightText = "2017 Kevin Siml " +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "ruby/samechannel.rb" +precedence = "override" +SPDX-FileCopyrightText = [ + "2013 Hendrik 'henk' Jaeger ", + "2015 arza ", +] +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "ruby/socket_notify.rb" +precedence = "override" +SPDX-FileCopyrightText = "2014 Christopher Giroir " +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "ruby/substitution.rb" +precedence = "override" +SPDX-FileCopyrightText = "2013 Samuel Laverdière " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "ruby/undernet_challenge.rb" +precedence = "override" +SPDX-FileCopyrightText = "2013 Daniel Bretoi " +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "ruby/url_hinter.rb" +precedence = "override" +SPDX-FileCopyrightText = "2014 Kengo Tateishi " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "ruby/url_shorten.rb" +precedence = "override" +SPDX-FileCopyrightText = "2008-2013 Daniel Bretoi " +SPDX-License-Identifier = "BSD-2-Clause" + +[[annotations]] +path = "ruby/weefish.rb" +precedence = "override" +SPDX-FileCopyrightText = "2010 Tobias Petersen " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "ruby/weespotify.rb" +precedence = "override" +SPDX-FileCopyrightText = "2013 Paweł Pogorzelski " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "ruby/xmms2.rb" +precedence = "override" +SPDX-FileCopyrightText = "2009, 2011 Łukasz P. Michalik " +SPDX-License-Identifier = "GPL-2.0-only" + +[[annotations]] +path = "ruby/zmq_notify.rb" +precedence = "override" +SPDX-FileCopyrightText = "2011 Mahlon E. Smith " +SPDX-License-Identifier = "BSD-3-Clause" + +[[annotations]] +path = "tcl/chan_hl.tcl" +precedence = "override" +SPDX-FileCopyrightText = "2009 Dmitry Kobylin " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "tcl/inverter.tcl" +precedence = "override" +SPDX-FileCopyrightText = "2016 CrazyCat " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "tcl/ipinfo.tcl" +precedence = "override" +SPDX-FileCopyrightText = "2022 CrazyCat " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "tcl/rnotify.tcl" +precedence = "override" +SPDX-FileCopyrightText = "2010-2015 Gotisch " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "tcl/wttr.tcl" +precedence = "override" +SPDX-FileCopyrightText = "2023 CrazyCat " +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "tcl/xosdnotify.tcl" +precedence = "override" +SPDX-FileCopyrightText = "2010-2013 Dmitry Kobylin " +SPDX-License-Identifier = "GPL-3.0-or-later" diff --git a/guile/emote.scm b/guile/emote.scm index 65f058da..c59f28d4 100644 --- a/guile/emote.scm +++ b/guile/emote.scm @@ -17,35 +17,47 @@ ; (this script requires WeeChat 0.4.1 or newer) ; ; History: -; 2016-06-03, nycatelos +; 2023-03-18, Yuval Langer +; version 0.3.1: Replace `apply string-append` with `string-join`. +; 2017-02-18, nycatelos +; version 0.3: added more emotes +; 2016-06-03, nycatelos ; version 0.2: added additional emotes ; 2014-05-03, csmith ; version 0.1: initial release (use-modules (srfi srfi-69)) -(weechat:register "emote" "Caleb Smith" "0.2" "GPL" "Emote" "" "") +(weechat:register "emote" "Caleb Smith" "0.3.1" "GPL" "Emote" "" "") ; Mappings of words with their emoticons (define patterns (alist->hash-table '( ("tableflip" . "(╯° °)╯︵ ┻━┻)") ("rageflip" . "(ノಠ益ಠ)ノ彡┻━┻") ("doubleflip" . "┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻") - ("lookofdisapproval" . "ಠ_ಠ") + ("disapproval" . "ಠ_ಠ") ("sun" . "☼") ("kitaa" . "キタ━━━(゜∀゜)━━━!!!!!") ("joy" . "◕‿◕") ("nyancat" . "~=[,,_,,]:3") - ("lennyface" . "( ͡° ͜ʖ ͡°)") + ("lenny" . "( ͡° ͜ʖ ͡°)") ("shrug" . "¯\\_(ツ)_/¯") ("denko" . "(・ω・)") ("tableplace" . "┬─┬ ノ( ゜-゜ノ)") + ("gface" . "( ≖‿≖)") + ("facepalm" . "(-‸ლ)") + ("tehe" . "☆~(◡﹏◕✿)") + ("angry" . "(╬ ಠ益ಠ)") + ("umu" . "( ̄ー ̄)") + ("toast" . "( ^_^)o自自o(^_^ )") + ("yay" . "ヽ(´ー`)ノ") + ))) ; Derive the tab completion string for the subcommands. (define tab-completions - (apply string-append + (string-join (map (lambda (i) (string-append "|| " i)) (hash-table-keys patterns)))) @@ -68,7 +80,7 @@ ; Handle the IRC command given by the user. Sets input buffer as a side-effect (define (main data buffer command) (weechat:buffer_set buffer "input" - (apply string-append (map (lambda (c) + (string-join (map (lambda (c) (string-append (hash-table-ref/default patterns c c) " ")) (string-tokenize command)))) weechat:WEECHAT_RC_OK) diff --git a/guile/gateway_rename.scm b/guile/gateway_rename.scm index 03209a4d..57c283d6 100644 --- a/guile/gateway_rename.scm +++ b/guile/gateway_rename.scm @@ -1,22 +1,57 @@ +;; -*- geiser-scheme-implementation: 'guile -*- +;; Copyright 2017 by Zephyr Pellerin +;; ------------------------------------------------------------ +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 3 of the License, or +;; (at your option) any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;; History: +;; 1.2 - Use weechat plugin configuration data to match IRC gateways +;; 0.9 - Lookup correct servername in /VERSION +;; 0.8 - Barebones, contained list of translations + (use-modules ((srfi srfi-1) - #:select (any))) + #:select (any fold))) (use-modules ((srfi srfi-26) #:select (cut))) (use-modules (ice-9 regex)) (use-modules (ice-9 hash-table)) (use-modules (ice-9 match)) -(if (defined? 'weechat:register) - (weechat:register "gateway-nickconverter" - "zv " - "1.1" - "GPL3" - "Convert usernames of gateway connections their real names" - "" - "")) +(define *weechat/script-name* "gateway_rename") +(define *weechat/script-author* "zv ") +(define *weechat/script-version* "1.2.2") +(define *weechat/script-license* "GPL3") +(define *weechat/script-description* "Convert usernames of gateway connections their real names") + + +;; A test-harness for checking if we are inside weechat +(define-syntax if-weechat + (syntax-rules () + ((_ conseq alt) (if (defined? 'weechat:register) conseq alt)) + ((_ conseq) (if (defined? 'weechat:register) conseq)))) + +(if-weechat + (weechat:register *weechat/script-name* + *weechat/script-author* + *weechat/script-version* + *weechat/script-license* + *weechat/script-description* + "" "")) ;; `user-prefix' is a distinguishing username prefix for 'fake' users (define *user-prefix* "^") +(define *gateway-config* "gateways") +(define *default-irc-gateways* "(freenode #radare r2tg ) (freenode #test-channel zv-test NICK:)") (define (print . msgs) (if (defined? 'weechat:print) @@ -25,15 +60,7 @@ ;; A regular expression must have the gateway username in the first matchgroup, ;; the "real" username in the 3rd, and the real-username along with it's enclosing ;; brackets in the 2nd -(define *gateway-regexps* - (alist->hash-table - `(("freenode" . - (;; r2tg - ,(make-regexp ":(r2tg)!\\S* PRIVMSG #radare :(<(\\S*?)>) .*") - ;; slack-irc-bot - ,(make-regexp ":(slack-irc-bot(1\\|2)?)!\\S* PRIVMSG #\\S* :(<(\\S*?)>) .*") - ;; test - ,(make-regexp ":(zv-test)!\\S* PRIVMSG #test-channel :(<(\\S*?)>) .*")))))) +(define *gateway-regexps* (make-hash-table)) (define (process-network-infolist) "Convert the internal user-defined servername to the 'true' servername @@ -60,12 +87,17 @@ returned during /version" (cons name network) (process (weechat:infolist_next il)))))) - (process (weechat:infolist_next il))) + (let ((result (process (weechat:infolist_next il)))) + (weechat:infolist_free il) + result)) -(define *hostname-table* (alist->hash-table (process-network-infolist))) +;; This is a table that maps a weechat network 'name' to it's IRC-style hostname +(define *hostname-table* (alist->hash-table '(("freenode" . "freenode")))) +(if-weechat + (set! *hostname-table* (alist->hash-table (process-network-infolist)))) (define (replace-privmsg msg gateways) - "A function to replace the privmsg sent by by a gateway " + "A function to replace the PRIVMSG sent by by a gateway " (let* ((match? (cut regexp-exec <> msg)) (result (any match? gateways))) (if result @@ -73,11 +105,12 @@ returned during /version" ;; take everything after username before message [username (nth-match 1)] [real-username (nth-match 3)] - ;; extract everything after the fake r2tg username - [message (string-copy msg - ;; skip the inserted space - (+ 1 (match:end result 2)) + ;; Extract everything after the gateway-user mask + [raw-message (string-copy msg + (match:end result 2) (string-length msg))] + ;; .. and be sure to strip any preceding characters + [message (string-trim raw-message)] ;; extract everything before the message but after the username [hostmask (string-copy msg (match:end result 1) @@ -86,17 +119,160 @@ returned during /version" msg))) (define (server->gateways server) - (hash-ref *gateway-regexps* - (hash-ref *hostname-table* server))) + (hash-ref *gateway-regexps* (hash-ref *hostname-table* server))) (define (privmsg-modifier data modifier-type server msg) - ;; fetch the appropriate gateway by server + "The hook for all PRIVMSGs in Weechat" (let ((gateways (server->gateways server))) (if gateways (replace-privmsg msg gateways) msg))) -(if (defined? 'weechat:hook_modifier) - (weechat:hook_modifier "irc_in_privmsg" "privmsg-modifier" "")) +(define* (make-gateway-regexp gateway-nick channel mask #:optional emit-string) + "Build a regular expression that will match the nick, channel and \"\"-style mask" + (let* ([mask-regexp ;; replace with <(\\S*?)> + (regexp-substitute/global #f "NICK" mask 'pre "(\\S*?)" 'post "")] + [composed-str (format #f + ":(~a)!\\S* PRIVMSG ~a :(~a)" + gateway-nick + (if (equal? "*" channel) "\\S*" channel) + mask-regexp)]) + (if emit-string composed-str (make-regexp composed-str)))) + +(define (extract-gateway-fields str) + "This is a hack around Guile's non-greedy matchers. + + # Example + scheme@(guile-user)> (extract-gateway-fields \"(freenode #radare r2tg )\") + $1 = (\"freenode\" \"#radare\" \"r2tg\" \"\")" + (let* ((range-end (λ (range) (+ 1 (cdr range)))) + (find-space (λ (end) (string-index str #\space end))) + ;; opening (first) and closing (last) parenthesis + (opening-par (string-index str #\()) + (closing-par (string-index str #\))) + ;; extract the range of each + (server (cons (+ 1 opening-par) (find-space 0))) + (channel (cons (range-end server) (find-space (range-end server)))) + (gateway-nick (cons (range-end channel) (find-space (range-end channel)))) + (mask (cons (range-end gateway-nick) closing-par))) + + ;; and then get the strings + (map (λ (window) (substring str (car window) (cdr window))) + (list server channel gateway-nick mask)))) + +(define* (process-weechat-option opt #:optional emit-string) + "Takes in the application-define weechat-options and emits a server and +matching regular expression. + +The optional parameter `emit-string' controls if a string or a compiled regular +expression is returned. + +# Example + +scheme@(guile-user)> (process-weechat-option \"(freenode #radare r2tg )\") +$1 = '(\"freenode\" . (make-regexp \":(r2tg)!\\S* PRIVMSG #radare :(<(\\S*?)>) .*\")))" + (let* ((fields (extract-gateway-fields opt)) + (server (list-ref fields 0)) + (channel (list-ref fields 1)) + (gateway-nick (list-ref fields 2)) + (mask (list-ref fields 3))) + (cons server (make-gateway-regexp gateway-nick channel mask emit-string)))) + + +(define (split-gateways config) + "Push our elts onto the stack to extract our configs + +# Example +scheme@(guile-user)> (split-gateways \"(freenode #radare r2tg )(* * slack-irc-bot NICK:)\") +$1 = (\"(freenode #radare r2tg )\" \"(* * slack-irc-bot NICK:)\") +" + (define (process stk current rest) + (if (string-null? rest) (cons current '()) + (let* ((head (string-ref rest 0)) + (nrest (string-drop rest 1)) + (ncurrent (string-append current (string head)))) + (cond + [(and (null? stk) (not (string-null? current))) + (cons current (process stk "" rest))] + [(eq? head #\() (process (cons #\( stk) ncurrent nrest)] + [(eq? head #\)) (process (cdr stk) ncurrent nrest)] + ;; skip characters if our stk is empty + [(null? stk) (process stk current nrest)] + [else (process stk ncurrent nrest)])))) + + (process '() "" config)) + +(define (fetch-weechat-gateway-config) + "Extract the gateway configuration string" + (if-weechat (weechat:config_get_plugin *gateway-config*) + *default-irc-gateways*)) + +(define (assign-gateways-regex) + "Fetch our weechat gateway configuration and assign it to our local regexps" + (let* ((config_str (fetch-weechat-gateway-config)) + (config_lst (split-gateways config_str)) + (gateways (map process-weechat-option config_lst))) + ;; for each gateway, add it to our `*gateway-regexps*' ht + (for-each + (λ (gt) + (let* ((server (car gt)) + (new-regex (cdr gt)) + (server-regexps (hash-ref *gateway-regexps* server '()))) + (hash-set! *gateway-regexps* server + (cons new-regex server-regexps)))) + gateways))) + +;; Initialize our weechat settings & privmsg hook +(define (renamer_command_cb data buffer args) weechat::WEECHAT_RC_OK) + +(if-weechat + (begin + (if (not (= 1 (weechat:config_is_set_plugin *gateway-config*))) + (weechat:config_set_plugin + *gateway-config* + *default-irc-gateways*)) + + (weechat:hook_modifier "irc_in_privmsg" "privmsg-modifier" "") + + (weechat:hook_command *weechat/script-name* + *weechat/script-description* + "" ;; arguments + " +There are many IRC gateway programs that, rather than sending as if they were +another user, simply prepend the name of the user that is using that gateway to +the messages they are sending. + +For example: `slack-irc-bot` might send a message to #weechat: + + slack-irc-bot: How about them Yankees? + +gateway_rename intercepts that message and converts it to: + + ^zv: How about them Yankees? + +(gateway_rename prefixes the `^' (caret) symbol to each message to prevent message spoofing) + +Adding a Renamer: + + Which servers, channels, users and nickname templates are renamed can all be + modified in `plugins.var.guile.gateway_rename.gateways' + + Two gateways are matched by default, but are primarily intended to serve as a + template for you to add others. + + Each gateway renamer is placed inside of a set of parenthesis and contain four fields respectively: + 1. IRC server name (use the same name that weechat uses) + 2. Channel + 3. Gateway's nick/user name + 4. The last field is a template for how to match the nickname of the 'real user' + For example, if you wanted to convert the message 'gateway-bot: zv: Yes' into 'zv: Yes' + You would set the last field to 'NICK:' because each NICK at the beginning of the message is suffixed with a `:' + " + "" + "renamer_command_cb" + ""))) + +;; Setup our gateways->regex map +(assign-gateways-regex) ;;(print "Gateway Nickconverter by zv ") diff --git a/guile/weechataboo.scm b/guile/weechataboo.scm new file mode 100644 index 00000000..f5f22c56 --- /dev/null +++ b/guile/weechataboo.scm @@ -0,0 +1,132 @@ +; WeeChat-Script to replace emotion-tags with random emoticons. +; Copyright (C) 2017-2018 Alvar +; +; This program is free software: you can redistribute it and/or modify +; it under the terms of the GNU General Public License as published by +; the Free Software Foundation, either version 3 of the License, or +; (at your option) any later version. +; +; This program is distributed in the hope that it will be useful, +; but WITHOUT ANY WARRANTY; without even the implied warranty of +; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +; GNU General Public License for more details. +; +; You should have received a copy of the GNU General Public License +; along with this program. If not, see . +; +; For usage see `/help weechataboo` + +(use-modules (srfi srfi-1)) + +; -> List +; Returns a list of available emotions. +(define (emotion-categories) + (string-split (weechat:config_get_plugin "emotions") #\,)) + +; String -> String +; Returns an emoticon for a known emotion. +(define (emotion->emoticon emo) + (let + ((emotions (string-split (weechat:config_get_plugin emo) #\,)) + (random-emotion (lambda (l) + (list-ref l (random (length l)))))) + (random-emotion emotions))) + +; String -> String +; Replaces in the given string every ~~EMOTION with a fitting emoticon. +(define (emoticonize-line line) + (let* + ((as-tag (lambda (emo) (string-append "~~" emo))) + (has-emotions? (lambda (txt) + (any (lambda (emo) + (number? (string-contains txt (as-tag emo)))) + (emotion-categories)))) + (replace (lambda (emo txt) + (let ((pos (string-contains txt (as-tag emo)))) + (if (number? pos) + (string-replace + txt (emotion->emoticon emo) + pos (+ (string-length (as-tag emo)) pos)) + txt)))) + (new-line (fold replace line (emotion-categories)))) + (if (has-emotions? new-line) + (emoticonize-line new-line) + new-line))) + +; Pointer ? ? String -> String +; This function was registered to be called when an input was submitted and +; will try to replace ~~EMOTIONs to emoticons. +(define (weechataboo-hook data modifier modifier-data msg) + (emoticonize-line msg)) + +; Pointer String List -> Weechat-Return +; Function which tells you to RTFM. +(define (weechataboo-func data buffer args) + (weechat:print "" "See /help weechataboo") + weechat:WEECHAT_RC_OK) + +; -> () +; Function to be executed when there is no config yet. Creates a dummy one. +(define (initial-setup) + (let* + ; Some defaults which may be useful‥ + ((emotions + '(("aggressive" "o(-`д´- 。),凸ಠ益ಠ)凸,' ̿'̵͇̿̿з=(◕_◕)=ε/̵͇̿̿/'̿'̿ ̿,O=('-'Q),。゜(`Д´)゜。,┌∩┐(ಠ_ಠ)┌∩┐") + ("angry" "눈_눈,(¬_¬),(`ε´),(¬▂¬),(▽д▽),ಠ_ರೃ,(⋋▂⋌),(‡▼益▼),(*`へ´*)") + ("blush" "(´ω`*),(‘-’*),(/ε\*),(*゚∀゚*),(*´ェ`*)") + ("cat" "≋≋≋≋≋̯̫⌧̯̫(ˆ•̮ ̮•ˆ)") + ("cry" "(;へ:),(πーπ),(iДi),(;Д;),(╥_╥),ಥ╭╮ಥ") + ("dance" "ヾ(^^ゞ),(ノ^o^)ノ,⌎⌈╹우╹⌉⌍,└|゚ε゚|┐,┌|゚з゚|┘,(〜 ̄△ ̄)〜") + ("drink" "(^-^)_日,(*^◇^)_旦,(  ゜Д゜)⊃旦,~~旦_(-ω-`。)") + ("excited" "(≧∇≦*),ヽ(^Д^)ノ,(* >ω<)") + ("gross" "(咒),( ≖ิ‿≖ิ ),ʅ(◔౪◔ ) ʃ") + ("happy" "≖‿≖,(^ω^),(^ω^),ヽ(ヅ)ノ,(¬‿¬),(◡‿◡✿),(❀◦‿◦),(⁎⚈᷀᷁ᴗ⚈᷀᷁⁎)") + ("heart" "♡^▽^♡,✿♥‿♥✿,(。♥‿♥。),ヽ(o♡o)/,(◍•ᴗ•◍)❤,(˘︶˘).。.:*♡,❣◕ ‿ ◕❣") + ("hug" "⊂(・﹏・⊂),(っ´▽`)っ,(づ ̄ ³ ̄)づ,⊂(´・ω・`⊂)") + ("kiss" "|°з°|,(*^3^),(´ε`*),(っ˘з(˘⌣˘ ),(*^3^)/~♡") + ("lenny" "( ͡ ͜ʖ ͡ ),( ͡~ ͜ʖ ͡°),( ͡~ ͜ʖ ͡~),ヽ( ͝° ͜ʖ͡°)ノ,(つ ͡° ͜ʖ ͡°)つ,( ͝סּ ͜ʖ͡סּ),") + ("magic" "(っ・ω・)っ≡≡≡≡≡≡☆,ヽ༼ຈل͜ຈ༽⊃─☆*:・゚") + ("sheep" "@^ェ^@,@・ェ・@") + ("shock" "(゚д゚;)") + ("shrug" "┐(´д`)┌,╮(╯∀╰)╭,┐(´∀`)┌,ʅ(́◡◝)ʃ,ヽ(~~~ )ノ,ヽ(。_°)ノ,¯\(◉◡◔)/¯,◔_◔") + ("shy" "(/ω\),(‘-’*),(´~`ヾ),(〃´∀`)") + ("smug" "( ̄ω ̄),( ̄ー ̄),( ̄ー ̄),(^~^)") + ("sword" "╰(◕ヮ◕)つ¤=[]———,╰(⇀︿⇀)つ-]═───,∩(˵☯‿☯˵)つ¤=[]:::::>") + ("wink" "ヾ(^∇^),ヾ(☆▽☆),(。-ω-)ノ,( ・ω・)ノ"))) + (names (string-join (map car emotions) ","))) + (and + (weechat:config_set_plugin "emotions" names) + (for-each + (lambda (emo) + (weechat:config_set_plugin (car emo) (cadr emo))) + emotions)))) + +; -> Weechat-Return +; Function to be called when the plugin is unloaded. Will hopefully clean +; up all settings. +(define (clean-up) + (for-each weechat:config_unset_plugin (emotion-categories)) + (weechat:config_unset_plugin "emotions") + weechat:WEECHAT_RC_OK) + + +(weechat:register + "weechataboo" "Alvar" "0.1.2" "GPL3" + "Replace emotion-tags with random emoticons" "clean-up" "") + +(and (eq? (weechat:config_is_set_plugin "emotions") 0) + (initial-setup)) + +(weechat:hook_modifier "irc_out1_privmsg" "weechataboo-hook" "") + +(weechat:hook_command + "weechataboo" + (string-append "This script automatically replaces written emotion-keywords\n" + "with a random emoticon from a list of matching ones. The\n" + "keyword must have two tildes (~~) as a prefix.\n" + "Example: ~~wink\n\n" + "All values are comma separated. Please make sure that every\n" + "emotion in the `emotions`-list has its own entry!\n\n" + "→ Keywords: /set plugins.var.guile.weechataboo.emotions\n" + "→ Emoticons: /set plugins.var.guile.weechataboo.$EMOTION\n") + "" "" "" "weechataboo-func" "") diff --git a/javascript/opall.js b/javascript/opall.js new file mode 100644 index 00000000..ce220457 --- /dev/null +++ b/javascript/opall.js @@ -0,0 +1,45 @@ +name = "opall"; +author = "gagz@riseup.net"; +version = "0.2"; +license = "wtfpl"; +description = "op people using chanserv instead of /mode"; +shutdown_function = ""; +charset = ""; + +weechat.register(name, author, version, license, description, shutdown_function, charset); + +weechat.hook_command("opall", "OP everybody on the channel, using chanserv instead of /mode", "", "", "", "chanserv_op_all", ""); + +function chanserv_op_all() { + var buffer = weechat.current_buffer() + var chan = weechat.buffer_get_string(buffer, "localvar_channel") + + // we must be sure to be on an IRC buffer + if( weechat.buffer_get_string(buffer, "plugin") != "irc" ) { + weechat.print("", "Works only on IRC channels") + return weechat.WEECHAT_RC_ERROR + } + + // lets get the nicklist of the current buffer + var nicklist = weechat.infolist_get("nicklist", buffer, ""); + // and walk through it + while( weechat.infolist_next(nicklist) ) { + var type = weechat.infolist_string(nicklist, "type"); + var visible = weechat.infolist_integer(nicklist, "visible"); + var prefix = weechat.infolist_string(nicklist, "prefix"); + + // we are only interested in actual non-op visible nicks + // TODO: find a more reliable way to op non-op users (ie. prefix + // can be changed in the settings and might not be "@") + // TODO: check the IRC server/services version to talk with + // chanserv correctly. This works with charybdis/atheme. + if( type == "nick" && visible == 1 && prefix != "@") { + var nick = weechat.infolist_string(nicklist, "name"); + var command = "/msg chanserv op " + chan + " " + nick; + weechat.print("", command); + weechat.command(buffer, command); + } + } + weechat.infolist_free(nicklist); + return weechat.WEECHAT_RC_OK; +} diff --git a/lua/cmus_announce.lua b/lua/cmus_announce.lua new file mode 100644 index 00000000..9d53b249 --- /dev/null +++ b/lua/cmus_announce.lua @@ -0,0 +1,167 @@ + +-- cmus_announce.lua - Announce currently playing file in cmus to channel + +--[[ +BSD Zero Clause License + +iCopyright © 2024-2024 by + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +--]] + +--[[ +NOTE! + cmus-remote could hang if you don't have mpris installed giving you a warning at the beginning + +Design + A defaultFormat is passed to cmus format_print, output is absorbed and s/\n/ / + be aware that spaces in cmus become newlines, so s/\n/ / is used + string.sub is run to limit the output + +TODO + Probably add/support colors %{red} and so on + defaultFormat should be more colorful, with some emojis +--]] + +local defaultFormat = "%{artist} - %{album} - %{title}" +local msgLimit = 250 + +local + io, string, os + = + io, string, os + +local Gsub = string.gsub + +local ShellArgS = function -- string +( + cmdS -- string +) + return "'" .. Gsub(cmdS, "'","'\\''") .. "'" +end + +local ExistsB, Exists do + local Rename = os.rename + ExistsB = function(fileS) + -- returns true/false if fileS exists + local B, _, code = Rename(fileS,fileS) + return B or code==13 + end + + -- returns fileS if fileS exists or falsy + Exists = function(fileS) + return ExistsB(fileS) and fileS + end +end + +local Getenv = os.getenv + +local XDG_RUNTIME_DIR = + Getenv"XDG_RUNTIME_DIR" + or "/run/nmz" + +local cmusSocketFile = Getenv"CMUS_SOCKET" + + +local IsAlive = function () + if cmusSocketFile then + return ExistsB(cmusSocketFile) + end + cmusSocketFile = Exists(XDG_RUNTIME_DIR .. "/cmus-socket") + return cmusSocketFile +end + +-- Get Current Song +local Current = function -- string +( + format -- string|falsy +) + format = format or defaultFormat + + local arg = ShellArgS("format_print " .. format) + local S + do + local H = io.popen("cmus-remote -C " .. arg) + S = H:read"a" + H:close() + end + return S or "" +end + +-- displays filename if nothing useful returns +local Display = function -- string +( + format -- string|nil +) + local msg = Current(format) + -- if Current is empty, then just print {path} + if not msg:match"[%ul]+" then + msg = Current"%{filename}" + end + msg = + Gsub(msg or "", "%s+"," ") + :gsub("^%s+","") + :gsub("%s+$","") + -- spaces turn into newlines, this reverses that + return msg +end + +if weechat then --------------------------- Weechat Section + local w = weechat + + do local name, author, version, license, description, shutdown_function, charset + name = "cmus_announce" + author = "nmz" + version = "1" + license = "0BSD" + description = "Messages current buffer/channel the currently listened to song" + charset = "" + shutdown_function = "" + w.register(name, author, version, license, description, shutdown_function, charset) + end + + -- semi global variables + local OK,ERR = w.WEECHAT_RC_OK, w.WEECHAT_RC_ERROR + + local Sub = string.sub + + -- data, buffer, args(string) + function cmus_announce(d,b,a) + if IsAlive() then + local msg = Sub(Display(a:match"%%{%g+}" and a),1,msgLimit) -- BUG: if its utf8 this might cut the last byte + w.command(b, "/me Is Listening To: " .. msg) + else + w.print("","cmus_announce.lua: cmus is not running!") + -- return ERR + end + return OK + end + + -- should be a /me + do + local command = "cmus_announce" + local description = 'Announces currently playing song in current buffer' + local args = 'format' + local args_description = + "passed to cmus-remote command, so look at cmus(1) for the relevant formatting" + local completion = "" + local callback = command + local callback_data = "" + w.hook_command(command, description, args, args_description, completion, callback, callback_data) + end + return OK +end -- weechat + +-- This is also a program which you can run as well. +-- run it to test it. +if ...==nil then + if IsAlive() then + print(os.date()) + print(Display()) + os.exit(0) + end + + os.exit(1) +end \ No newline at end of file diff --git a/lua/emoji.lua b/lua/emoji.lua index 75d4b443..14ac9ca0 100644 --- a/lua/emoji.lua +++ b/lua/emoji.lua @@ -25,6 +25,10 @@ Usage: Changelog: + version 5, 2020-05-09, FlashCode: + * remove obsolete comment on weechat_print modifier data + version 4, 2017-10-12, massa1240 + * add support of :+1: and :-1: version 3, 2016-11-04, xt * add a slack specific shortcode version 2, 2016-10-31, xt @@ -65,7 +69,7 @@ print "}" local SCRIPT_NAME = "emoji" local SCRIPT_AUTHOR = "xt " -local SCRIPT_VERSION = "3" +local SCRIPT_VERSION = "5" local SCRIPT_LICENSE = "GPL3" local SCRIPT_DESC = "Emoji output helper" @@ -76,10 +80,12 @@ local emoji = { four="4⃣",kiss_ww="👩❤💋👩",maple_leaf="🍁",waxing_gibbous_moon="🌔",bike="🚲",recycle="♻",family_mwgb="👨👩👧👦",flag_dk="🇩🇰",thought_balloon="💭",oncoming_automobile="🚘",guardsman_tone5="💂🏿",tickets="🎟",school="🏫",house_abandoned="🏚",blue_book="📘",video_game="🎮",triumph="😤",suspension_railway="🚟",umbrella="☔",levitate="🕴",cactus="🌵",monorail="🚝",stars="🌠",new="🆕",herb="🌿",pouting_cat="😾",blue_heart="💙",["100"]="💯",leaves="🍃",family_mwbb="👨👩👦👦",information_desk_person_tone2="💁🏼",dragon_face="🐲",track_next="⏭",cloud_snow="🌨",flag_jp="🇯🇵",children_crossing="🚸",information_desk_person_tone1="💁🏻",arrow_up_down="↕",mount_fuji="🗻",massage_tone1="💆🏻",flag_mq="🇲🇶",massage_tone3="💆🏽",massage_tone2="💆🏼",massage_tone5="💆🏿",flag_je="🇯🇪",flag_jm="🇯🇲",flag_jo="🇯🇴",red_car="🚗",hospital="🏥",red_circle="🔴",princess="👸",tm="™",curly_loop="➰",boy_tone5="👦🏿",pouch="👝",boy_tone3="👦🏽",boy_tone1="👦🏻",izakaya_lantern="🏮",fist_tone5="✊🏿",fist_tone4="✊🏾",fist_tone1="✊🏻",fist_tone3="✊🏽",fist_tone2="✊🏼",arrow_lower_left="↙",game_die="🎲",pushpin="📌",dividers="🗂",dolphin="🐬",night_with_stars="🌃",cruise_ship="🛳",white_medium_small_square="◽",kissing_closed_eyes="😚",earth_americas="🌎",["end"]="🔚",mouse="🐭",rewind="⏪",beach="🏖",pizza="🍕",briefcase="💼",customs="🛃",heartpulse="💗",sparkler="🎇",sparkles="✨",hand_splayed_tone1="🖐🏻",snowman2="☃",tulip="🌷",speaking_head="🗣",ambulance="🚑",office="🏢",clapper="🎬",keyboard="⌨",japan="🗾",post_office="🏣",dizzy_face="😵",imp="👿",flag_ve="🇻🇪",coffee="☕",flag_vg="🇻🇬",flag_va="🇻🇦",flag_vc="🇻🇨",flag_vn="🇻🇳",flag_vi="🇻🇮",open_mouth="😮",flag_vu="🇻🇺",page_with_curl="📃",bank="🏦",bread="🍞",oncoming_police_car="🚔",capricorn="♑",point_left="👈",tokyo_tower="🗼",fishing_pole_and_fish="🎣",thumbsdown="👎",telescope="🔭",spider="🕷",u7121="🈚",camera_with_flash="📸",lifter="🏋",sweet_potato="🍠",lock_with_ink_pen="🔏",ok_woman_tone2="🙆🏼",ok_woman_tone3="🙆🏽",smirk="😏",baggage_claim="🛄",cherry_blossom="🌸",sparkle="❇",zap="⚡",construction_site="🏗",dancers="👯",flower_playing_cards="🎴",hatching_chick="🐣",free="🆓",bullettrain_side="🚄",poultry_leg="🍗",grapes="🍇",smirk_cat="😼",lollipop="🍭",water_buffalo="🐃",black_medium_small_square="◾",atm="🏧",gift_heart="💝",older_woman_tone5="👵🏿",older_woman_tone4="👵🏾",older_woman_tone1="👵🏻",older_woman_tone3="👵🏽",older_woman_tone2="👵🏼",scissors="✂",woman_tone2="👩🏼",basketball="🏀",hammer_pick="⚒",top="🔝",clock630="🕡",raising_hand_tone5="🙋🏿",railway_track="🛤",nail_care="💅",crossed_flags="🎌",minibus="🚐",white_sun_cloud="🌥",shower="🚿",smile_cat="😸",dog2="🐕",loud_sound="🔊",kaaba="🕋",runner="🏃",ram="🐏",writing_hand="✍",rat="🐀",rice_scene="🎑",milky_way="🌌",vulcan_tone5="🖖🏿",necktie="👔",kissing_cat="😽",snowflake="❄",paintbrush="🖌",crystal_ball="🔮",mountain_bicyclist_tone4="🚵🏾",mountain_bicyclist_tone3="🚵🏽",mountain_bicyclist_tone2="🚵🏼",mountain_bicyclist_tone1="🚵🏻",koko="🈁",flag_it="🇮🇹",flag_iq="🇮🇶",flag_is="🇮🇸",flag_ir="🇮🇷",flag_im="🇮🇲",flag_il="🇮🇱",flag_io="🇮🇴",flag_in="🇮🇳",flag_ie="🇮🇪",flag_id="🇮🇩",flag_ic="🇮🇨",ballot_box_with_check="☑",mountain_bicyclist_tone5="🚵🏿",metal="🤘",dog="🐶",pineapple="🍍",no_good_tone3="🙅🏽",no_good_tone2="🙅🏼",no_good_tone1="🙅🏻",scream="😱",no_good_tone5="🙅🏿",no_good_tone4="🙅🏾",flag_ua="🇺🇦",bomb="💣",flag_ug="🇺🇬",flag_um="🇺🇲",flag_us="🇺🇸",construction_worker_tone1="👷🏻",radio="📻",flag_uy="🇺🇾",flag_uz="🇺🇿",person_with_blond_hair_tone1="👱🏻",cupid="💘",mens="🚹",rice="🍚",point_right_tone1="👉🏻",point_right_tone3="👉🏽",point_right_tone2="👉🏼",sunglasses="😎",point_right_tone4="👉🏾",watch="⌚",frowning="😦",watermelon="🍉",wedding="💒",person_frowning_tone4="🙍🏾",person_frowning_tone5="🙍🏿",person_frowning_tone2="🙍🏼",person_frowning_tone3="🙍🏽",person_frowning_tone1="🙍🏻",flag_gw="🇬🇼",flag_gu="🇬🇺",flag_gt="🇬🇹",flag_gs="🇬🇸",flag_gr="🇬🇷",flag_gq="🇬🇶",flag_gp="🇬🇵",flag_gy="🇬🇾",flag_gg="🇬🇬",flag_gf="🇬🇫",microscope="🔬",flag_gd="🇬🇩",flag_gb="🇬🇧",flag_ga="🇬🇦",flag_gn="🇬🇳",flag_gm="🇬🇲",flag_gl="🇬🇱",japanese_ogre="👹",flag_gi="🇬🇮",flag_gh="🇬🇭",man_with_turban="👳",star_and_crescent="☪",writing_hand_tone3="✍🏽",dromedary_camel="🐪",hash="#⃣",hammer="🔨",hourglass="⌛",postbox="📮",writing_hand_tone5="✍🏿",writing_hand_tone4="✍🏾",wc="🚾",aquarius="♒",couple_with_heart="💑",ok_woman="🙆",raised_hands_tone4="🙌🏾",cop="👮",raised_hands_tone1="🙌🏻",cow="🐮",raised_hands_tone3="🙌🏽",white_large_square="⬜",pig_nose="🐽",ice_skate="⛸",hotsprings="♨",tone5="🏿",three="3⃣",beer="🍺",stadium="🏟",airplane_departure="🛫",heavy_division_sign="➗",flag_black="🏴",mushroom="🍄",record_button="⏺",vulcan="🖖",dash="💨",wind_chime="🎐",anchor="⚓",seven="7⃣",flag_hr="🇭🇷",roller_coaster="🎢",pen_ballpoint="🖊",sushi="🍣",flag_ht="🇭🇹",flag_hu="🇭🇺",flag_hk="🇭🇰",dizzy="💫",flag_hn="🇭🇳",flag_hm="🇭🇲",arrow_forward="▶",violin="🎻",orthodox_cross="☦",id="🆔",heart_decoration="💟",first_quarter_moon="🌓",satellite="📡",tone3="🏽",christmas_tree="🎄",unicorn="🦄",broken_heart="💔",ocean="🌊",hearts="♥",snowman="⛄",person_with_blond_hair_tone4="👱🏾",person_with_blond_hair_tone5="👱🏿",person_with_blond_hair_tone2="👱🏼",person_with_blond_hair_tone3="👱🏽",yen="💴",straight_ruler="📏",sleepy="😪",green_apple="🍏",white_medium_square="◻",flag_fr="🇫🇷",grey_exclamation="❕",innocent="😇",flag_fm="🇫🇲",flag_fo="🇫🇴",flag_fi="🇫🇮",flag_fj="🇫🇯",flag_fk="🇫🇰",menorah="🕎",yin_yang="☯",clock130="🕜",gift="🎁",prayer_beads="📿",stuck_out_tongue="😛",om_symbol="🕉",city_dusk="🌆",massage_tone4="💆🏾",couple_ww="👩❤👩",crown="👑",sparkling_heart="💖",clubs="♣",person_with_pouting_face="🙎",newspaper2="🗞",fog="🌫",dango="🍡",large_orange_diamond="🔶",flag_tn="🇹🇳",flag_to="🇹🇴",point_up="☝",flag_tm="🇹🇲",flag_tj="🇹🇯",flag_tk="🇹🇰",flag_th="🇹🇭",flag_tf="🇹🇫",flag_tg="🇹🇬",corn="🌽",flag_tc="🇹🇨",flag_ta="🇹🇦",flag_tz="🇹🇿",flag_tv="🇹🇻",flag_tw="🇹🇼",flag_tt="🇹🇹",flag_tr="🇹🇷",eight_spoked_asterisk="✳",trophy="🏆",black_small_square="▪",o="⭕",no_bell="🔕",curry="🍛",alembic="⚗",sob="😭",waxing_crescent_moon="🌒",tiger2="🐅",two="2⃣",sos="🆘",compression="🗜",heavy_multiplication_x="✖",tennis="🎾",fireworks="🎆",skull_crossbones="☠",astonished="😲",congratulations="㊗",grey_question="❔",arrow_upper_left="↖",arrow_double_up="⏫",triangular_flag_on_post="🚩",gemini="♊",door="🚪",ship="🚢",point_down_tone3="👇🏽",point_down_tone4="👇🏾",point_down_tone5="👇🏿",movie_camera="🎥",ng="🆖",couple_mm="👨❤👨",football="🏈",asterisk="*⃣",taurus="♉",articulated_lorry="🚛",police_car="🚓",flushed="😳",spades="♠",cloud_lightning="🌩",wine_glass="🍷",clock830="🕣",punch_tone2="👊🏼",punch_tone3="👊🏽",punch_tone1="👊🏻",department_store="🏬",punch_tone4="👊🏾",punch_tone5="👊🏿",crocodile="🐊",white_square_button="🔳",hole="🕳",boy_tone2="👦🏼",mountain_cableway="🚠",melon="🍈",persevere="😣",trident="🔱",head_bandage="🤕",u7a7a="🈳",cool="🆒",high_brightness="🔆",deciduous_tree="🌳",white_flower="💮",gun="🔫",flag_sk="🇸🇰",flag_sj="🇸🇯",flag_si="🇸🇮",flag_sh="🇸🇭",flag_so="🇸🇴",flag_sn="🇸🇳",flag_sm="🇸🇲",flag_sl="🇸🇱",flag_sc="🇸🇨",flag_sb="🇸🇧",flag_sa="🇸🇦",flag_sg="🇸🇬",flag_tl="🇹🇱",flag_se="🇸🇪",arrow_left="⬅",flag_sz="🇸🇿",flag_sy="🇸🇾",small_orange_diamond="🔸",flag_ss="🇸🇸",flag_sr="🇸🇷",flag_sv="🇸🇻",flag_st="🇸🇹",file_folder="📁",flag_td="🇹🇩",["1234"]="🔢",smiling_imp="😈",surfer_tone2="🏄🏼",surfer_tone3="🏄🏽",surfer_tone4="🏄🏾",surfer_tone5="🏄🏿",amphora="🏺",baseball="⚾",boy="👦",flag_es="🇪🇸",raised_hands="🙌",flag_eu="🇪🇺",flag_et="🇪🇹",heavy_plus_sign="➕",bow="🙇",flag_ea="🇪🇦",flag_ec="🇪🇨",flag_ee="🇪🇪",light_rail="🚈",flag_eg="🇪🇬",flag_eh="🇪🇭",massage="💆",man_with_gua_pi_mao_tone4="👲🏾",man_with_gua_pi_mao_tone3="👲🏽",outbox_tray="📤",clock330="🕞",projector="📽",sake="🍶",confounded="😖",angry="😠",iphone="📱",sweat_smile="😅",aries="♈",ear_of_rice="🌾",mouse2="🐁",bicyclist_tone4="🚴🏾",bicyclist_tone5="🚴🏿",guardsman="💂",bicyclist_tone1="🚴🏻",bicyclist_tone2="🚴🏼",bicyclist_tone3="🚴🏽",envelope="✉",money_with_wings="💸",beers="🍻",heart_exclamation="❣",notepad_spiral="🗒",cat="🐱",running_shirt_with_sash="🎽",ferry="⛴",spy="🕵",chart_with_upwards_trend="📈",green_heart="💚",confused="😕",angel_tone4="👼🏾",scorpius="♏",sailboat="⛵",elephant="🐘",map="🗺",disappointed_relieved="😥",flag_xk="🇽🇰",motorway="🛣",sun_with_face="🌞",birthday="🎂",mag="🔍",date="📅",dove="🕊",man="👨",octopus="🐙",wheelchair="♿",truck="🚚",sa="🈂",shield="🛡",haircut="💇",last_quarter_moon_with_face="🌜",rosette="🏵",currency_exchange="💱",mailbox_with_no_mail="📭",bath="🛀",clock930="🕤",bowling="🎳",turtle="🐢",pause_button="⏸",construction_worker="👷",unlock="🔓",anger_right="🗯",beetle="🐞",girl="👧",sunrise="🌅",exclamation="❗",flag_dz="🇩🇿",family_mmgg="👨👨👧👧",factory="🏭",flag_do="🇩🇴",flag_dm="🇩🇲",flag_dj="🇩🇯",mouse_three_button="🖱",flag_dg="🇩🇬",flag_de="🇩🇪",star_of_david="✡",reminder_ribbon="🎗",grimacing="😬",thumbsup_tone3="👍🏽",thumbsup_tone2="👍🏼",thumbsup_tone1="👍🏻",musical_note="🎵",thumbsup_tone5="👍🏿",thumbsup_tone4="👍🏾",high_heel="👠",green_book="📗",headphones="🎧",flag_aw="🇦🇼",stop_button="⏹",yum="😋",flag_aq="🇦🇶",warning="⚠",cheese="🧀",ophiuchus="⛎",revolving_hearts="💞",one="1⃣",ring="💍",point_right="👉",sheep="🐑",bookmark="🔖",spider_web="🕸",eyes="👀",flag_ro="🇷🇴",flag_re="🇷🇪",flag_rs="🇷🇸",sweat_drops="💦",flag_ru="🇷🇺",flag_rw="🇷🇼",middle_finger="🖕",race_car="🏎",evergreen_tree="🌲",biohazard="☣",girl_tone3="👧🏽",scream_cat="🙀",computer="💻",hourglass_flowing_sand="⏳",flag_lb="🇱🇧",tophat="🎩",clock1230="🕧",tractor="🚜",u6709="🈶",u6708="🈷",crying_cat_face="😿",angel="👼",ant="🐜",information_desk_person="💁",anger="💢",mailbox_with_mail="📬",pencil2="✏",wink="😉",thermometer="🌡",relaxed="☺",printer="🖨",credit_card="💳",checkered_flag="🏁",family_mmg="👨👨👧",pager="📟",family_mmb="👨👨👦",radioactive="☢",fried_shrimp="🍤",link="🔗",walking="🚶",city_sunset="🌇",shopping_bags="🛍",hockey="🏒",arrow_up="⬆",gem="💎",negative_squared_cross_mark="❎",worried="😟",walking_tone5="🚶🏿",walking_tone1="🚶🏻",hear_no_evil="🙉",convenience_store="🏪",seat="💺",girl_tone1="👧🏻",cloud_rain="🌧",girl_tone2="👧🏼",girl_tone5="👧🏿",girl_tone4="👧🏾",parking="🅿",pisces="♓",calendar="📆",loudspeaker="📢",camping="🏕",bicyclist="🚴",label="🏷",diamonds="♦",older_man_tone1="👴🏻",older_man_tone3="👴🏽",older_man_tone2="👴🏼",older_man_tone5="👴🏿",older_man_tone4="👴🏾",microphone2="🎙",raising_hand="🙋",hot_pepper="🌶",guitar="🎸",tropical_drink="🍹",upside_down="🙃",restroom="🚻",pen_fountain="🖋",comet="☄",cancer="♋",jeans="👖",flag_qa="🇶🇦",boar="🐗",turkey="🦃",person_with_blond_hair="👱",oden="🍢",stuck_out_tongue_closed_eyes="😝",helicopter="🚁",control_knobs="🎛",performing_arts="🎭",tiger="🐯",foggy="🌁",sound="🔉",flag_cz="🇨🇿",flag_cy="🇨🇾",flag_cx="🇨🇽",speech_balloon="💬",seedling="🌱",flag_cr="🇨🇷",envelope_with_arrow="📩",flag_cp="🇨🇵",flag_cw="🇨🇼",flag_cv="🇨🇻",flag_cu="🇨🇺",flag_ck="🇨🇰",flag_ci="🇨🇮",flag_ch="🇨🇭",flag_co="🇨🇴",flag_cn="🇨🇳",flag_cm="🇨🇲",u5408="🈴",flag_cc="🇨🇨",flag_ca="🇨🇦",flag_cg="🇨🇬",flag_cf="🇨🇫",flag_cd="🇨🇩",purse="👛",telephone="☎",sleeping="😴",point_down_tone1="👇🏻",frowning2="☹",point_down_tone2="👇🏼",muscle_tone4="💪🏾",muscle_tone5="💪🏿",synagogue="🕍",muscle_tone1="💪🏻",muscle_tone2="💪🏼",muscle_tone3="💪🏽",clap_tone5="👏🏿",clap_tone4="👏🏾",clap_tone1="👏🏻",train2="🚆",clap_tone2="👏🏼",oil="🛢",diamond_shape_with_a_dot_inside="💠",barber="💈",metal_tone3="🤘🏽",ice_cream="🍨",rowboat_tone4="🚣🏾",burrito="🌯",metal_tone1="🤘🏻",joystick="🕹",rowboat_tone1="🚣🏻",taxi="🚕",u7533="🈸",racehorse="🐎",snowboarder="🏂",thinking="🤔",wave_tone1="👋🏻",wave_tone2="👋🏼",wave_tone3="👋🏽",wave_tone4="👋🏾",wave_tone5="👋🏿",desktop="🖥",stopwatch="⏱",pill="💊",skier="⛷",orange_book="📙",dart="🎯",disappointed="😞",grin="😁",place_of_worship="🛐",japanese_goblin="👺",arrows_counterclockwise="🔄",laughing="😆",clap="👏",left_right_arrow="↔",japanese_castle="🏯",nail_care_tone4="💅🏾",nail_care_tone5="💅🏿",nail_care_tone2="💅🏼",nail_care_tone3="💅🏽",nail_care_tone1="💅🏻",raised_hand_tone4="✋🏾",raised_hand_tone5="✋🏿",raised_hand_tone1="✋🏻",raised_hand_tone2="✋🏼",raised_hand_tone3="✋🏽",point_left_tone3="👈🏽",point_left_tone2="👈🏼",tanabata_tree="🎋",point_left_tone5="👈🏿",point_left_tone4="👈🏾",o2="🅾",knife="🔪",volcano="🌋",kissing_heart="😘",on="🔛",ok="🆗",package="📦",island="🏝",arrow_right="➡",chart_with_downwards_trend="📉",haircut_tone3="💇🏽",wolf="🐺",ox="🐂",dagger="🗡",full_moon_with_face="🌝",syringe="💉",flag_by="🇧🇾",flag_bz="🇧🇿",flag_bq="🇧🇶",flag_br="🇧🇷",flag_bs="🇧🇸",flag_bt="🇧🇹",flag_bv="🇧🇻",flag_bw="🇧🇼",flag_bh="🇧🇭",flag_bi="🇧🇮",flag_bj="🇧🇯",flag_bl="🇧🇱",flag_bm="🇧🇲",flag_bn="🇧🇳",flag_bo="🇧🇴",flag_ba="🇧🇦",flag_bb="🇧🇧",flag_bd="🇧🇩",flag_be="🇧🇪",flag_bf="🇧🇫",flag_bg="🇧🇬",satellite_orbital="🛰",radio_button="🔘",arrow_heading_down="⤵",rage="😡",whale2="🐋",vhs="📼",hand_splayed_tone3="🖐🏽",strawberry="🍓",["non-potable_water"]="🚱",hand_splayed_tone5="🖐🏿",star2="🌟",toilet="🚽",ab="🆎",cinema="🎦",floppy_disk="💾",princess_tone4="👸🏾",princess_tone5="👸🏿",princess_tone2="👸🏼",nerd="🤓",telephone_receiver="📞",princess_tone1="👸🏻",arrow_double_down="⏬",clock1030="🕥",flag_pr="🇵🇷",flag_ps="🇵🇸",poop="💩",flag_pw="🇵🇼",flag_pt="🇵🇹",flag_py="🇵🇾",pear="🍐",m="Ⓜ",flag_pa="🇵🇦",flag_pf="🇵🇫",flag_pg="🇵🇬",flag_pe="🇵🇪",flag_pk="🇵🇰",flag_ph="🇵🇭",flag_pn="🇵🇳",flag_pl="🇵🇱",flag_pm="🇵🇲",mask="😷",hushed="😯",sunrise_over_mountains="🌄",partly_sunny="⛅",dollar="💵",helmet_with_cross="⛑",smoking="🚬",no_bicycles="🚳",man_with_gua_pi_mao="👲",tv="📺",open_hands="👐",rotating_light="🚨",information_desk_person_tone4="💁🏾",information_desk_person_tone5="💁🏿",part_alternation_mark="〽",pray_tone5="🙏🏿",pray_tone4="🙏🏾",pray_tone3="🙏🏽",pray_tone2="🙏🏼",pray_tone1="🙏🏻",smile="😄",large_blue_circle="🔵",man_tone4="👨🏾",man_tone5="👨🏿",fax="📠",woman="👩",man_tone1="👨🏻",man_tone2="👨🏼",man_tone3="👨🏽",eye_in_speech_bubble="👁🗨",blowfish="🐡",card_box="🗃",ticket="🎫",ramen="🍜",twisted_rightwards_arrows="🔀",swimmer_tone4="🏊🏾",swimmer_tone5="🏊🏿",swimmer_tone1="🏊🏻",swimmer_tone2="🏊🏼",swimmer_tone3="🏊🏽",saxophone="🎷",bath_tone1="🛀🏻",notebook_with_decorative_cover="📔",bath_tone3="🛀🏽",ten="🔟",raising_hand_tone4="🙋🏾",tea="🍵",raising_hand_tone1="🙋🏻",raising_hand_tone2="🙋🏼",raising_hand_tone3="🙋🏽",zero="0⃣",ribbon="🎀",santa_tone1="🎅🏻",abc="🔤",clock="🕰",purple_heart="💜",bow_tone1="🙇🏻",no_smoking="🚭",flag_cl="🇨🇱",surfer="🏄",newspaper="📰",busstop="🚏",new_moon="🌑",traffic_light="🚥",thumbsup="👍",no_entry="⛔",name_badge="📛",classical_building="🏛",hamster="🐹",pick="⛏",two_women_holding_hands="👭",family_mmbb="👨👨👦👦",family="👪",rice_cracker="🍘",wind_blowing_face="🌬",inbox_tray="📥",tired_face="😫",carousel_horse="🎠",eye="👁",poodle="🐩",chestnut="🌰",slight_smile="🙂",mailbox_closed="📪",cloud_tornado="🌪",jack_o_lantern="🎃",lifter_tone3="🏋🏽",lifter_tone2="🏋🏼",lifter_tone1="🏋🏻",lifter_tone5="🏋🏿",lifter_tone4="🏋🏾",nine="9⃣",chocolate_bar="🍫",v="✌",man_with_turban_tone4="👳🏾",man_with_turban_tone5="👳🏿",man_with_turban_tone2="👳🏼",man_with_turban_tone3="👳🏽",man_with_turban_tone1="👳🏻",family_wwbb="👩👩👦👦",hamburger="🍔",accept="🉑",airplane="✈",dress="👗",speedboat="🚤",ledger="📒",goat="🐐",flag_ae="🇦🇪",flag_ad="🇦🇩",flag_ag="🇦🇬",flag_af="🇦🇫",flag_ac="🇦🇨",flag_am="🇦🇲",flag_al="🇦🇱",flag_ao="🇦🇴",flag_ai="🇦🇮",flag_au="🇦🇺",flag_at="🇦🇹",fork_and_knife="🍴",fast_forward="⏩",flag_as="🇦🇸",flag_ar="🇦🇷",cow2="🐄",flag_ax="🇦🇽",flag_az="🇦🇿",a="🅰",volleyball="🏐",dragon="🐉",wrench="🔧",point_up_2="👆",egg="🍳",small_red_triangle="🔺",soon="🔜",bow_tone4="🙇🏾",joy_cat="😹",pray="🙏",dark_sunglasses="🕶",rugby_football="🏉",soccer="⚽",dolls="🎎",monkey_face="🐵",clap_tone3="👏🏽",bar_chart="📊",european_castle="🏰",military_medal="🎖",frame_photo="🖼",rice_ball="🍙",trolleybus="🚎",older_woman="👵",information_source="ℹ",postal_horn="📯",house="🏠",fish="🐟",bride_with_veil="👰",fist="✊",lipstick="💄",fountain="⛲",cyclone="🌀",thumbsdown_tone2="👎🏼",thumbsdown_tone3="👎🏽",thumbsdown_tone1="👎🏻",thumbsdown_tone4="👎🏾",thumbsdown_tone5="👎🏿",cookie="🍪",heartbeat="💓",blush="😊",fire_engine="🚒",feet="🐾",horse="🐴",blossom="🌼",crossed_swords="⚔",station="🚉",clock730="🕢",banana="🍌",relieved="😌",hotel="🏨",park="🏞",aerial_tramway="🚡",flag_sd="🇸🇩",panda_face="🐼",b="🅱",flag_sx="🇸🇽",six_pointed_star="🔯",shaved_ice="🍧",chipmunk="🐿",mountain="⛰",koala="🐨",white_small_square="▫",open_hands_tone2="👐🏼",open_hands_tone3="👐🏽",u55b6="🈺",open_hands_tone1="👐🏻",open_hands_tone4="👐🏾",open_hands_tone5="👐🏿",baby_tone5="👶🏿",baby_tone4="👶🏾",baby_tone3="👶🏽",baby_tone2="👶🏼",baby_tone1="👶🏻",chart="💹",beach_umbrella="⛱",basketball_player_tone5="⛹🏿",basketball_player_tone4="⛹🏾",basketball_player_tone1="⛹🏻",basketball_player_tone3="⛹🏽",basketball_player_tone2="⛹🏼",mans_shoe="👞",shinto_shrine="⛩",ideograph_advantage="🉐",airplane_arriving="🛬",golf="⛳",minidisc="💽",hugging="🤗",crayon="🖍",point_down="👇",copyright="©",person_with_pouting_face_tone2="🙎🏼",person_with_pouting_face_tone3="🙎🏽",person_with_pouting_face_tone1="🙎🏻",person_with_pouting_face_tone4="🙎🏾",person_with_pouting_face_tone5="🙎🏿",busts_in_silhouette="👥",alarm_clock="⏰",couplekiss="💏",circus_tent="🎪",sunny="☀",incoming_envelope="📨",yellow_heart="💛",cry="😢",x="❌",arrow_up_small="🔼",art="🎨",surfer_tone1="🏄🏻",bride_with_veil_tone4="👰🏾",bride_with_veil_tone5="👰🏿",bride_with_veil_tone2="👰🏼",bride_with_veil_tone3="👰🏽",bride_with_veil_tone1="👰🏻",hibiscus="🌺",black_joker="🃏",raised_hand="✋",no_mouth="😶",basketball_player="⛹",champagne="🍾",no_entry_sign="🚫",older_man="👴",moyai="🗿",mailbox="📫",slight_frown="🙁",statue_of_liberty="🗽",mega="📣",eggplant="🍆",rose="🌹",bell="🔔",battery="🔋",wastebasket="🗑",dancer="💃",page_facing_up="📄",church="⛪",underage="🔞",secret="㊙",clock430="🕟",fork_knife_plate="🍽",u7981="🈲",fire="🔥",cold_sweat="😰",flag_er="🇪🇷",family_mwgg="👨👩👧👧",heart_eyes="😍",guardsman_tone1="💂🏻",guardsman_tone2="💂🏼",guardsman_tone3="💂🏽",guardsman_tone4="💂🏾",earth_africa="🌍",arrow_right_hook="↪",spy_tone2="🕵🏼",closed_umbrella="🌂",bikini="👙",vertical_traffic_light="🚦",kissing="😗",loop="➿",potable_water="🚰",pound="💷",["fleur-de-lis"]="⚜",key2="🗝",heavy_dollar_sign="💲",shamrock="☘",boy_tone4="👦🏾",shirt="👕",kimono="👘",left_luggage="🛅",meat_on_bone="🍖",ok_woman_tone4="🙆🏾",ok_woman_tone5="🙆🏿",arrow_heading_up="⤴",calendar_spiral="🗓",snail="🐌",ok_woman_tone1="🙆🏻",arrow_down_small="🔽",leopard="🐆",paperclips="🖇",cityscape="🏙",woman_tone1="👩🏻",slot_machine="🎰",woman_tone3="👩🏽",woman_tone4="👩🏾",woman_tone5="👩🏿",euro="💶",musical_score="🎼",triangular_ruler="📐",flags="🎏",five="5⃣",love_hotel="🏩",hotdog="🌭",speak_no_evil="🙊",eyeglasses="👓",dancer_tone4="💃🏾",dancer_tone5="💃🏿",vulcan_tone4="🖖🏾",bridge_at_night="🌉",writing_hand_tone1="✍🏻",couch="🛋",vulcan_tone1="🖖🏻",vulcan_tone2="🖖🏼",vulcan_tone3="🖖🏽",womans_hat="👒",sandal="👡",cherries="🍒",full_moon="🌕",flag_om="🇴🇲",play_pause="⏯",couple="👫",money_mouth="🤑",womans_clothes="👚",globe_with_meridians="🌐",bath_tone5="🛀🏿",bangbang="‼",stuck_out_tongue_winking_eye="😜",heart="❤",bamboo="🎍",mahjong="🀄",waning_gibbous_moon="🌖",back="🔙",point_up_2_tone4="👆🏾",point_up_2_tone5="👆🏿",lips="👄",point_up_2_tone1="👆🏻",point_up_2_tone2="👆🏼",point_up_2_tone3="👆🏽",candle="🕯",middle_finger_tone3="🖕🏽",middle_finger_tone2="🖕🏼",middle_finger_tone1="🖕🏻",middle_finger_tone5="🖕🏿",middle_finger_tone4="🖕🏾",heavy_minus_sign="➖",nose="👃",zzz="💤",stew="🍲",santa="🎅",tropical_fish="🐠",point_up_tone1="☝🏻",point_up_tone3="☝🏽",point_up_tone2="☝🏼",point_up_tone5="☝🏿",point_up_tone4="☝🏾",field_hockey="🏑",school_satchel="🎒",womens="🚺",baby_symbol="🚼",baby_chick="🐤",ok_hand_tone2="👌🏼",ok_hand_tone3="👌🏽",ok_hand_tone1="👌🏻",ok_hand_tone4="👌🏾",ok_hand_tone5="👌🏿",family_mmgb="👨👨👧👦",last_quarter_moon="🌗",tada="🎉",clock530="🕠",question="❓",registered="®",level_slider="🎚",black_circle="⚫",atom="⚛",penguin="🐧",electric_plug="🔌",skull="💀",kiss_mm="👨❤💋👨",walking_tone4="🚶🏾",fries="🍟",up="🆙",walking_tone3="🚶🏽",walking_tone2="🚶🏼",athletic_shoe="👟",hatched_chick="🐥",black_nib="✒",black_large_square="⬛",bow_and_arrow="🏹",rainbow="🌈",metal_tone5="🤘🏿",metal_tone4="🤘🏾",lemon="🍋",metal_tone2="🤘🏼",peach="🍑",peace="☮",steam_locomotive="🚂",oncoming_bus="🚍",heart_eyes_cat="😻",smiley="😃",haircut_tone1="💇🏻",haircut_tone2="💇🏼",u6e80="🈵",haircut_tone4="💇🏾",haircut_tone5="💇🏿",black_medium_square="◼",closed_book="📕",desert="🏜",expressionless="😑",dvd="📀",construction_worker_tone2="👷🏼",construction_worker_tone3="👷🏽",construction_worker_tone4="👷🏾",construction_worker_tone5="👷🏿",mag_right="🔎",bento="🍱",scroll="📜",flag_nl="🇳🇱",flag_no="🇳🇴",flag_ni="🇳🇮",european_post_office="🏤",flag_ne="🇳🇪",flag_nf="🇳🇫",flag_ng="🇳🇬",flag_na="🇳🇦",flag_nc="🇳🇨",alien="👽",first_quarter_moon_with_face="🌛",flag_nz="🇳🇿",flag_nu="🇳🇺",golfer="🏌",flag_np="🇳🇵",flag_nr="🇳🇷",anguished="😧",mosque="🕌",point_left_tone1="👈🏻",ear_tone1="👂🏻",ear_tone2="👂🏼",ear_tone3="👂🏽",ear_tone4="👂🏾",ear_tone5="👂🏿",eight_pointed_black_star="✴",wave="👋",runner_tone5="🏃🏿",runner_tone4="🏃🏾",runner_tone3="🏃🏽",runner_tone2="🏃🏼",runner_tone1="🏃🏻",railway_car="🚃",notes="🎶",no_good="🙅",trackball="🖲",spaghetti="🍝",love_letter="💌",clipboard="📋",baby_bottle="🍼",bird="🐦",card_index="📇",punch="👊",leo="♌",house_with_garden="🏡",family_wwgg="👩👩👧👧",family_wwgb="👩👩👧👦",see_no_evil="🙈",metro="🚇",popcorn="🍿",apple="🍎",scales="⚖",sleeping_accommodation="🛌",clock230="🕝",tools="🛠",cloud="☁",honey_pot="🍯",ballot_box="🗳",frog="🐸",camera="📷",crab="🦀",video_camera="📹",pencil="📝",thunder_cloud_rain="⛈",mountain_bicyclist="🚵",tangerine="🍊",train="🚋",rabbit="🐰",baby="👶",palm_tree="🌴",capital_abcd="🔠",put_litter_in_its_place="🚮",coffin="⚰",abcd="🔡",lock="🔒",pig2="🐖",family_mwg="👨👩👧",point_right_tone5="👉🏿",trumpet="🎺",film_frames="🎞",six="6⃣",leftwards_arrow_with_hook="↩",earth_asia="🌏",heavy_check_mark="✔",notebook="📓",taco="🌮",tomato="🍅",robot="🤖",mute="🔇",symbols="🔣",motorcycle="🏍",thermometer_face="🤒",paperclip="📎",moneybag="💰",neutral_face="😐",white_sun_rain_cloud="🌦",snake="🐍",kiss="💋",blue_car="🚙",confetti_ball="🎊",tram="🚊",repeat_one="🔂",smiley_cat="😺",beginner="🔰",mobile_phone_off="📴",books="📚",["8ball"]="🎱",cocktail="🍸",flag_ge="🇬🇪",horse_racing_tone2="🏇🏼",flag_mh="🇲🇭",flag_mk="🇲🇰",flag_mm="🇲🇲",flag_ml="🇲🇱",flag_mo="🇲🇴",flag_mn="🇲🇳",flag_ma="🇲🇦",flag_mc="🇲🇨",flag_me="🇲🇪",flag_md="🇲🇩",flag_mg="🇲🇬",flag_mf="🇲🇫",flag_my="🇲🇾",flag_mx="🇲🇽",flag_mz="🇲🇿",mountain_snow="🏔",flag_mp="🇲🇵",flag_ms="🇲🇸",flag_mr="🇲🇷",flag_mu="🇲🇺",flag_mt="🇲🇹",flag_mw="🇲🇼",flag_mv="🇲🇻",timer="⏲",passport_control="🛂",small_blue_diamond="🔹",lion_face="🦁",white_check_mark="✅",bouquet="💐",track_previous="⏮",monkey="🐒",tone4="🏾",closed_lock_with_key="🔐",family_wwb="👩👩👦",family_wwg="👩👩👧",tone1="🏻",crescent_moon="🌙",shell="🐚",gear="⚙",tone2="🏼",small_red_triangle_down="🔻",nut_and_bolt="🔩",umbrella2="☂",unamused="😒",fuelpump="⛽",bed="🛏",bee="🐝",round_pushpin="📍",flag_white="🏳",microphone="🎤",bus="🚌",eight="8⃣",handbag="👜",medal="🏅",arrows_clockwise="🔃",urn="⚱",bookmark_tabs="📑",new_moon_with_face="🌚",fallen_leaf="🍂",horse_racing="🏇",chicken="🐔",ear="👂",wheel_of_dharma="☸",arrow_lower_right="↘",man_with_gua_pi_mao_tone5="👲🏿",scorpion="🦂",waning_crescent_moon="🌘",man_with_gua_pi_mao_tone2="👲🏼",man_with_gua_pi_mao_tone1="👲🏻",bug="🐛",virgo="♍",libra="♎",angel_tone1="👼🏻",angel_tone3="👼🏽",angel_tone2="👼🏼",angel_tone5="👼🏿",sagittarius="♐",bear="🐻",information_desk_person_tone3="💁🏽",no_mobile_phones="📵",hand_splayed="🖐",motorboat="🛥",calling="📲",interrobang="⁉",oncoming_taxi="🚖",flag_lt="🇱🇹",flag_lu="🇱🇺",flag_lr="🇱🇷",flag_ls="🇱🇸",flag_ly="🇱🇾",bellhop="🛎",arrow_down="⬇",flag_lc="🇱🇨",flag_la="🇱🇦",flag_lk="🇱🇰",flag_li="🇱🇮",ferris_wheel="🎡",hand_splayed_tone2="🖐🏼",large_blue_diamond="🔷",cat2="🐈",icecream="🍦",tent="⛺",joy="😂",hand_splayed_tone4="🖐🏾",file_cabinet="🗄",key="🔑",weary="😩",bath_tone2="🛀🏼",flag_lv="🇱🇻",low_brightness="🔅",rowboat_tone5="🚣🏿",rowboat_tone2="🚣🏼",rowboat_tone3="🚣🏽",four_leaf_clover="🍀",space_invader="👾",cl="🆑",cd="💿",bath_tone4="🛀🏾",flag_za="🇿🇦",swimmer="🏊",wavy_dash="〰",flag_zm="🇿🇲",flag_zw="🇿🇼",raised_hands_tone5="🙌🏿",two_hearts="💕",bulb="💡",cop_tone4="👮🏾",cop_tone5="👮🏿",cop_tone2="👮🏼",cop_tone3="👮🏽",cop_tone1="👮🏻",open_file_folder="📂",homes="🏘",raised_hands_tone2="🙌🏼",fearful="😨",grinning="😀",bow_tone5="🙇🏿",santa_tone3="🎅🏽",santa_tone2="🎅🏼",santa_tone5="🎅🏿",santa_tone4="🎅🏾",bow_tone2="🙇🏼",bow_tone3="🙇🏽",bathtub="🛁",ping_pong="🏓",u5272="🈹",rooster="🐓",vs="🆚",bullettrain_front="🚅",airplane_small="🛩",white_circle="⚪",balloon="🎈",cross="✝",princess_tone3="👸🏽",speaker="🔈",zipper_mouth="🤐",u6307="🈯",whale="🐳",pensive="😔",signal_strength="📶",muscle="💪",rocket="🚀",camel="🐫",boot="👢",flashlight="🔦",spy_tone4="🕵🏾",spy_tone5="🕵🏿",ski="🎿",spy_tone3="🕵🏽",musical_keyboard="🎹",spy_tone1="🕵🏻",rolling_eyes="🙄",clock1="🕐",clock2="🕑",clock3="🕒",clock4="🕓",clock5="🕔",clock6="🕕",clock7="🕖",clock8="🕗",clock9="🕘",doughnut="🍩",dancer_tone1="💃🏻",dancer_tone2="💃🏼",dancer_tone3="💃🏽",candy="🍬",two_men_holding_hands="👬",badminton="🏸",bust_in_silhouette="👤",writing_hand_tone2="✍🏼",sunflower="🌻",["e-mail"]="📧",chains="⛓",kissing_smiling_eyes="😙",fish_cake="🍥",no_pedestrians="🚷",v_tone4="✌🏾",v_tone5="✌🏿",v_tone1="✌🏻",v_tone2="✌🏼",v_tone3="✌🏽",arrow_backward="◀",clock12="🕛",clock10="🕙",clock11="🕚",sweat="😓",mountain_railway="🚞",tongue="👅",black_square_button="🔲",do_not_litter="🚯",nose_tone4="👃🏾",nose_tone5="👃🏿",nose_tone2="👃🏼",nose_tone3="👃🏽",nose_tone1="👃🏻",horse_racing_tone5="🏇🏿",horse_racing_tone4="🏇🏾",horse_racing_tone3="🏇🏽",ok_hand="👌",horse_racing_tone1="🏇🏻",custard="🍮",rowboat="🚣",white_sun_small_cloud="🌤",flag_kr="🇰🇷",cricket="🏏",flag_kp="🇰🇵",flag_kw="🇰🇼",flag_kz="🇰🇿",flag_ky="🇰🇾",construction="🚧",flag_kg="🇰🇬",flag_ke="🇰🇪",flag_ki="🇰🇮",flag_kh="🇰🇭",flag_kn="🇰🇳",flag_km="🇰🇲",cake="🍰",flag_wf="🇼🇫",mortar_board="🎓",pig="🐷",flag_ws="🇼🇸",person_frowning="🙍",arrow_upper_right="↗",book="📖",clock1130="🕦",boom="💥",["repeat"]="🔁",star="⭐",rabbit2="🐇",footprints="👣",ghost="👻",droplet="💧",vibration_mode="📳",flag_ye="🇾🇪",flag_yt="🇾🇹", } emoji['slightly_smiling_face'] = ':)' +emoji['+1'] = "👍" +emoji['-1'] = "👎" local function str2emoji(str) if not str then return '' end - return (str:gsub(':[a-zA-Z0-9%-_]+:', function(word) + return (str:gsub(':[a-zA-Z0-9%-_+]+:', function(word) return emoji[word:match(':(.+):')] or word end)) end @@ -152,7 +158,6 @@ function emoji_completion_cb(data, completion_item, buffer, completion) end function incoming_cb(data, modifier, modifier_data, msg) - --local plugin, buffer_name, tags = modifier_data:match('^(.-);(.-);(.-)$') -- Only replace in incoming "messages" if modifier_data:match('nick_') then return str2emoji(msg) diff --git a/lua/urlselect.lua b/lua/urlselect.lua index 81cefeb4..f1ad9c3d 100644 --- a/lua/urlselect.lua +++ b/lua/urlselect.lua @@ -16,7 +16,7 @@ local w = weechat local g = { script = { name = "urlselect", - version = "0.4", + version = "0.5", author = "tomoe-mami ", license = "WTFPL", description = "Interactively select URL" @@ -1501,22 +1501,43 @@ function setup_bar() for key, info in pairs(g.bar) do local bar = w.bar_search(info.name) if not bar or bar == "" then - bar = w.bar_new( - info.name, -- name - "on", -- hidden? - settings[key].priority, -- priority - "root", -- type - "active", -- condition - "top", -- position - settings[key].filling_tb, -- filling top/bottom - "vertical", -- filling left/right - 0, -- size - settings[key].max_size, -- max size - "default", -- text fg - "cyan", -- delim fg - "default", -- bar bg - "on", -- separator - settings[key].items) -- items + local wee_ver = tonumber(w.info_get("version_number", "") or 0) + if wee_ver >= 0x02090000 then + bar = w.bar_new( + info.name, -- name + "on", -- hidden? + settings[key].priority, -- priority + "root", -- type + "active", -- condition + "top", -- position + settings[key].filling_tb, -- filling top/bottom + "vertical", -- filling left/right + 0, -- size + settings[key].max_size, -- max size + "default", -- text fg + "cyan", -- delim fg + "default", -- bar bg + "default", -- bar bg inactive + "on", -- separator + settings[key].items) -- items + else + bar = w.bar_new( + info.name, -- name + "on", -- hidden? + settings[key].priority, -- priority + "root", -- type + "active", -- condition + "top", -- position + settings[key].filling_tb, -- filling top/bottom + "vertical", -- filling left/right + 0, -- size + settings[key].max_size, -- max size + "default", -- text fg + "cyan", -- delim fg + "default", -- bar bg + "on", -- separator + settings[key].items) -- items + end end g.bar[key].ptr = bar end diff --git a/perl/atcomplete.pl b/perl/atcomplete.pl index e0b5d5c8..0ea2f589 100644 --- a/perl/atcomplete.pl +++ b/perl/atcomplete.pl @@ -1,4 +1,6 @@ -# Copyright 2015 by David A. Golden. All rights reserved. +# SPDX-FileCopyrightText: 2015-2016 David A. Golden +# +# SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -28,7 +30,7 @@ use strict; use warnings; my $SCRIPT_NAME = "atcomplete"; -my $VERSION = "0.001"; +my $VERSION = "0.002"; my %options_default = ( 'enabled' => ['on', 'enable completion of nicks starting with @'], diff --git a/perl/autonickprefix.pl b/perl/autonickprefix.pl new file mode 100644 index 00000000..27ad0b00 --- /dev/null +++ b/perl/autonickprefix.pl @@ -0,0 +1,51 @@ +use strict; +use Encode qw(encode_utf8); + +weechat::register( + 'autonickprefix', + 'Juerd <#####@juerd.nl>', + '1.00', + 'PD', + "Change 'nick: ' prefix if the nick is changed while you're still editing.", + '', + '' +); + +# This is a port of the Irssi script autonickprefix.pl, the main difference +# being that WeeChat has an input *per buffer*, so the script needs to iterate +# over the buffers instead of just the current one, because there could be +# multiple messages waiting to be sent. + +sub nick_changed { + my (undef, $server, $args) = @_; + + $server = (split /,/, $server)[0]; + + my ($oldnick, $newnick) = $args =~ /\:(.*)\!(?:.*)\:(.*)/ + or return weechat::WEECHAT_RC_OK; + + my $hdata = weechat::hdata_get("buffer"); + my $buffer = weechat::hdata_get_list($hdata, "gui_buffers"); + my $char = weechat::config_get('completion.nick_completer'); + + while ($buffer) { + weechat::buffer_get_string($buffer,'localvar_server') eq $server + or next; + + my $pos = weechat::buffer_get_integer($buffer, 'input_pos'); + my $input = weechat::buffer_get_string($buffer, 'input'); + $pos >= length("$oldnick$char") or next; + + $input =~ s/^\Q$oldnick$char/$newnick$char/ or next; + my $delta = length($newnick) - length($oldnick); + + weechat::buffer_set($buffer, "input", $input); + weechat::buffer_set($buffer, "input_pos", $pos + $delta); + } continue { + $buffer = weechat::hdata_pointer($hdata, $buffer, "next_buffer"); + } + + return weechat::WEECHAT_RC_OK; +} + +weechat::hook_signal("*,irc_in_nick", "nick_changed", ""); diff --git a/perl/beep.pl b/perl/beep.pl deleted file mode 100644 index 69096c21..00000000 --- a/perl/beep.pl +++ /dev/null @@ -1,257 +0,0 @@ -# -# Copyright (C) 2006-2012 Sebastien Helleu -# Copyright (C) 2011 Nils Görs -# Copyright (C) 2011 ArZa -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# -# Beep (terminal bell) and/or run command on highlight/private message or new DCC. -# -# History: -# 2012-06-05, ldvx: -# version 1.1: Added wildcard support for whitelist_nicks. -# 2012-05-09, ldvx : -# version 1.0: Added beep_pv_blacklist, beep_highlight_blacklist, blacklist_nicks, -# and wildcard support for blacklist_nicks. -# 2012-05-02, ldvx : -# version 0.9: fix regex for nick in tags, add options "whitelist_channels" -# and "bell_always" -# 2012-04-19, ldvx : -# version 0.8: add whitelist, trigger, use hook_process for commands, -# rename option "beep_command" to "beep_command_pv", add help for options -# 2011-04-16, ArZa : -# version 0.7: fix default beep command -# 2011-03-11, nils_2 : -# version 0.6: add additional command options for dcc and highlight -# 2011-03-09, nils_2 : -# version 0.5: add option for beep command and dcc -# 2009-05-02, Sebastien Helleu : -# version 0.4: sync with last API changes -# 2008-11-05, Sebastien Helleu : -# version 0.3: conversion to WeeChat 0.3.0+ -# 2007-08-10, Sebastien Helleu : -# version 0.2: upgraded licence to GPL 3 -# 2006-09-02, Sebastien Helleu : -# version 0.1: initial release - -use strict; -my $SCRIPT_NAME = "beep"; -my $VERSION = "1.1"; - -# default values in setup file (~/.weechat/plugins.conf) -my %options_default = ('beep_pv' => ['on', 'beep on private message'], - 'beep_pv_whitelist' => ['off', 'turn whitelist for private messages on or off'], - 'beep_pv_blacklist' => ['off', 'turn blacklist for private messages on or off'], - 'beep_highlight' => ['on', 'beep on highlight'], - 'beep_highlight_whitelist' => ['off', 'turn whitelist for highlights on or off'], - 'beep_highlight_blacklist' => ['off', 'turn blacklist for highlights on or off'], - 'beep_dcc' => ['on', 'beep on dcc'], - 'beep_trigger_pv' => ['', 'word that will trigger execution of beep_command_pv (it empty, anything will trigger)'], - 'beep_trigger_highlight' => ['', 'word that will trigger execution of beep_command_highlight (if empty, anything will trigger)'], - 'beep_command_pv' => ['$bell', 'command for beep on private message, special value "$bell" is allowed, as well as "$bell;command"'], - 'beep_command_highlight' => ['$bell', 'command for beep on highlight, special value "$bell" is allowed, as well as "$bell;command"'], - 'beep_command_dcc' => ['$bell', 'command for beep on dcc, special value "$bell" is allowed, as well as "$bell;command"'], - 'beep_command_timeout' => ['30000', 'timeout for command run (in milliseconds, 0 = never kill (not recommended))'], - 'whitelist_nicks' => ['', 'comma-separated list of "server.nick": if not empty, only these nicks will trigger execution of commands (example: "freenode.nick1,freenode.nick2")'], - 'blacklist_nicks' => ['', 'comma-separated list of "server.nick": if not empty, these nicks will not be able to trigger execution of commands. Cannot be used in conjuction with whitelist (example: "freenode.nick1,freenode.nick2")'], - 'whitelist_channels' => ['', 'comma-separated list of "server.#channel": if not empty, only these channels will trigger execution of commands (example: "freenode.#weechat,freenode.#channel2")'], - 'bell_always' => ['', 'use $bell on private messages and/or highlights regardless of trigger and whitelist settings (example: "pv,highlight")'], -); -my %options = (); - -weechat::register($SCRIPT_NAME, "FlashCode ", $VERSION, - "GPL3", "Beep (terminal bell) and/or run command on highlight/private message or new DCC", "", ""); -init_config(); - -weechat::hook_config("plugins.var.perl.$SCRIPT_NAME.*", "toggle_config_by_set", ""); -weechat::hook_print("", "", "", 1, "pv_and_highlight", ""); -weechat::hook_signal("irc_dcc", "dcc", ""); - -sub pv_and_highlight -{ - my ($data, $buffer, $date, $tags, $displayed, $highlight, $prefix, $message) = @_; - - # return if message is filtered - return weechat::WEECHAT_RC_OK if ($displayed != 1); - - # return if nick in message is own nick - my $nick = ""; - $nick = $2 if ($tags =~ m/(^|,)nick_([^,]*)(,|$)/); - return weechat::WEECHAT_RC_OK if (weechat::buffer_get_string($buffer, "localvar_nick") eq $nick); - - # highlight - if ($highlight) - { - # Always print visual bel, regardless of whitelist and trigger settings - # beep_command_highlight does not need to contain $bell - if ($options{bell_always} =~ m/(^|,)highlight(,|$)/) - { - print STDERR "\a"; - } - # Channels whitelist for highlights - if ($options{beep_highlight} eq "on") - { - if ($options{whitelist_channels} ne "") - { - my $serverandchannel = weechat::buffer_get_string($buffer, "localvar_server"). "." . - weechat::buffer_get_string($buffer, "localvar_channel"); - if ($options{beep_trigger_highlight} eq "" or $message =~ m/\b$options{beep_trigger_highlight}\b/) - { - if ($options{whitelist_channels} =~ /(^|,)$serverandchannel(,|$)/) - { - beep_exec_command($options{beep_command_highlight}); - } - # What if we are highlighted and we're in a PM? For now, do nothing. - } - } - else - { - # Execute $bell and/or command with trigger and whitelist/blacklist settings - beep_trigger_whitelist_blacklist($buffer, $message, $nick, $options{beep_trigger_highlight}, - $options{beep_highlight_whitelist}, $options{beep_highlight_blacklist}, - $options{beep_command_highlight}); - } - } - } - # private message - elsif (weechat::buffer_get_string($buffer, "localvar_type") eq "private" and $tags =~ m/(^|,)notify_private(,|$)/) - { - # Always print visual bel, regardless of whitelist and trigger settings - # beep_command_pv does not need to contain $bell - if ($options{bell_always} =~ m/(^|,)pv(,|$)/) - { - print STDERR "\a"; - } - # Execute $bell and/or command with trigger and whitelist/blacklist settings - if ($options{beep_pv} eq "on") - { - beep_trigger_whitelist_blacklist($buffer, $message, $nick, $options{beep_trigger_pv}, - $options{beep_pv_whitelist}, $options{beep_pv_blacklist}, - $options{beep_command_pv}); - } - } - return weechat::WEECHAT_RC_OK; -} - -sub dcc -{ - beep_exec_command($options{beep_command_dcc}) if ($options{beep_dcc} eq "on"); - return weechat::WEECHAT_RC_OK; -} - -sub beep_trigger_whitelist_blacklist -{ - my ($buffer, $message, $nick, $trigger, $whitelist, $blacklist, $command) = @_; - - if ($trigger eq "" or $message =~ m/\b$trigger\b/) - { - my $serverandnick = weechat::buffer_get_string($buffer, "localvar_server").".".$nick; - if ($whitelist eq "on" and $options{whitelist_nicks} ne "") - { - # What to do if there's a wildcard in the whitelit - if ($options{whitelist_nicks} =~ m/\*/ and - match_in_wild_card($serverandnick, $options{whitelist_nicks})) - { - beep_exec_command($command); - } - # What to do if there's no wildcard in the whitelist - elsif ($options{whitelist_nicks} =~ /(^|,)$serverandnick(,|$)/) - { - beep_exec_command($command); - } - } - elsif ($blacklist eq "on" and $options{blacklist_nicks} ne "") - { - # What to do if there's a wildcard in the blacklist - if ($options{blacklist_nicks} =~ m/\*/ and - !match_in_wild_card($serverandnick, $options{blacklist_nicks})) - { - beep_exec_command($command); - } - # What to do if there's no wildcard in the blacklist - elsif ($options{blacklist_nicks} !~ /(^|,)$serverandnick(,|$)/) - { - beep_exec_command($command); - } - } - # What to do if we are not using whitelist of blacklist feature - elsif ($whitelist eq "off" and $blacklist eq "off") - { - beep_exec_command($command); - } - } -} - -sub beep_exec_command -{ - my $command = $_[0]; - if ($command =~ /^\$bell/) - { - print STDERR "\a"; - ($command) = $command =~ /^\$bell;(.+)$/; - } - weechat::hook_process($command, $options{beep_command_timeout}, "my_process", "") if ($command); -} - -sub match_in_wild_card -{ - my ($serverandnick, $white_or_black) = @_; - my $nick_iter; - my @array_of_nicks = split(",", $white_or_black); - - foreach $nick_iter (@array_of_nicks) - { - $nick_iter =~ s/\*/[^,]*/g; - if ($serverandnick =~ /$nick_iter/) - { - return 1; - } - } - return 0; -} - -sub my_process -{ - return weechat::WEECHAT_RC_OK; -} - -sub toggle_config_by_set -{ - my ($pointer, $name, $value) = @_; - $name = substr($name, length("plugins.var.perl.".$SCRIPT_NAME."."), length($name)); - $options{$name} = $value; - return weechat::WEECHAT_RC_OK; -} - -sub init_config -{ - my $version = weechat::info_get("version_number", "") || 0; - foreach my $option (keys %options_default) - { - if (!weechat::config_is_set_plugin($option)) - { - weechat::config_set_plugin($option, $options_default{$option}[0]); - $options{$option} = $options_default{$option}[0]; - } - else - { - $options{$option} = weechat::config_get_plugin($option); - } - if ($version >= 0x00030500) - { - weechat::config_set_desc_plugin($option, $options_default{$option}[1]." (default: \"".$options_default{$option}[0]."\")"); - } - } -} diff --git a/perl/buddylist.pl b/perl/buddylist.pl index 1e4101b2..43db698e 100644 --- a/perl/buddylist.pl +++ b/perl/buddylist.pl @@ -1,5 +1,6 @@ # -# Copyright (c) 2010-2013 by Nils Görs +# Copyright (c) 2010-2023 by Nils Görs +# Copyright (c) 2025 by Kamil Wiśniewski # # display the status and visited buffers of your buddies in a buddylist bar # @@ -15,7 +16,11 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# +# 2.3 : fix (get nickname) function and remake it to use "irc_message_parse". Fixed "completion_list_add" for compatibility with WeeChat >= 2.9 +# 2.2 : fix: make /whois lower-case +# 2.1 : add compatibility with WeeChat >= 3.2 (XDG directories) +# 2.0 : make call to bar_new compatible with WeeChat >= 2.9 +# 1.9 : added: cursor support # 1.8 : fixed: problem with temporary server # : added: %h variable for filename # 1.7 : fixed: perl error when adding and removing nick @@ -34,7 +39,7 @@ # 1.2.1 : fixed: bitlbee_service was not set, for new added buddy # v1.2 : added: function "hide_bar = always" (requested by: Emralegna) # : added: buddies will be separated by protocol (msn/jabber etc.pp.) on bitlbee server (requested by: ArcAngel) -# : added: options "text.online", "text.away", "text.offline" (maybe usefull for color-blind people:-) +# : added: options "text.online", "text.away", "text.offline" (maybe usefull for color-blind people:-) # : added: function weechat_config_set_desc_plugin() (only for weechat >= v0.3.5) # v1.1 : fixed: offline users on bitlbee were shown as away. (reported and beta-testing by javuchi) # v1.0 : redirection implemented (needs weechat >= 0.3.4). Now, its a real buddylist @@ -68,7 +73,7 @@ # 0.4 : added option "sort" # 0.3 : remove spaces for indenting when bar position is top/bottom # hook_config when settings changed. -# 0.2 : work-around for crash when searching nick in buffer without nicklist (function nicklist_search_nick) removed +# 0.2 : work-around for crash when searching nick in buffer without nicklist (function nicklist_search_nick) removed # 0.1 : initial release # # Development is currently hosted at @@ -81,7 +86,7 @@ use strict; my $prgname = "buddylist"; -my $version = "1.8"; +my $version = "2.3"; my $description = "display status from your buddies a bar-item."; # -------------------------------[ config ]------------------------------------- @@ -121,6 +126,9 @@ "\@chat(*)>item(buddylist):button1-gesture-*" => "hsignal:buddylist_mouse", "\@item(buddylist):button1-gesture-*" => "hsignal:buddylist_mouse"); +my %cursor_keys = ( "\@item(buddylist):q" => "hsignal:buddylist_cursor", + "\@item(buddylist):w" => "hsignal:buddylist_cursor"); + my $debug_redir_out = "off"; # ------------------------------[ internal ]----------------------------------- @@ -168,9 +176,15 @@ buddylist_read(); weechat::bar_item_new($prgname, "build_buddylist", ""); -weechat::bar_new($prgname, "1", "0", "root", "", "left", "horizontal", - "vertical", "0", "0", "default", "default", "default", "1", - $prgname); +if ($weechat_version >= 0x02090000) { + weechat::bar_new($prgname, "1", "0", "root", "", "left", "horizontal", + "vertical", "0", "0", "default", "default", "default", "default", "1", + $prgname); +} else { + weechat::bar_new($prgname, "1", "0", "root", "", "left", "horizontal", + "vertical", "0", "0", "default", "default", "default", "1", + $prgname); +} weechat::hook_signal("buffer_*", "buddylist_signal_buffer", ""); @@ -196,21 +210,23 @@ if ($weechat_version >= 0x00030600){ weechat::hook_focus($prgname, "hook_focus_buddylist", ""); weechat::hook_hsignal("buddylist_mouse","buddylist_hsignal_mouse", ""); + weechat::hook_hsignal("buddylist_cursor","buddylist_hsignal_cursor", ""); weechat::key_bind("mouse", \%mouse_keys); + weechat::key_bind("cursor", \%cursor_keys); } hook_timer_and_redirect() if ($default_options{check_buddies} ne "0" and $default_options{use_redirection} eq "on"); weechat::hook_command($prgname, $description, - "[nick_1 [... nick_n]] | [nick_1 [... nick_n]]", + "[nick_1 [... nick_n]] | [nick_1 [... nick_n]]", " [nick(s)] add nick(s) to the buddylist\n". " [nick(s)] delete nick(s) from the buddylist\n". " show buddylist\n". "\n". "Options:\n". - "'plugins.var.perl.buddylist.buddylist' : path/file-name to store your buddies. \"%h\" will be replaced by WeeChat home (by default: ~/.weechat)\n". + "'plugins.var.perl.buddylist.buddylist' : path/file-name to store your buddies. \"%h\" will be replaced by WeeChat config directory\n". "\n". "'plugins.var.perl.buddylist.color.default : fall back color. (default: standard weechat color)\n". "'plugins.var.perl.buddylist.color.online' : color for " . weechat::color($default_color_buddylist{online}) . "online " . weechat::color("reset") . "buddies.\n". @@ -248,10 +264,13 @@ "\n". "'plugins.var.perl.buddylist.use.redirection' : using redirection to get status of buddies (needs weechat >=0.3.4) (default: on).\n". "\n\n". - "Mouse-support (standard key bindings):\n". + "Mouse-support:\n". " click left mouse-button on buddy to open a query buffer.\n". " add a buddy by dragging a nick with left mouse-button from nicklist or chat-area and drop on buddylist.\n". " remove a buddy by dragging a buddy with left mouse-button from buddylist and drop it on any area.\n". + "Cursor-Mode:\n". + " q open query with nick (/query)\n". + " w query information about user (/whois)\n". "\n\n". "Troubleshooting:\n". "If buddylist will not be refreshed in nicklist-mode, check the following WeeChat options:\n". @@ -428,7 +447,7 @@ sub build_buddylist{ # sorted by status first, then bitlbee_service and nick case insensitiv at least # foreach my $n (sort { $nick_structure{$s}{$a}->{status} cmp $nick_structure{$s}{$b}->{status}} (sort {uc($a) cmp uc($b)} (sort keys(%{$nick_structure{$s}})))){ - + my $status_output = ""; my $status_output_copy = ""; foreach my $n (sort { $nick_structure{$s}{$a}->{status} cmp $nick_structure{$s}{$b}->{status}} (sort { $nick_structure{$s}{$a}->{bitlbee_service} cmp $nick_structure{$s}{$b}->{bitlbee_service}} (sort {uc($a) cmp uc($b)} (sort keys(%{$nick_structure{$s}})) ) ) ){ @@ -943,13 +962,8 @@ sub buddylist_signal_buffer sub weechat_dir { - my $dir = weechat::config_get_plugin("buddylist"); - if ( $dir =~ /%h/ ) - { - my $weechat_dir = weechat::info_get( 'weechat_dir', ''); - $dir =~ s/%h/$weechat_dir/; - } - return $dir; + my $options = { "directory" => "config" }; + return weechat::string_eval_path_home(weechat::config_get_plugin("buddylist"), {}, {}, $options); } # init the settings @@ -1097,7 +1111,7 @@ sub init{ if ( ($weechat_version ne "") && (weechat::info_get("version_number", "") >= 0x00030500) ) # v0.3.5 { - weechat::config_set_desc_plugin("buddylist","path/file-name to store your buddies. \"%h\" will be replaced by WeeChat home (by default: ~/.weechat)"); + weechat::config_set_desc_plugin("buddylist","path/file-name to store your buddies. \"%h\" will be replaced by WeeChat config directory"); weechat::config_set_desc_plugin("color.default","fall back color. (default: standard weechat color)"); weechat::config_set_desc_plugin("color.online","color for online buddies"); weechat::config_set_desc_plugin("color.away","color for away buddies"); @@ -1479,9 +1493,27 @@ sub parse_redirect{ } } - # get nick name - my $rfc_311 = " 311 "; # nick username address * :info - my (undef, undef, undef, $nickname2, undef) = split /\s+/, $args, 5 if ($args =~ /($rfc_311)/); # get nickname +# get nick name ($rfc_311) +my $nickname2; +my $parsed_hash = weechat::info_get_hashtable("irc_message_parse", { "message" => $args }); +if ($parsed_hash) { + my $parsed_message = $parsed_hash->{'message_without_tags'}; + if ($parsed_message =~ /^\S+ 311 \S+ (\S+)/) { # gets the nickname + $nickname2 = $1; + if ($debug_redir_out eq "on") { + weechat::print("", "Parsed nickname: $nickname2"); # DEBUG + } + } else { + if ($debug_redir_out eq "on") { + weechat::print("", "Failed to extract nickname from: $parsed_message"); # DEBUG + } + } +} else { + if ($debug_redir_out eq "on") { + weechat::print("", "Failed to parse: $args"); # DEBUG + } +} + # check nick away.... $args =~ /($rfc_301)/; @@ -1516,12 +1548,12 @@ sub buddy_list_completion_cb{ $server = $channel if ( $server eq "server"); # are we in server buffer? foreach my $nickname ( keys %{$nick_structure{$server}} ) { - weechat::hook_completion_list_add($completion, $nickname,1, weechat::WEECHAT_LIST_POS_SORT); + weechat::completion_list_add($completion, $nickname,1, weechat::WEECHAT_LIST_POS_SORT); } return weechat::WEECHAT_RC_OK; } -# -------------------------------[ mouse support ]------------------------------------- +# --------------------------[ mouse and cursor support ]-------------------------------- sub hook_focus_buddylist{ my %info = %{$_[1]}; my $bar_item_line = int($info{"_bar_item_line"}); @@ -1529,9 +1561,9 @@ sub hook_focus_buddylist{ return if ($#buddylist_focus == -1); my $flag = 0; - # if button1 was pressed on "offline" buddy, do nothing!!! - if ( ($info{"_bar_item_name"} eq $prgname) && ($bar_item_line >= 0) && ($bar_item_line <= $#buddylist_focus) && ($info{"_key"} eq "button1" ) ){ - $hash = $buddylist_focus[$bar_item_line]; + # mouse or key pressed on "offline" buddy, do nothing!!! + if ( ($info{"_bar_item_name"} eq $prgname) && ($bar_item_line >= 0) && ($bar_item_line <= $#buddylist_focus) ){ + $hash = $buddylist_focus[$bar_item_line]; my $hash_focus = $hash; while ( my ($key,$value) = each %$hash_focus ){ if ( $key eq "status" and $value eq "2" ){ @@ -1574,4 +1606,20 @@ sub buddylist_hsignal_mouse{ weechat::bar_item_update($prgname); return weechat::WEECHAT_RC_OK; } -# this is the end + +sub buddylist_hsignal_cursor{ + my ($data, $signal, %hash) = ($_[0], $_[1], %{$_[2]}); + + # no server? + return weechat::WEECHAT_RC_OK if (not defined $hash{"server"}); + + # check which key was pressed and do some magic! + if ( $hash{"_key"} eq "q" ){ + weechat::command("", "/query -server " . $hash{"server"} . " " . $hash{"nick"}); + }elsif ( $hash{"_key"} eq "w" ){ + weechat::command(weechat::buffer_search("==","irc.server.".$hash{"server"}), "/whois " . $hash{"nick"}); + } + # STOP cursor mode + weechat::command("", "/cursor stop"); + return weechat::WEECHAT_RC_OK; +} diff --git a/perl/buffers.pl b/perl/buffers.pl deleted file mode 100644 index 73eb4b55..00000000 --- a/perl/buffers.pl +++ /dev/null @@ -1,1840 +0,0 @@ -# -# Copyright (C) 2008-2014 Sebastien Helleu -# Copyright (C) 2011-2013 Nils G -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# -# Display sidebar with list of buffers. -# -# History: -# -# 2016-05-01, mumixam : -# v5.4: added option "detach_buffer_immediately_level" -# 2015-08-21, Matthew Cox -# v5.3: add option "indenting_amount", to adjust the indenting of channel buffers -# 2015-05-02, arza : -# v5.2: truncate long names (name_size_max) more when mark_inactive adds parenthesis -# 2015-03-29, Ed Santiago : -# v5.1: merged buffers: always indent, except when filling is horizontal -# 2014-12-12 -# v5.0: fix cropping non-latin buffer names -# 2014-08-29, Patrick Steinhardt : -# v4.9: add support for specifying custom buffer names -# 2014-07-19, Sebastien Helleu : -# v4.8: add support of ctrl + mouse wheel to jump to previous/next buffer, -# new option "mouse_wheel" -# 2014-06-22, Sebastien Helleu : -# v4.7: fix typos in options -# 2014-04-05, Sebastien Helleu : -# v4.6: add support of hidden buffers (WeeChat >= 0.4.4) -# 2014-01-01, Sebastien Helleu : -# v4.5: add option "mouse_move_buffer" -# 2013-12-11, Sebastien Helleu : -# v4.4: fix buffer number on drag to the end of list when option -# weechat.look.buffer_auto_renumber is off -# 2013-12-10, nils_2@freenode.#weechat: -# v4.3: add options "prefix_bufname" and "suffix_bufname (idea by silverd) -# : fix hook_timer() for show_lag wasn't disabled -# : improved signal handling (less updating of buffers list) -# 2013-11-07, Sebastien Helleu : -# v4.2: use default filling "columns_vertical" when bar position is top/bottom -# 2013-10-31, nils_2@freenode.#weechat: -# v4.1: add option "detach_buffer_immediately" (idea by farn) -# 2013-10-20, nils_2@freenode.#weechat: -# v4.0: add options "detach_displayed_buffers", "detach_display_window_number" -# 2013-09-27, nils_2@freenode.#weechat: -# v3.9: add option "toggle_bar" and option "show_prefix_query" (idea by IvarB) -# : fix problem with linefeed at end of list of buffers (reported by grawity) -# 2012-10-18, nils_2@freenode.#weechat: -# v3.8: add option "mark_inactive", to mark buffers you are not in (idea by xrdodrx) -# : add wildcard "*" for immune_detach_buffers (idea by StarWeaver) -# : add new options "detach_query" and "detach_free_content" (idea by StarWeaver) -# 2012-10-06, Nei : -# v3.7: call menu on right mouse if menu script is loaded. -# 2012-10-06, nils_2 : -# v3.6: add new option "hotlist_counter" (idea by torque). -# 2012-06-02, nils_2 : -# v3.5: add values "server|channel|private|all|keepserver|none" to option "hide_merged_buffers" (suggested by dominikh). -# 2012-05-25, nils_2 : -# v3.4: add new option "show_lag". -# 2012-04-07, Sebastien Helleu : -# v3.3: fix truncation of wide chars in buffer name (option name_size_max) (bug #36034) -# 2012-03-15, nils_2 : -# v3.2: add new option "detach"(weechat >= 0.3.8) -# add new option "immune_detach_buffers" (requested by Mkaysi) -# add new function buffers_whitelist add|del|reset (suggested by FiXato) -# add new function buffers_detach add|del|reset -# 2012-03-09, Sebastien Helleu : -# v3.1: fix reload of config file -# 2012-01-29, nils_2 : -# v3.0: fix: buffers did not update directly during window_switch (reported by FiXato) -# 2012-01-29, nils_2 : -# v2.9: add options "name_size_max" and "name_crop_suffix" -# 2012-01-08, nils_2 : -# v2.8: fix indenting for option "show_number off" -# fix unset of buffer activity in hotlist when buffer was moved with mouse -# add buffer with free content and core buffer sorted first (suggested by nyuszika7h) -# add options queries_default_fg/bg and queries_message_fg/bg (suggested by FiXato) -# add clicking with left button on current buffer will do a jump_previously_visited_buffer (suggested by FiXato) -# add clicking with right button on current buffer will do a jump_next_visited_buffer -# add additional informations in help texts -# add default_fg and default_bg for whitelist channels -# internal changes (script is now 3Kb smaller) -# 2012-01-04, Sebastien Helleu : -# v2.7: fix regex lookup in whitelist buffers list -# 2011-12-04, nils_2 : -# v2.6: add own config file (buffers.conf) -# add new behavior for indenting (under_name) -# add new option to set different color for server buffers and buffers with free content -# 2011-10-30, nils_2 : -# v2.5: add new options "show_number_char" and "color_number_char", -# add help-description for options -# 2011-08-24, Sebastien Helleu : -# v2.4: add mouse support -# 2011-06-06, nils_2 : -# v2.3: added: missed option "color_whitelist_default" -# 2011-03-23, Sebastien Helleu : -# v2.2: fix color of nick prefix with WeeChat >= 0.3.5 -# 2011-02-13, nils_2 : -# v2.1: add options "color_whitelist_*" -# 2010-10-05, Sebastien Helleu : -# v2.0: add options "sort" and "show_number" -# 2010-04-12, Sebastien Helleu : -# v1.9: replace call to log() by length() to align buffer numbers -# 2010-04-02, Sebastien Helleu : -# v1.8: fix bug with background color and option indenting_number -# 2010-04-02, Helios : -# v1.7: add indenting_number option -# 2010-02-25, m4v : -# v1.6: add option to hide empty prefixes -# 2010-02-12, Sebastien Helleu : -# v1.5: add optional nick prefix for buffers like IRC channels -# 2009-09-30, Sebastien Helleu : -# v1.4: remove spaces for indenting when bar position is top/bottom -# 2009-06-14, Sebastien Helleu : -# v1.3: add option "hide_merged_buffers" -# 2009-06-14, Sebastien Helleu : -# v1.2: improve display with merged buffers -# 2009-05-02, Sebastien Helleu : -# v1.1: sync with last API changes -# 2009-02-21, Sebastien Helleu : -# v1.0: remove timer used to update bar item first time (not needed any more) -# 2009-02-17, Sebastien Helleu : -# v0.9: fix bug with indenting of private buffers -# 2009-01-04, Sebastien Helleu : -# v0.8: update syntax for command /set (comments) -# 2008-10-20, Jiri Golembiovsky : -# v0.7: add indenting option -# 2008-10-01, Sebastien Helleu : -# v0.6: add default color for buffers, and color for current active buffer -# 2008-09-18, Sebastien Helleu : -# v0.5: fix color for "low" level entry in hotlist -# 2008-09-18, Sebastien Helleu : -# v0.4: rename option "show_category" to "short_names", -# remove option "color_slash" -# 2008-09-15, Sebastien Helleu : -# v0.3: fix bug with priority in hotlist (var not defined) -# 2008-09-02, Sebastien Helleu : -# v0.2: add color for buffers with activity and config options for -# colors, add config option to display/hide categories -# 2008-03-15, Sebastien Helleu : -# v0.1: script creation -# -# Help about settings: -# display all settings for script (or use iset.pl script to change settings): -# /set buffers* -# show help text for option buffers.look.whitelist_buffers: -# /help buffers.look.whitelist_buffers -# -# Mouse-support (standard key bindings): -# left mouse-button: -# - click on a buffer to switch to selected buffer -# - click on current buffer will do action jump_previously_visited_buffer -# - drag a buffer and drop it on another position will move the buffer to position -# right mouse-button: -# - click on current buffer will do action jump_next_visited_buffer -# - moving buffer to the left/right will close buffer. -# - -use strict; -use Encode qw( decode encode ); -# -----------------------------[ internal ]------------------------------------- -my $SCRIPT_NAME = "buffers"; -my $SCRIPT_VERSION = "5.4"; - -my $BUFFERS_CONFIG_FILE_NAME = "buffers"; -my $buffers_config_file; -my $cmd_buffers_whitelist= "buffers_whitelist"; -my $cmd_buffers_detach = "buffers_detach"; - -my $maxlength; - -my %mouse_keys = ("\@item(buffers):button1*" => "hsignal:buffers_mouse", - "\@item(buffers):button2*" => "hsignal:buffers_mouse", - "\@bar(buffers):ctrl-wheelup" => "hsignal:buffers_mouse", - "\@bar(buffers):ctrl-wheeldown" => "hsignal:buffers_mouse"); -my %options; -my %hotlist_level = (0 => "low", 1 => "message", 2 => "private", 3 => "highlight"); -my @whitelist_buffers = (); -my @immune_detach_buffers= (); -my @detach_buffer_immediately= (); -my @buffers_focus = (); -my %buffers_timer = (); -my %Hooks = (); - -# --------------------------------[ init ]-------------------------------------- -weechat::register($SCRIPT_NAME, "Sebastien Helleu ", - $SCRIPT_VERSION, "GPL3", - "Sidebar with list of buffers", "shutdown_cb", ""); -my $weechat_version = weechat::info_get("version_number", "") || 0; - -buffers_config_init(); -buffers_config_read(); - -weechat::bar_item_new($SCRIPT_NAME, "build_buffers", ""); -weechat::bar_new($SCRIPT_NAME, "0", "0", "root", "", "left", "columns_vertical", - "vertical", "0", "0", "default", "default", "default", "1", - $SCRIPT_NAME); - -if ( check_bar_item() == 0 ) -{ - weechat::command("", "/bar show " . $SCRIPT_NAME) if ( weechat::config_boolean($options{"toggle_bar"}) eq 1 ); -} - -weechat::hook_signal("buffer_opened", "buffers_signal_buffer", ""); -weechat::hook_signal("buffer_closed", "buffers_signal_buffer", ""); -weechat::hook_signal("buffer_merged", "buffers_signal_buffer", ""); -weechat::hook_signal("buffer_unmerged", "buffers_signal_buffer", ""); -weechat::hook_signal("buffer_moved", "buffers_signal_buffer", ""); -weechat::hook_signal("buffer_renamed", "buffers_signal_buffer", ""); -weechat::hook_signal("buffer_switch", "buffers_signal_buffer", ""); -weechat::hook_signal("buffer_hidden", "buffers_signal_buffer", ""); # WeeChat >= 0.4.4 -weechat::hook_signal("buffer_unhidden", "buffers_signal_buffer", ""); # WeeChat >= 0.4.4 -weechat::hook_signal("buffer_localvar_added", "buffers_signal_buffer", ""); -weechat::hook_signal("buffer_localvar_changed", "buffers_signal_buffer", ""); - -weechat::hook_signal("window_switch", "buffers_signal_buffer", ""); -weechat::hook_signal("hotlist_changed", "buffers_signal_hotlist", ""); -#weechat::hook_command_run("/input switch_active_*", "buffers_signal_buffer", ""); -weechat::bar_item_update($SCRIPT_NAME); - - -if ($weechat_version >= 0x00030600) -{ - weechat::hook_focus($SCRIPT_NAME, "buffers_focus_buffers", ""); - weechat::hook_hsignal("buffers_mouse", "buffers_hsignal_mouse", ""); - weechat::key_bind("mouse", \%mouse_keys); -} - -weechat::hook_command($cmd_buffers_whitelist, - "add/del current buffer to/from buffers whitelist", - "[add] || [del] || [reset]", - " add: add current buffer in configuration file\n". - " del: delete current buffer from configuration file\n". - "reset: reset all buffers from configuration file ". - "(no confirmation!)\n\n". - "Examples:\n". - "/$cmd_buffers_whitelist add\n", - "add %-||". - "del %-||". - "reset %-", - "buffers_cmd_whitelist", ""); -weechat::hook_command($cmd_buffers_detach, - "add/del current buffer to/from buffers detach", - "[add] || [del] || [reset]", - " add: add current buffer in configuration file\n". - " del: delete current buffer from configuration file\n". - "reset: reset all buffers from configuration file ". - "(no confirmation!)\n\n". - "Examples:\n". - "/$cmd_buffers_detach add\n", - "add %-||". - "del %-||". - "reset %-", - "buffers_cmd_detach", ""); - -if ($weechat_version >= 0x00030800) -{ - weechat::hook_config("buffers.look.detach", "hook_timer_detach", ""); - if (weechat::config_integer($options{"detach"}) > 0) - { - $Hooks{timer_detach} = weechat::hook_timer(weechat::config_integer($options{"detach"}) * 1000, - 60, 0, "buffers_signal_hotlist", ""); - } -} - -weechat::hook_config("buffers.look.show_lag", "hook_timer_lag", ""); - -if (weechat::config_boolean($options{"show_lag"})) -{ - $Hooks{timer_lag} = weechat::hook_timer( - weechat::config_integer(weechat::config_get("irc.network.lag_refresh_interval")) * 1000, - 0, 0, "buffers_signal_hotlist", ""); -} - -# -------------------------------- [ command ] -------------------------------- -sub buffers_cmd_whitelist -{ -my ( $data, $buffer, $args ) = @_; - $args = lc($args); - my $buffers_whitelist = weechat::config_string( weechat::config_get("buffers.look.whitelist_buffers") ); - return weechat::WEECHAT_RC_OK if ( $buffers_whitelist eq "" and $args eq "del" or $buffers_whitelist eq "" and $args eq "reset" ); - my @buffers_list = split( /,/, $buffers_whitelist ); - # get buffers name - my $infolist = weechat::infolist_get("buffer", weechat::current_buffer(), ""); - weechat::infolist_next($infolist); - my $buffers_name = weechat::infolist_string($infolist, "name"); - weechat::infolist_free($infolist); - return weechat::WEECHAT_RC_OK if ( $buffers_name eq "" ); # should never happen - - if ( $args eq "add" ) - { - return weechat::WEECHAT_RC_OK if ( grep /^$buffers_name$/, @buffers_list ); # check if buffer already in list - push @buffers_list, ( $buffers_name ); - my $buffers_list = &create_whitelist(\@buffers_list); - weechat::config_option_set( weechat::config_get("buffers.look.whitelist_buffers"), $buffers_list, 1); - weechat::print(weechat::current_buffer(), "buffer \"$buffers_name\" added to buffers whitelist"); - } - elsif ( $args eq "del" ) - { - return weechat::WEECHAT_RC_OK unless ( grep /^$buffers_name$/, @buffers_list ); # check if buffer is in list - @buffers_list = grep {$_ ne $buffers_name} @buffers_list; # delete entry - my $buffers_list = &create_whitelist(\@buffers_list); - weechat::config_option_set( weechat::config_get("buffers.look.whitelist_buffers"), $buffers_list, 1); - weechat::print(weechat::current_buffer(), "buffer \"$buffers_name\" deleted from buffers whitelist"); - } - elsif ( $args eq "reset" ) - { - return weechat::WEECHAT_RC_OK if ( $buffers_whitelist eq "" ); - weechat::config_option_set( weechat::config_get("buffers.look.whitelist_buffers"), "", 1); - weechat::print(weechat::current_buffer(), "buffers whitelist is empty, now..."); - } - return weechat::WEECHAT_RC_OK; -} -sub buffers_cmd_detach -{ - my ( $data, $buffer, $args ) = @_; - $args = lc($args); - my $immune_detach_buffers = weechat::config_string( weechat::config_get("buffers.look.immune_detach_buffers") ); - return weechat::WEECHAT_RC_OK if ( $immune_detach_buffers eq "" and $args eq "del" or $immune_detach_buffers eq "" and $args eq "reset" ); - - my @buffers_list = split( /,/, $immune_detach_buffers ); - # get buffers name - my $infolist = weechat::infolist_get("buffer", weechat::current_buffer(), ""); - weechat::infolist_next($infolist); - my $buffers_name = weechat::infolist_string($infolist, "name"); - weechat::infolist_free($infolist); - return weechat::WEECHAT_RC_OK if ( $buffers_name eq "" ); # should never happen - - if ( $args eq "add" ) - { - return weechat::WEECHAT_RC_OK if ( grep /^$buffers_name$/, @buffers_list ); # check if buffer already in list - push @buffers_list, ( $buffers_name ); - my $buffers_list = &create_whitelist(\@buffers_list); - weechat::config_option_set( weechat::config_get("buffers.look.immune_detach_buffers"), $buffers_list, 1); - weechat::print(weechat::current_buffer(), "buffer \"$buffers_name\" added to immune detach buffers"); - } - elsif ( $args eq "del" ) - { - return weechat::WEECHAT_RC_OK unless ( grep /^$buffers_name$/, @buffers_list ); # check if buffer is in list - @buffers_list = grep {$_ ne $buffers_name} @buffers_list; # delete entry - my $buffers_list = &create_whitelist(\@buffers_list); - weechat::config_option_set( weechat::config_get("buffers.look.immune_detach_buffers"), $buffers_list, 1); - weechat::print(weechat::current_buffer(), "buffer \"$buffers_name\" deleted from immune detach buffers"); - } - elsif ( $args eq "reset" ) - { - return weechat::WEECHAT_RC_OK if ( $immune_detach_buffers eq "" ); - weechat::config_option_set( weechat::config_get("buffers.look.immune_detach_buffers"), "", 1); - weechat::print(weechat::current_buffer(), "immune detach buffers is empty, now..."); - } - return weechat::WEECHAT_RC_OK; -} - -sub create_whitelist -{ - my @buffers_list = @{$_[0]}; - my $buffers_list = ""; - foreach (@buffers_list) - { - $buffers_list .= $_ .","; - } - # remove last "," - chop $buffers_list; - return $buffers_list; -} - -# -------------------------------- [ config ] -------------------------------- -sub hook_timer_detach -{ - my $detach = $_[2]; - if ( $detach eq 0 ) - { - weechat::unhook($Hooks{timer_detach}) if $Hooks{timer_detach}; - $Hooks{timer_detach} = ""; - } - else - { - weechat::unhook($Hooks{timer_detach}) if $Hooks{timer_detach}; - $Hooks{timer_detach} = weechat::hook_timer( weechat::config_integer( $options{"detach"}) * 1000, 60, 0, "buffers_signal_hotlist", ""); - } - weechat::bar_item_update($SCRIPT_NAME); - return weechat::WEECHAT_RC_OK; -} - -sub hook_timer_lag -{ - my $lag = $_[2]; - if ( $lag eq "off" ) - { - weechat::unhook($Hooks{timer_lag}) if $Hooks{timer_lag}; - $Hooks{timer_lag} = ""; - } - else - { - weechat::unhook($Hooks{timer_lag}) if $Hooks{timer_lag}; - $Hooks{timer_lag} = weechat::hook_timer( weechat::config_integer(weechat::config_get("irc.network.lag_refresh_interval")) * 1000, 0, 0, "buffers_signal_hotlist", ""); - } - weechat::bar_item_update($SCRIPT_NAME); - return weechat::WEECHAT_RC_OK; -} - -sub buffers_config_read -{ - return weechat::config_read($buffers_config_file) if ($buffers_config_file ne ""); -} -sub buffers_config_write -{ - return weechat::config_write($buffers_config_file) if ($buffers_config_file ne ""); -} -sub buffers_config_reload_cb -{ - my ($data, $config_file) = ($_[0], $_[1]); - return weechat::config_reload($config_file) -} -sub buffers_config_init -{ - $buffers_config_file = weechat::config_new($BUFFERS_CONFIG_FILE_NAME, - "buffers_config_reload_cb", ""); - return if ($buffers_config_file eq ""); - -my %default_options_color = -("color_current_fg" => [ - "current_fg", "color", - "foreground color for current buffer", - "", 0, 0, "lightcyan", "lightcyan", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_current_bg" => [ - "current_bg", "color", - "background color for current buffer", - "", 0, 0, "red", "red", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_default_fg" => [ - "default_fg", "color", - "default foreground color for buffer name", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_default_bg" => [ - "default_bg", "color", - "default background color for buffer name", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_hotlist_highlight_fg" => [ - "hotlist_highlight_fg", "color", - "change foreground color of buffer name if a highlight messaged received", - "", 0, 0, "magenta", "magenta", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_hotlist_highlight_bg" => [ - "hotlist_highlight_bg", "color", - "change background color of buffer name if a highlight messaged received", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_hotlist_low_fg" => [ - "hotlist_low_fg", "color", - "change foreground color of buffer name if a low message received", - "", 0, 0, "white", "white", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_hotlist_low_bg" => [ - "hotlist_low_bg", "color", - "change background color of buffer name if a low message received", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_hotlist_message_fg" => [ - "hotlist_message_fg", "color", - "change foreground color of buffer name if a normal message received", - "", 0, 0, "yellow", "yellow", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_hotlist_message_bg" => [ - "hotlist_message_bg", "color", - "change background color of buffer name if a normal message received", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_hotlist_private_fg" => [ - "hotlist_private_fg", "color", - "change foreground color of buffer name if a private message received", - "", 0, 0, "lightgreen", "lightgreen", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_hotlist_private_bg" => [ - "hotlist_private_bg", "color", - "change background color of buffer name if a private message received", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_number" => [ - "number", "color", - "color for buffer number", - "", 0, 0, "lightgreen", "lightgreen", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_number_char" => [ - "number_char", "color", - "color for buffer number char", - "", 0, 0, "lightgreen", "lightgreen", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_whitelist_default_fg" => [ - "whitelist_default_fg", "color", - "default foreground color for whitelist buffer name", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_whitelist_default_bg" => [ - "whitelist_default_bg", "color", - "default background color for whitelist buffer name", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_whitelist_low_fg" => [ - "whitelist_low_fg", "color", - "low color of whitelist buffer name", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_whitelist_low_bg" => [ - "whitelist_low_bg", "color", - "low color of whitelist buffer name", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_whitelist_message_fg" => [ - "whitelist_message_fg", "color", - "message color of whitelist buffer name", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_whitelist_message_bg" => [ - "whitelist_message_bg", "color", - "message color of whitelist buffer name", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_whitelist_private_fg" => [ - "whitelist_private_fg", "color", - "private color of whitelist buffer name", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_whitelist_private_bg" => [ - "whitelist_private_bg", "color", - "private color of whitelist buffer name", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_whitelist_highlight_fg" => [ - "whitelist_highlight_fg", "color", - "highlight color of whitelist buffer name", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_whitelist_highlight_bg" => [ - "whitelist_highlight_bg", "color", - "highlight color of whitelist buffer name", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_none_channel_fg" => [ - "none_channel_fg", "color", - "foreground color for none channel buffer (e.g.: core/server/plugin ". - "buffer)", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_none_channel_bg" => [ - "none_channel_bg", "color", - "background color for none channel buffer (e.g.: core/server/plugin ". - "buffer)", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "queries_default_fg" => [ - "queries_default_fg", "color", - "foreground color for query buffer without message", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "queries_default_bg" => [ - "queries_default_bg", "color", - "background color for query buffer without message", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "queries_message_fg" => [ - "queries_message_fg", "color", - "foreground color for query buffer with unread message", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "queries_message_bg" => [ - "queries_message_bg", "color", - "background color for query buffer with unread message", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "queries_highlight_fg" => [ - "queries_highlight_fg", "color", - "foreground color for query buffer with unread highlight", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "queries_highlight_bg" => [ - "queries_highlight_bg", "color", - "background color for query buffer with unread highlight", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_prefix_bufname" => [ - "prefix_bufname", "color", - "color for prefix of buffer name", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "color_suffix_bufname" => [ - "suffix_bufname", "color", - "color for suffix of buffer name", - "", 0, 0, "default", "default", 0, - "", "", "buffers_signal_config", "", "", "" - ], -); - -my %default_options_look = -( - "hotlist_counter" => [ - "hotlist_counter", "boolean", - "show number of message for the buffer (this option needs WeeChat >= ". - "0.3.5). The relevant option for notification is \"weechat.look.". - "buffer_notify_default\"", - "", 0, 0, "off", "off", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "show_lag" => [ - "show_lag", "boolean", - "show lag behind server name. This option is using \"irc.color.". - "item_lag_finished\", ". - "\"irc.network.lag_min_show\" and \"irc.network.lag_refresh_interval\"", - "", 0, 0, "off", "off", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "look_whitelist_buffers" => [ - "whitelist_buffers", "string", - "comma separated list of buffers for using a different color scheme ". - "(for example: freenode.#weechat,freenode.#weechat-fr)", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config_whitelist", "", "", "" - ], - "hide_merged_buffers" => [ - "hide_merged_buffers", "integer", - "hide merged buffers. The value determines which merged buffers should ". - "be hidden, keepserver meaning 'all except server buffers'. Other values ". - "correspondent to the buffer type.", - "server|channel|private|keepserver|all|none", 0, 0, "none", "none", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "indenting" => [ - "indenting", "integer", "use indenting for channel and query buffers. ". - "This option only takes effect if bar is left/right positioned", - "off|on|under_name", 0, 0, "off", "off", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "indenting_number" => [ - "indenting_number", "boolean", - "use indenting for numbers. This option only takes effect if bar is ". - "left/right positioned", - "", 0, 0, "on", "on", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "indenting_amount" => [ - "indenting_amount", "integer", - "amount of indenting to use. This option only takes effect if bar ". - "is left/right positioned, and indenting is enabled", - "", 0, 16, 2, 2, 0, - "", "", "buffers_signal_config", "", "", "" - ], - "short_names" => [ - "short_names", "boolean", - "display short names (remove text before first \".\" in buffer name)", - "", 0, 0, "on", "on", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "show_number" => [ - "show_number", "boolean", - "display buffer number in front of buffer name", - "", 0, 0, "on", "on", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "show_number_char" => [ - "number_char", "string", - "display a char behind buffer number", - "", 0, 0, ".", ".", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "show_prefix_bufname" => [ - "prefix_bufname", "string", - "prefix displayed in front of buffer name", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "show_suffix_bufname" => [ - "suffix_bufname", "string", - "suffix displayed at end of buffer name", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "show_prefix" => [ - "prefix", "boolean", - "displays your prefix for channel in front of buffer name", - "", 0, 0, "off", "off", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "show_prefix_empty" => [ - "prefix_empty", "boolean", - "use a placeholder for channels without prefix", - "", 0, 0, "on", "on", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "show_prefix_query" => [ - "prefix_for_query", "string", - "prefix displayed in front of query buffer", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "sort" => [ - "sort", "integer", - "sort buffer-list by \"number\" or \"name\"", - "number|name", 0, 0, "number", "number", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "core_to_front" => [ - "core_to_front", "boolean", - "core buffer and buffers with free content will be listed first. ". - "Take only effect if buffer sort is by name", - "", 0, 0, "off", "off", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "jump_prev_next_visited_buffer" => [ - "jump_prev_next_visited_buffer", "boolean", - "jump to previously or next visited buffer if you click with ". - "left/right mouse button on currently visiting buffer", - "", 0, 0, "off", "off", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "name_size_max" => [ - "name_size_max", "integer", - "maximum size of buffer name. 0 means no limitation", - "", 0, 256, 0, 0, 0, - "", "", "buffers_signal_config", "", "", "" - ], - "name_crop_suffix" => [ - "name_crop_suffix", "string", - "contains an optional char(s) that is appended when buffer name is ". - "shortened", - "", 0, 0, "+", "+", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "detach" => [ - "detach", "integer", - "detach buffer from buffers list after a specific period of time ". - "(in seconds) without action (weechat ≥ 0.3.8 required) (0 means \"off\")", - "", 0, 31536000, 0, "number", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "immune_detach_buffers" => [ - "immune_detach_buffers", "string", - "comma separated list of buffers to NOT automatically detach. ". - "Allows \"*\" wildcard. Ex: \"BitlBee,freenode.*\"", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config_immune_detach_buffers", "", "", "" - ], - "detach_query" => [ - "detach_query", "boolean", - "query buffer will be detached", - "", 0, 0, "off", "off", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "detach_buffer_immediately" => [ - "detach_buffer_immediately", "string", - "comma separated list of buffers to detach immediately. Buffers ". - "will attach again based on notify level set in ". - "\"detach_buffer_immediately_level\". Allows \"*\" wildcard. ". - "Ex: \"BitlBee,freenode.*\"", - "", 0, 0, "", "", 0, - "", "", "buffers_signal_config_detach_buffer_immediately", "", "", "" - ], - "detach_buffer_immediately_level" => [ - "detach_buffer_immediately_level", "integer", - "The value determines what notify level messages are reattached from activity. ". - " This option works in conjunction with \"detach_buffer_immediately\" ". - "0: low priority (like join/part messages), ". - "1: message, ". - "2: private, ". - "3: highlight", - "", 0, 3, 2, 2, 0, - "", "", "buffers_signal_config", "", "", "" - ], - "detach_free_content" => [ - "detach_free_content", "boolean", - "buffers with free content will be detached (Ex: iset, chanmon)", - "", 0, 0, "off", "off", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "detach_displayed_buffers" => [ - "detach_displayed_buffers", "boolean", - "buffers displayed in a (split) window will be detached", - "", 0, 0, "on", "on", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "detach_display_window_number" => [ - "detach_display_window_number", "boolean", - "window number will be add, behind buffer name (this option takes only ". - "effect with \"detach_displayed_buffers\" option)", - "", 0, 0, "off", "off", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "mark_inactive" => [ - "mark_inactive", "boolean", - "if option is \"on\", inactive buffers (those you are not in) will have ". - "parentheses around them. An inactive buffer will not be detached.", - "", 0, 0, "off", "off", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "toggle_bar" => [ - "toggle_bar", "boolean", - "if option is \"on\", buffers bar will hide/show when script is ". - "(un)loaded.", - "", 0, 0, "on", "on", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "mouse_move_buffer" => [ - "mouse_move_buffer", "boolean", - "if option is \"on\", mouse gestures (drag & drop) can move buffers in list.", - "", 0, 0, "on", "on", 0, - "", "", "buffers_signal_config", "", "", "" - ], - "mouse_wheel" => [ - "mouse_wheel", "boolean", - "if option is \"on\", mouse wheel jumps to previous/next buffer in list.", - "", 0, 0, "on", "on", 0, - "", "", "buffers_signal_config", "", "", "" - ], -); - # section "color" - my $section_color = weechat::config_new_section( - $buffers_config_file, - "color", 0, 0, "", "", "", "", "", "", "", "", "", ""); - if ($section_color eq "") - { - weechat::config_free($buffers_config_file); - return; - } - foreach my $option (keys %default_options_color) - { - $options{$option} = weechat::config_new_option( - $buffers_config_file, - $section_color, - $default_options_color{$option}[0], - $default_options_color{$option}[1], - $default_options_color{$option}[2], - $default_options_color{$option}[3], - $default_options_color{$option}[4], - $default_options_color{$option}[5], - $default_options_color{$option}[6], - $default_options_color{$option}[7], - $default_options_color{$option}[8], - $default_options_color{$option}[9], - $default_options_color{$option}[10], - $default_options_color{$option}[11], - $default_options_color{$option}[12], - $default_options_color{$option}[13], - $default_options_color{$option}[14]); - } - - # section "look" - my $section_look = weechat::config_new_section( - $buffers_config_file, - "look", 0, 0, "", "", "", "", "", "", "", "", "", ""); - if ($section_look eq "") - { - weechat::config_free($buffers_config_file); - return; - } - foreach my $option (keys %default_options_look) - { - $options{$option} = weechat::config_new_option( - $buffers_config_file, - $section_look, - $default_options_look{$option}[0], - $default_options_look{$option}[1], - $default_options_look{$option}[2], - $default_options_look{$option}[3], - $default_options_look{$option}[4], - $default_options_look{$option}[5], - $default_options_look{$option}[6], - $default_options_look{$option}[7], - $default_options_look{$option}[8], - $default_options_look{$option}[9], - $default_options_look{$option}[10], - $default_options_look{$option}[11], - $default_options_look{$option}[12], - $default_options_look{$option}[13], - $default_options_look{$option}[14], - $default_options_look{$option}[15]); - } -} - -sub build_buffers -{ - my $str = ""; - - # get bar position (left/right/top/bottom) - my $position = "left"; - my $option_position = weechat::config_get("weechat.bar.buffers.position"); - if ($option_position ne "") - { - $position = weechat::config_string($option_position); - } - - # read hotlist - my %hotlist; - my $infolist = weechat::infolist_get("hotlist", "", ""); - while (weechat::infolist_next($infolist)) - { - $hotlist{weechat::infolist_pointer($infolist, "buffer_pointer")} = - weechat::infolist_integer($infolist, "priority"); - if ( weechat::config_boolean( $options{"hotlist_counter"} ) eq 1 and $weechat_version >= 0x00030500) - { - $hotlist{weechat::infolist_pointer($infolist, "buffer_pointer")."_count_00"} = - weechat::infolist_integer($infolist, "count_00"); # low message - $hotlist{weechat::infolist_pointer($infolist, "buffer_pointer")."_count_01"} = - weechat::infolist_integer($infolist, "count_01"); # channel message - $hotlist{weechat::infolist_pointer($infolist, "buffer_pointer")."_count_02"} = - weechat::infolist_integer($infolist, "count_02"); # private message - $hotlist{weechat::infolist_pointer($infolist, "buffer_pointer")."_count_03"} = - weechat::infolist_integer($infolist, "count_03"); # highlight message - } - } - weechat::infolist_free($infolist); - - # read buffers list - @buffers_focus = (); - my @buffers; - my @current1 = (); - my @current2 = (); - my $old_number = -1; - my $max_number = 0; - my $max_number_digits = 0; - my $active_seen = 0; - $infolist = weechat::infolist_get("buffer", "", ""); - while (weechat::infolist_next($infolist)) - { - # ignore hidden buffers (WeeChat >= 0.4.4) - if ($weechat_version >= 0x00040400) - { - next if (weechat::infolist_integer($infolist, "hidden")); - } - my $buffer; - my $number = weechat::infolist_integer($infolist, "number"); - if ($number ne $old_number) - { - @buffers = (@buffers, @current2, @current1); - @current1 = (); - @current2 = (); - $active_seen = 0; - } - if ($number > $max_number) - { - $max_number = $number; - } - $old_number = $number; - my $active = weechat::infolist_integer($infolist, "active"); - if ($active) - { - $active_seen = 1; - } - $buffer->{"pointer"} = weechat::infolist_pointer($infolist, "pointer"); - $buffer->{"number"} = $number; - $buffer->{"active"} = $active; - $buffer->{"current_buffer"} = weechat::infolist_integer($infolist, "current_buffer"); - $buffer->{"num_displayed"} = weechat::infolist_integer($infolist, "num_displayed"); - $buffer->{"plugin_name"} = weechat::infolist_string($infolist, "plugin_name"); - $buffer->{"name"} = weechat::infolist_string($infolist, "name"); - $buffer->{"short_name"} = weechat::infolist_string($infolist, "short_name"); - $buffer->{"full_name"} = $buffer->{"plugin_name"}.".".$buffer->{"name"}; - $buffer->{"type"} = weechat::buffer_get_string($buffer->{"pointer"}, "localvar_type"); - #weechat::print("", $buffer->{"type"}); - - # check if buffer is active (or maybe a /part, /kick channel) - if ($buffer->{"type"} eq "channel" and weechat::config_boolean( $options{"mark_inactive"} ) eq 1) - { - my $server = weechat::buffer_get_string($buffer->{"pointer"}, "localvar_server"); - my $channel = weechat::buffer_get_string($buffer->{"pointer"}, "localvar_channel"); - my $infolist_channel = weechat::infolist_get("irc_channel", "", $server.",".$channel); - if ($infolist_channel) - { - weechat::infolist_next($infolist_channel); - $buffer->{"nicks_count"} = weechat::infolist_integer($infolist_channel, "nicks_count"); - }else - { - $buffer->{"nicks_count"} = 0; - } - weechat::infolist_free($infolist_channel); - } - - my $result = check_immune_detached_buffers($buffer->{"name"}); # checking for wildcard - my $maxlevel = weechat::config_integer($options{"detach_buffer_immediately_level"}); - next if ( check_detach_buffer_immediately($buffer->{"name"}) eq 1 - and $buffer->{"current_buffer"} eq 0 - and ( not exists $hotlist{$buffer->{"pointer"}} or $hotlist{$buffer->{"pointer"}} < $maxlevel) ); # checking for buffer to immediately detach - - unless ($result) - { - my $detach_time = weechat::config_integer( $options{"detach"}); - my $current_time = time(); - # set timer for buffers with no hotlist action - $buffers_timer{$buffer->{"pointer"}} = $current_time - if ( not exists $hotlist{$buffer->{"pointer"}} - and $buffer->{"type"} eq "channel" - and not exists $buffers_timer{$buffer->{"pointer"}} - and $detach_time > 0); - - $buffers_timer{$buffer->{"pointer"}} = $current_time - if (weechat::config_boolean($options{"detach_query"}) eq 1 - and not exists $hotlist{$buffer->{"pointer"}} - and $buffer->{"type"} eq "private" - and not exists $buffers_timer{$buffer->{"pointer"}} - and $detach_time > 0); - - $detach_time = 0 - if (weechat::config_boolean($options{"detach_query"}) eq 0 - and $buffer->{"type"} eq "private"); - - # free content buffer - $buffers_timer{$buffer->{"pointer"}} = $current_time - if (weechat::config_boolean($options{"detach_free_content"}) eq 1 - and not exists $hotlist{$buffer->{"pointer"}} - and $buffer->{"type"} eq "" - and not exists $buffers_timer{$buffer->{"pointer"}} - and $detach_time > 0); - $detach_time = 0 - if (weechat::config_boolean($options{"detach_free_content"}) eq 0 - and $buffer->{"type"} eq ""); - - $detach_time = 0 if (weechat::config_boolean($options{"mark_inactive"}) eq 1 and defined $buffer->{"nicks_count"} and $buffer->{"nicks_count"} == 0); - - # check for detach - unless ( $buffer->{"current_buffer"} eq 0 - and not exists $hotlist{$buffer->{"pointer"}} -# and $buffer->{"type"} eq "channel" - and exists $buffers_timer{$buffer->{"pointer"}} - and $detach_time > 0 - and $weechat_version >= 0x00030800 - and $current_time - $buffers_timer{$buffer->{"pointer"}} >= $detach_time) - { - if ($active_seen) - { - push(@current2, $buffer); - } - else - { - push(@current1, $buffer); - } - } - elsif ( $buffer->{"current_buffer"} eq 0 - and not exists $hotlist{$buffer->{"pointer"}} -# and $buffer->{"type"} eq "channel" - and exists $buffers_timer{$buffer->{"pointer"}} - and $detach_time > 0 - and $weechat_version >= 0x00030800 - and $current_time - $buffers_timer{$buffer->{"pointer"}} >= $detach_time) - { # check for option detach_displayed_buffers and if buffer is displayed in a split window - if ( $buffer->{"num_displayed"} eq 1 - and weechat::config_boolean($options{"detach_displayed_buffers"}) eq 0 ) - { - my $infolist_window = weechat::infolist_get("window", "", ""); - while (weechat::infolist_next($infolist_window)) - { - my $buffer_ptr = weechat::infolist_pointer($infolist_window, "buffer"); - if ($buffer_ptr eq $buffer->{"pointer"}) - { - $buffer->{"window"} = weechat::infolist_integer($infolist_window, "number"); - } - } - weechat::infolist_free($infolist_window); - - push(@current2, $buffer); - } - } - } - else # buffer in "immune_detach_buffers" - { - if ($active_seen) - { - push(@current2, $buffer); - } - else - { - push(@current1, $buffer); - } - } - } # while end - - - if ($max_number >= 1) - { - $max_number_digits = length(int($max_number)); - } - @buffers = (@buffers, @current2, @current1); - weechat::infolist_free($infolist); - - # sort buffers by number, name or shortname - my %sorted_buffers; - if (1) - { - my $number = 0; - for my $buffer (@buffers) - { - my $key; - if (weechat::config_integer( $options{"sort"} ) eq 1) # number = 0; name = 1 - { - my $name = weechat::buffer_get_string($buffer->{"pointer"}, "localvar_custom_name"); - if (not defined $name or $name eq "") { - if (weechat::config_boolean( $options{"short_names"} ) eq 1) { - $name = $buffer->{"short_name"}; - } else { - $name = $buffer->{"name"}; - } - } - if (weechat::config_integer($options{"name_size_max"}) >= 1) - { - $maxlength = weechat::config_integer($options{"name_size_max"}); - if($buffer->{"type"} eq "channel" and weechat::config_boolean( $options{"mark_inactive"} ) eq 1 and $buffer->{"nicks_count"} == 0) - { - $maxlength -= 2; - } - $name = encode("UTF-8", substr(decode("UTF-8", $name), 0, $maxlength)); - } - if ( weechat::config_boolean($options{"core_to_front"}) eq 1) - { - if ( (weechat::buffer_get_string($buffer->{"pointer"}, "localvar_type") ne "channel" ) and ( weechat::buffer_get_string($buffer->{"pointer"}, "localvar_type") ne "private") ) - { - my $type = weechat::buffer_get_string($buffer->{"pointer"}, "localvar_type"); - if ( $type eq "" and $name ne "weechat") - { - $name = " " . $name - }else - { - $name = " " . $name; - } - } - } - $key = sprintf("%s%08d", lc($name), $buffer->{"number"}); - } - else - { - $key = sprintf("%08d", $number); - } - $sorted_buffers{$key} = $buffer; - $number++; - } - } - - # build string with buffers - $old_number = -1; - foreach my $key (sort keys %sorted_buffers) - { - my $buffer = $sorted_buffers{$key}; - - if ( weechat::config_string($options{"hide_merged_buffers"}) eq "server" ) - { - # buffer type "server" or merged with core? - if ( ($buffer->{"type"} eq "server" or $buffer->{"plugin_name"} eq "core") && (! $buffer->{"active"}) ) - { - next; - } - } - if ( weechat::config_string($options{"hide_merged_buffers"}) eq "channel" ) - { - # buffer type "channel" or merged with core? - if ( ($buffer->{"type"} eq "channel" or $buffer->{"plugin_name"} eq "core") && (! $buffer->{"active"}) ) - { - next; - } - } - if ( weechat::config_string($options{"hide_merged_buffers"}) eq "private" ) - { - # buffer type "private" or merged with core? - if ( ($buffer->{"type"} eq "private" or $buffer->{"plugin_name"} eq "core") && (! $buffer->{"active"}) ) - { - next; - } - } - if ( weechat::config_string($options{"hide_merged_buffers"}) eq "keepserver" ) - { - if ( ($buffer->{"type"} ne "server" or $buffer->{"plugin_name"} eq "core") && (! $buffer->{"active"}) ) - { - next; - } - } - if ( weechat::config_string($options{"hide_merged_buffers"}) eq "all" ) - { - if ( ! $buffer->{"active"} ) - { - next; - } - } - - push(@buffers_focus, $buffer); # buffer > buffers_focus, for mouse support - my $color = ""; - my $bg = ""; - - $color = weechat::config_color( $options{"color_default_fg"} ); - $bg = weechat::config_color( $options{"color_default_bg"} ); - - if ( weechat::buffer_get_string($buffer->{"pointer"}, "localvar_type") eq "private" ) - { - if ( (weechat::config_color($options{"queries_default_bg"})) ne "default" || (weechat::config_color($options{"queries_default_fg"})) ne "default" ) - { - $bg = weechat::config_color( $options{"queries_default_bg"} ); - $color = weechat::config_color( $options{"queries_default_fg"} ); - } - } - # check for core and buffer with free content - if ( (weechat::buffer_get_string($buffer->{"pointer"}, "localvar_type") ne "channel" ) and ( weechat::buffer_get_string($buffer->{"pointer"}, "localvar_type") ne "private") ) - { - $color = weechat::config_color( $options{"color_none_channel_fg"} ); - $bg = weechat::config_color( $options{"color_none_channel_bg"} ); - } - # default whitelist buffer? - if (grep {$_ eq $buffer->{"name"}} @whitelist_buffers) - { - $color = weechat::config_color( $options{"color_whitelist_default_fg"} ); - $bg = weechat::config_color( $options{"color_whitelist_default_bg"} ); - } - - $color = "default" if ($color eq ""); - - # color for channel and query buffer - if (exists $hotlist{$buffer->{"pointer"}}) - { - delete $buffers_timer{$buffer->{"pointer"}}; - # check if buffer is in whitelist buffer - if (grep {$_ eq $buffer->{"name"}} @whitelist_buffers) - { - $bg = weechat::config_color( $options{"color_whitelist_".$hotlist_level{$hotlist{$buffer->{"pointer"}}}."_bg"} ); - $color = weechat::config_color( $options{"color_whitelist_".$hotlist_level{$hotlist{$buffer->{"pointer"}}}."_fg"} ); - } - elsif ( weechat::buffer_get_string($buffer->{"pointer"}, "localvar_type") eq "private" ) - { - # queries_default_fg/bg and buffers.color.queries_message_fg/bg - if ( (weechat::config_color($options{"queries_highlight_fg"})) ne "default" || - (weechat::config_color($options{"queries_highlight_bg"})) ne "default" || - (weechat::config_color($options{"queries_message_fg"})) ne "default" || - (weechat::config_color($options{"queries_message_bg"})) ne "default" ) - { - if ( ($hotlist{$buffer->{"pointer"}}) == 2 ) - { - $bg = weechat::config_color( $options{"queries_message_bg"} ); - $color = weechat::config_color( $options{"queries_message_fg"} ); - } - - elsif ( ($hotlist{$buffer->{"pointer"}}) == 3 ) - { - $bg = weechat::config_color( $options{"queries_highlight_bg"} ); - $color = weechat::config_color( $options{"queries_highlight_fg"} ); - } - }else - { - $bg = weechat::config_color( $options{"color_hotlist_".$hotlist_level{$hotlist{$buffer->{"pointer"}}}."_bg"} ); - $color = weechat::config_color( $options{"color_hotlist_".$hotlist_level{$hotlist{$buffer->{"pointer"}}}."_fg"} ); - } - }else - { - $bg = weechat::config_color( $options{"color_hotlist_".$hotlist_level{$hotlist{$buffer->{"pointer"}}}."_bg"} ); - $color = weechat::config_color( $options{"color_hotlist_".$hotlist_level{$hotlist{$buffer->{"pointer"}}}."_fg"} ); - } - } - - if ($buffer->{"current_buffer"}) - { - $color = weechat::config_color( $options{"color_current_fg"} ); - $bg = weechat::config_color( $options{"color_current_bg"} ); - } - my $color_bg = ""; - $color_bg = weechat::color(",".$bg) if ($bg ne ""); - - # create channel number for output - if ( weechat::config_string( $options{"show_prefix_bufname"} ) ne "" ) - { - $str .= $color_bg . - weechat::color( weechat::config_color( $options{"color_prefix_bufname"} ) ). - weechat::config_string( $options{"show_prefix_bufname"} ). - weechat::color("default"); - } - - if ( weechat::config_boolean( $options{"show_number"} ) eq 1 ) # on - { - if (( weechat::config_boolean( $options{"indenting_number"} ) eq 1) - && (($position eq "left") || ($position eq "right"))) - { - $str .= weechat::color("default").$color_bg - .(" " x ($max_number_digits - length(int($buffer->{"number"})))); - } - if ($old_number ne $buffer->{"number"}) - { - $str .= weechat::color( weechat::config_color( $options{"color_number"} ) ) - .$color_bg - .$buffer->{"number"} - .weechat::color("default") - .$color_bg - .weechat::color( weechat::config_color( $options{"color_number_char"} ) ) - .weechat::config_string( $options{"show_number_char"} ) - .$color_bg; - } - else - { - # Indentation aligns channels in a visually appealing way - # when viewing list top-to-bottom... - my $indent = (" " x length($buffer->{"number"}))." "; - # ...except when list is top/bottom and channels left-to-right. - my $option_pos = weechat::config_string( weechat::config_get( "weechat.bar.buffers.position" ) ); - if (($option_pos eq 'top') || ($option_pos eq 'bottom')) { - my $option_filling = weechat::config_string( weechat::config_get( "weechat.bar.buffers.filling_top_bottom" ) ); - if ($option_filling =~ /horizontal/) { - $indent = ''; - } - } - $str .= weechat::color("default") - .$color_bg - .$indent; - } - } - - if (( weechat::config_integer( $options{"indenting"} ) ne 0 ) # indenting NOT off - && (($position eq "left") || ($position eq "right"))) - { - my $type = weechat::buffer_get_string($buffer->{"pointer"}, "localvar_type"); - if (($type eq "channel") || ($type eq "private")) - { - if ( weechat::config_integer( $options{"indenting"} ) eq 1 ) - { - $str .= (" " x weechat::config_integer( $options{"indenting_amount"} ) ); - } - elsif ( (weechat::config_integer($options{"indenting"}) eq 2) and (weechat::config_integer($options{"indenting_number"}) eq 0) ) #under_name - { - if ( weechat::config_boolean( $options{"show_number"} ) eq 0 ) - { - $str .= (" " x weechat::config_integer( $options{"indenting_amount"} ) ); - } - else - { - $str .= ( (" " x ( $max_number_digits - length($buffer->{"number"}) )).(" " x weechat::config_integer( $options{"indenting_amount"} ) ) ); - } - } - } - } - - $str .= weechat::config_string( $options{"show_prefix_query"}) if (weechat::config_string( $options{"show_prefix_query"} ) ne "" and $buffer->{"type"} eq "private"); - - if (weechat::config_boolean( $options{"show_prefix"} ) eq 1) - { - my $nickname = weechat::buffer_get_string($buffer->{"pointer"}, "localvar_nick"); - if ($nickname ne "") - { - # with version >= 0.3.2, this infolist will return only nick - # with older versions, whole nicklist is returned for buffer, and this can be very slow - my $infolist_nick = weechat::infolist_get("nicklist", $buffer->{"pointer"}, "nick_".$nickname); - if ($infolist_nick ne "") - { - while (weechat::infolist_next($infolist_nick)) - { - if ((weechat::infolist_string($infolist_nick, "type") eq "nick") - && (weechat::infolist_string($infolist_nick, "name") eq $nickname)) - { - my $prefix = weechat::infolist_string($infolist_nick, "prefix"); - if (($prefix ne " ") or (weechat::config_boolean( $options{"show_prefix_empty"} ) eq 1)) - { - # with version >= 0.3.5, it is now a color name (for older versions: option name with color) - if (int($weechat_version) >= 0x00030500) - { - $str .= weechat::color(weechat::infolist_string($infolist_nick, "prefix_color")); - } - else - { - $str .= weechat::color(weechat::config_color( - weechat::config_get( - weechat::infolist_string($infolist_nick, "prefix_color")))); - } - $str .= $prefix; - } - last; - } - } - weechat::infolist_free($infolist_nick); - } - } - } - if ($buffer->{"type"} eq "channel" and weechat::config_boolean( $options{"mark_inactive"} ) eq 1 and $buffer->{"nicks_count"} == 0) - { - $str .= "("; - } - - $str .= weechat::color($color) . weechat::color(",".$bg); - - my $name = weechat::buffer_get_string($buffer->{"pointer"}, "localvar_custom_name"); - if (not defined $name or $name eq "") - { - if (weechat::config_boolean( $options{"short_names"} ) eq 1) { - $name = $buffer->{"short_name"}; - } else { - $name = $buffer->{"name"}; - } - } - - if (weechat::config_integer($options{"name_size_max"}) >= 1) # check max_size of buffer name - { - $name = decode("UTF-8", $name); - - $maxlength = weechat::config_integer($options{"name_size_max"}); - if($buffer->{"type"} eq "channel" and weechat::config_boolean( $options{"mark_inactive"} ) eq 1 and $buffer->{"nicks_count"} == 0) - { - $maxlength -= 2; - } - - $str .= encode("UTF-8", substr($name, 0, $maxlength)); - $str .= weechat::color(weechat::config_color( $options{"color_number_char"})).weechat::config_string($options{"name_crop_suffix"}) if (length($name) > weechat::config_integer($options{"name_size_max"})); - $str .= add_inactive_parentless($buffer->{"type"}, $buffer->{"nicks_count"}); - $str .= add_hotlist_count($buffer->{"pointer"}, %hotlist); - } - else - { - $str .= $name; - $str .= add_inactive_parentless($buffer->{"type"}, $buffer->{"nicks_count"}); - $str .= add_hotlist_count($buffer->{"pointer"}, %hotlist); - } - - if ( weechat::buffer_get_string($buffer->{"pointer"}, "localvar_type") eq "server" and weechat::config_boolean($options{"show_lag"}) eq 1) - { - my $color_lag = weechat::config_color(weechat::config_get("irc.color.item_lag_finished")); - my $min_lag = weechat::config_integer(weechat::config_get("irc.network.lag_min_show")); - my $infolist_server = weechat::infolist_get("irc_server", "", $buffer->{"short_name"}); - weechat::infolist_next($infolist_server); - my $lag = (weechat::infolist_integer($infolist_server, "lag")); - weechat::infolist_free($infolist_server); - if ( int($lag) > int($min_lag) ) - { - $lag = $lag / 1000; - $str .= weechat::color("default") . " (" . weechat::color($color_lag) . $lag . weechat::color("default") . ")"; - } - } - if (weechat::config_boolean($options{"detach_displayed_buffers"}) eq 0 - and weechat::config_boolean($options{"detach_display_window_number"}) eq 1) - { - if ($buffer->{"window"}) - { - $str .= weechat::color("default") . " (" . weechat::color(weechat::config_color( $options{"color_number"})) . $buffer->{"window"} . weechat::color("default") . ")"; - } - } - $str .= weechat::color("default"); - - if ( weechat::config_string( $options{"show_suffix_bufname"} ) ne "" ) - { - $str .= weechat::color( weechat::config_color( $options{"color_suffix_bufname"} ) ). - weechat::config_string( $options{"show_suffix_bufname"} ). - weechat::color("default"); - } - - $str .= "\n"; - $old_number = $buffer->{"number"}; - } - - # remove spaces and/or linefeed at the end - $str =~ s/\s+$//; - chomp($str); - return $str; -} - -sub add_inactive_parentless -{ -my ($buf_type, $buf_nicks_count) = @_; -my $str = ""; - if ($buf_type eq "channel" and weechat::config_boolean( $options{"mark_inactive"} ) eq 1 and $buf_nicks_count == 0) - { - $str .= weechat::color(weechat::config_color( $options{"color_number_char"})); - $str .= ")"; - } -return $str; -} - -sub add_hotlist_count -{ -my ($bufpointer, %hotlist) = @_; - -return "" if ( weechat::config_boolean( $options{"hotlist_counter"} ) eq 0 or ($weechat_version < 0x00030500)); # off -my $col_number_char = weechat::color(weechat::config_color( $options{"color_number_char"}) ); -my $str = " ".$col_number_char."("; - -# 0 = low level -if (defined $hotlist{$bufpointer."_count_00"}) -{ - my $bg = weechat::config_color( $options{"color_hotlist_low_bg"} ); - my $color = weechat::config_color( $options{"color_hotlist_low_fg"} ); - $str .= weechat::color($bg). - weechat::color($color). - $hotlist{$bufpointer."_count_00"} if ($hotlist{$bufpointer."_count_00"} ne "0"); -} - -# 1 = message -if (defined $hotlist{$bufpointer."_count_01"}) -{ - my $bg = weechat::config_color( $options{"color_hotlist_message_bg"} ); - my $color = weechat::config_color( $options{"color_hotlist_message_fg"} ); - if ($str =~ /[0-9]$/) - { - $str .= ",". - weechat::color($bg). - weechat::color($color). - $hotlist{$bufpointer."_count_01"} if ($hotlist{$bufpointer."_count_01"} ne "0"); - }else - { - $str .= weechat::color($bg). - weechat::color($color). - $hotlist{$bufpointer."_count_01"} if ($hotlist{$bufpointer."_count_01"} ne "0"); - } -} -# 2 = private -if (defined $hotlist{$bufpointer."_count_02"}) -{ - my $bg = weechat::config_color( $options{"color_hotlist_private_bg"} ); - my $color = weechat::config_color( $options{"color_hotlist_private_fg"} ); - if ($str =~ /[0-9]$/) - { - $str .= ",". - weechat::color($bg). - weechat::color($color). - $hotlist{$bufpointer."_count_02"} if ($hotlist{$bufpointer."_count_02"} ne "0"); - }else - { - $str .= weechat::color($bg). - weechat::color($color). - $hotlist{$bufpointer."_count_02"} if ($hotlist{$bufpointer."_count_02"} ne "0"); - } -} -# 3 = highlight -if (defined $hotlist{$bufpointer."_count_03"}) -{ - my $bg = weechat::config_color( $options{"color_hotlist_highlight_bg"} ); - my $color = weechat::config_color( $options{"color_hotlist_highlight_fg"} ); - if ($str =~ /[0-9]$/) - { - $str .= ",". - weechat::color($bg). - weechat::color($color). - $hotlist{$bufpointer."_count_03"} if ($hotlist{$bufpointer."_count_03"} ne "0"); - }else - { - $str .= weechat::color($bg). - weechat::color($color). - $hotlist{$bufpointer."_count_03"} if ($hotlist{$bufpointer."_count_03"} ne "0"); - } -} -$str .= $col_number_char. ")"; - -$str = "" if (weechat::string_remove_color($str, "") eq " ()"); # remove color and check for buffer with no messages -return $str; -} - -sub buffers_signal_buffer -{ - my ($data, $signal, $signal_data) = @_; - - # check for buffer_switch and set or remove detach time - if ($weechat_version >= 0x00030800) - { - if ($signal eq "buffer_switch") - { - my $pointer = weechat::hdata_get_list (weechat::hdata_get("buffer"), "gui_buffer_last_displayed"); # get switched buffer - my $current_time = time(); - if ( weechat::buffer_get_string($pointer, "localvar_type") eq "channel") - { - $buffers_timer{$pointer} = $current_time; - } - else - { - delete $buffers_timer{$pointer}; - } - } - if ($signal eq "buffer_opened") - { - my $current_time = time(); - $buffers_timer{$signal_data} = $current_time; - } - if ($signal eq "buffer_closing") - { - delete $buffers_timer{$signal_data}; - } - } - weechat::bar_item_update($SCRIPT_NAME); - return weechat::WEECHAT_RC_OK; -} - -sub buffers_signal_hotlist -{ - weechat::bar_item_update($SCRIPT_NAME); - return weechat::WEECHAT_RC_OK; -} - - -sub buffers_signal_config_whitelist -{ - @whitelist_buffers = (); - @whitelist_buffers = split( /,/, weechat::config_string( $options{"look_whitelist_buffers"} ) ); - weechat::bar_item_update($SCRIPT_NAME); - return weechat::WEECHAT_RC_OK; -} - -sub buffers_signal_config_immune_detach_buffers -{ - @immune_detach_buffers = (); - @immune_detach_buffers = split( /,/, weechat::config_string( $options{"immune_detach_buffers"} ) ); - weechat::bar_item_update($SCRIPT_NAME); - return weechat::WEECHAT_RC_OK; -} - -sub buffers_signal_config_detach_buffer_immediately -{ - @detach_buffer_immediately = (); - @detach_buffer_immediately = split( /,/, weechat::config_string( $options{"detach_buffer_immediately"} ) ); - weechat::bar_item_update($SCRIPT_NAME); - return weechat::WEECHAT_RC_OK; -} - -sub buffers_signal_config -{ - weechat::bar_item_update($SCRIPT_NAME); - return weechat::WEECHAT_RC_OK; -} - -# called when mouse click occured in buffers item: this callback returns buffer -# hash according to line of item where click occured -sub buffers_focus_buffers -{ - my %info = %{$_[1]}; - my $item_line = int($info{"_bar_item_line"}); - undef my $hash; - if (($info{"_bar_item_name"} eq $SCRIPT_NAME) && ($item_line >= 0) && ($item_line <= $#buffers_focus)) - { - $hash = $buffers_focus[$item_line]; - } - else - { - $hash = {}; - my $hash_focus = $buffers_focus[0]; - foreach my $key (keys %$hash_focus) - { - $hash->{$key} = "?"; - } - } - return $hash; -} - -# called when a mouse action is done on buffers item, to execute action -# possible actions: jump to a buffer or move buffer in list (drag & drop of buffer) -sub buffers_hsignal_mouse -{ - my ($data, $signal, %hash) = ($_[0], $_[1], %{$_[2]}); - my $current_buffer = weechat::buffer_get_integer(weechat::current_buffer(), "number"); # get current buffer number - - if ( $hash{"_key"} eq "button1" ) - { - # left mouse button - if ($hash{"number"} eq $hash{"number2"}) - { - if ( weechat::config_boolean($options{"jump_prev_next_visited_buffer"}) ) - { - if ( $current_buffer eq $hash{"number"} ) - { - weechat::command("", "/input jump_previously_visited_buffer"); - } - else - { - weechat::command("", "/buffer ".$hash{"full_name"}); - } - } - else - { - weechat::command("", "/buffer ".$hash{"full_name"}); - } - } - else - { - move_buffer(%hash) if (weechat::config_boolean($options{"mouse_move_buffer"})); - } - } - elsif ( ($hash{"_key"} eq "button2") && (weechat::config_boolean($options{"jump_prev_next_visited_buffer"})) ) - { - # right mouse button - if ( $current_buffer eq $hash{"number2"} ) - { - weechat::command("", "/input jump_next_visited_buffer"); - } - } - elsif ( $hash{"_key"} =~ /wheelup$/ ) - { - # wheel up - if (weechat::config_boolean($options{"mouse_wheel"})) - { - weechat::command("", "/buffer -1"); - } - } - elsif ( $hash{"_key"} =~ /wheeldown$/ ) - { - # wheel down - if (weechat::config_boolean($options{"mouse_wheel"})) - { - weechat::command("", "/buffer +1"); - } - } - else - { - my $infolist = weechat::infolist_get("hook", "", "command,menu"); - my $has_menu_command = weechat::infolist_next($infolist); - weechat::infolist_free($infolist); - - if ( $has_menu_command && $hash{"_key"} =~ /button2/ ) - { - if ($hash{"number"} eq $hash{"number2"}) - { - weechat::command($hash{"pointer"}, "/menu buffer1 $hash{short_name} $hash{number}"); - } - else - { - weechat::command($hash{"pointer"}, "/menu buffer2 $hash{short_name}/$hash{short_name2} $hash{number} $hash{number2}") - } - } - else - { - move_buffer(%hash) if (weechat::config_boolean($options{"mouse_move_buffer"})); - } - } -} - -sub move_buffer -{ - my %hash = @_; - my $number2 = $hash{"number2"}; - if ($number2 eq "?") - { - # if number 2 is not known (end of gesture outside buffers list), then set it - # according to mouse gesture - $number2 = "1"; - if (($hash{"_key"} =~ /gesture-right/) || ($hash{"_key"} =~ /gesture-down/)) - { - $number2 = "999999"; - if ($weechat_version >= 0x00030600) - { - my $hdata_buffer = weechat::hdata_get("buffer"); - my $last_gui_buffer = weechat::hdata_get_list($hdata_buffer, "last_gui_buffer"); - if ($last_gui_buffer) - { - $number2 = weechat::hdata_integer($hdata_buffer, $last_gui_buffer, "number") + 1; - } - } - } - } - my $ptrbuf = weechat::current_buffer(); - weechat::command($hash{"pointer"}, "/buffer move ".$number2); -} - -sub check_immune_detached_buffers -{ - my ($buffername) = @_; - foreach ( @immune_detach_buffers ){ - my $immune_buffer = weechat::string_mask_to_regex($_); - if ($buffername =~ /^$immune_buffer$/i) - { - return 1; - } - } - return 0; -} - -sub check_detach_buffer_immediately -{ - my ($buffername) = @_; - foreach ( @detach_buffer_immediately ){ - my $detach_buffer = weechat::string_mask_to_regex($_); - if ($buffername =~ /^$detach_buffer$/i) - { - return 1; - } - } - return 0; -} - -sub shutdown_cb -{ - weechat::command("", "/bar hide " . $SCRIPT_NAME) if ( weechat::config_boolean($options{"toggle_bar"}) eq 1 ); - return weechat::WEECHAT_RC_OK -} - -sub check_bar_item -{ - my $item = 0; - my $infolist = weechat::infolist_get("bar", "", ""); - while (weechat::infolist_next($infolist)) - { - my $bar_items = weechat::infolist_string($infolist, "items"); - if (index($bar_items, $SCRIPT_NAME) != -1) - { - my $name = weechat::infolist_string($infolist, "name"); - if ($name ne $SCRIPT_NAME) - { - $item = 1; - last; - } - } - } - weechat::infolist_free($infolist); - return $item; -} diff --git a/perl/chanmon.pl b/perl/chanmon.pl index dbd7f3a7..6809d116 100644 --- a/perl/chanmon.pl +++ b/perl/chanmon.pl @@ -1,6 +1,5 @@ # # chanmon.pl - Channel Monitoring for weechat 0.3.0 -# Version 2.5 # # Add 'Channel Monitor' buffer/bar that you can position to show IRC channel # messages in a single location without constantly switching buffers @@ -53,6 +52,10 @@ # Changes the amount of lines the output bar will hold. # (Only appears once output has been set to bar, defaults to 10) # +# /set plugins.var.perl.chanmon.tags +# A comma seperated list of tags. chanmon will only work on lines matching one of these tags +# (default: "irc_privmsg,irc_topic") +# # /set plugins.var.perl.chanmon.nick_prefix # /set plugins.var.perl.chanmon.nick_suffix # Sets the prefix and suffix chars in the chanmon buffer @@ -74,6 +77,10 @@ # Bugs and feature requests at: https://github.com/KenjiE20/chanmon # History: +# 2021-04-13, Tobias Rehbein +# v2.7: add configurable list of tags that chanmon should work on +# 2020-06-21, Sebastien Helleu : +# v2.6: make call to bar_new compatible with WeeChat >= 2.9 # 2014-08-16, KenjiE20 : # v2.5: -add: clearbar command to clear bar output # -add: firstrun output prompt to check the help text for set up hints as they were being missed @@ -235,6 +242,10 @@ Changes the amount of lines the output bar will hold. (Only appears once output has been set to bar, defaults to 10) +".weechat::color("bold")."/set plugins.var.perl.chanmon.tags".weechat::color("-bold")." + A comma seperated list of tags. chanmon will only work on lines matching one of these tags + (default: \"irc_privmsg,irc_topic\") + ".weechat::color("bold")."/set plugins.var.perl.chanmon.nick_prefix".weechat::color("-bold")." ".weechat::color("bold")."/set plugins.var.perl.chanmon.nick_suffix".weechat::color("-bold")." Sets the prefix and suffix chars in the chanmon buffer @@ -305,7 +316,15 @@ sub chanmon_bar_open # Make the bar item weechat::bar_item_new("chanmon", "chanmon_bar_build", ""); - $chanmon_bar = weechat::bar_new ("chanmon", "off", 100, "root", "", "bottom", "vertical", "vertical", 0, 0, "default", "cyan", "default", "on", "chanmon"); + my $weechat_version = weechat::info_get("version_number", ""); + if ($weechat_version >= 0x02090000) + { + $chanmon_bar = weechat::bar_new ("chanmon", "off", 100, "root", "", "bottom", "vertical", "vertical", 0, 0, "default", "cyan", "default", "default", "on", "chanmon"); + } + else + { + $chanmon_bar = weechat::bar_new ("chanmon", "off", 100, "root", "", "bottom", "vertical", "vertical", 0, 0, "default", "cyan", "default", "on", "chanmon"); + } return weechat::WEECHAT_RC_OK; } @@ -586,6 +605,12 @@ sub chanmon_config_init weechat::config_set_plugin("merge_private", "off"); } + # Messages to work on + if (!(weechat::config_is_set_plugin ("tags"))) + { + weechat::config_set_plugin("tags", "irc_privmsg,irc_topic"); + } + # Check for exisiting prefix/suffix chars, and setup accordingly $prefix = weechat::config_get("irc.look.nick_prefix"); $prefix = weechat::config_string($prefix); @@ -723,6 +748,9 @@ sub chanmon_new_message my $window_displayed = ""; my $dyncheck = "0"; + my @tags = split(/,/, weechat::config_get_plugin("tags")); + my $tag_matched = 0; + # DEBUG point # $string = "\t"."0: ".$_[0]." 1: ".$_[1]." 2: ".$_[2]." 3: ".$_[3]." 4: ".$_[4]." 5: ".$_[5]." 6: ".$_[6]." 7: ".$_[7]; # weechat::print("", "\t".$string); @@ -736,8 +764,17 @@ sub chanmon_new_message $cb_prefix = $_[6]; $cb_msg = $_[7]; - # Only work on messages and topic notices - if ($cb_tags =~ /irc_privmsg/ || $cb_tags =~ /irc_topic/) + # Only work on specific messages + foreach (@tags) + { + if ($cb_tags =~ $_) + { + $tag_matched = 1; + last; + } + } + + if ($tag_matched) { # Check buffer name is an IRC channel or private message when enabled $bufname = weechat::buffer_get_string($cb_bufferp, 'name'); @@ -1182,7 +1219,7 @@ sub format_buffer_name } # Check result of register, and attempt to behave in a sane manner -if (!weechat::register("chanmon", "KenjiE20", "2.5", "GPL3", "Channel Monitor", "", "")) +if (!weechat::register("chanmon", "KenjiE20", "2.7", "GPL3", "Channel Monitor", "", "")) { # Double load weechat::print ("", "\tChanmon is already loaded"); diff --git a/perl/colorize_lines.pl b/perl/colorize_lines.pl index 78f11017..cee053c2 100644 --- a/perl/colorize_lines.pl +++ b/perl/colorize_lines.pl @@ -1,5 +1,5 @@ # -# Copyright (c) 2010-2015 by Nils Görs +# Copyright (c) 2010-2019 by Nils Görs # Copyleft (ɔ) 2013 by oakkitten # # colors the channel text with nick color and also highlight the whole line @@ -18,32 +18,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# with version 3.0 some options were renamed or have new possible values: -# old: new: -# avail_buffer buffer -# blacklist_channels blacklist_buffers -# highlight new values - -# obsolete options: -# buffer_autoset -# hotlist_max_level_nicks_add -# highlight_regex -# highlight_words -# shuffle -# chat see option highlight - -# i recommend to remove all colorize_lines options, first: -# /script unload colorize_lines -# /unset plugins.var.perl.colorize_lines.* -# /unset plugins.desc.perl.colorize_lines* -# /script load colorize_lines.pl - - -# you can colorize lines with a channel color instead of nick color: -# /buffer set localvar_set_colorize_lines yellow -# /buffer set localvar_set_colorize_lines *yellow - # history: +# 4.0.1: fix display of multiline messages +# 4.0: add compatibility with XDG directories (WeeChat >= 3.2) +# 3.9: add compatibility with new weechat_print modifier data (WeeChat >= 2.9) +# 3.8: new option custom_action_text (https://github.com/weechat/scripts/issues/313) (idea by 3v1n0) +# 3.7: new option "alternate_color" (https://github.com/weechat/scripts/issues/333) (idea by snuffkins) +# 3.6: new option "own_lines_color" (idea by Linkandzelda) +# : add help about "localvar" to option +# 3.5: new options "highlight_words" and "highlight_words_color" (idea by jokrebel) # 3.4: new options "tags" and "ignore_tags" # 3.3: use localvar "colorize_lines" for buffer related color (idea by tomoe-mami) # 3.2: minor logic fix @@ -106,29 +89,39 @@ use strict; my $PRGNAME = "colorize_lines"; -my $VERSION = "3.4"; +my $VERSION = "4.0.1"; my $AUTHOR = "Nils Görs "; my $LICENCE = "GPL3"; my $DESCR = "Colorize users' text in chat area with their nick color, including highlights"; -my %config = ("buffers" => "all", # all, channel, query - "blacklist_buffers" => "", # "a,b,c" - "lines" => "on", - "highlight" => "on", # on, off, nicks - "nicks" => "", # "d,e,f", "/file" - "own_lines" => "on", # on, off, only - "tags" => "irc_privmsg", - "ignore_tags" => "irc_ctcp", +my %config = ("buffers" => "all", # all, channel, query + "blacklist_buffers" => "", # "a,b,c" + "lines" => "on", + "highlight" => "on", # on, off, nicks + "nicks" => "", # "d,e,f", "/file" + "own_lines" => "on", # on, off, only + "own_lines_color" => "", # empty means, use color from option "chat_nick_self" + "tags" => "irc_privmsg", + "ignore_tags" => "irc_ctcp", + "highlight_words" => "off", # on, off + "highlight_words_color" => "black,darkgray", + "alternate_color" => "", + "custom_action_text" => "", ); -my %help_desc = ("buffers" => "Buffer type affected by the script (all/channel/query, default: all)", - "blacklist_buffers" => "Comma-separated list of channels to be ignored (e.g. freenode.#weechat,*.#python)", - "lines" => "Apply nickname color to the lines (off/on/nicks). The latter will limit highlighting to nicknames in option 'nicks'", - "highlight" => "Apply highlight color to the highlighted lines (off/on/nicks). The latter will limit highlighting to nicknames in option 'nicks'", - "nicks" => "Comma-separater list of nicks (e.g. freenode.cat,*.dog) OR file name starting with '/' (e.g. /file.txt). In the latter case, nicknames will get loaded from that file inside weechat folder (e.g. from ~/.weechat/file.txt). Nicknames in file are newline-separated (e.g. freenode.dog\\n*.cat)", - "own_lines" => "Apply nickname color to own lines (off/on/only). The latter turns off all other kinds of coloring altogether", - "tags" => "Comma-separated list of tags to accept (see /debug tags)", - "ignore_tags" => "Comma-separated list of tags to ignore (see /debug tags)", +my %help_desc = ("buffers" => "Buffer type affected by the script (all/channel/query, default: all)", + "blacklist_buffers" => "Comma-separated list of channels to be ignored (e.g. freenode.#weechat,*.#python)", + "lines" => "Apply nickname color to the lines (off/on/nicks). The latter will limit highlighting to nicknames in option 'nicks'. You can use a localvar to color all lines with a given color (eg: /buffer set localvar_set_colorize_lines *yellow). You'll have enable this option to use alternate_color.", + "highlight" => "Apply highlight color to the highlighted lines (off/on/nicks). The latter will limit highlighting to nicknames in option 'nicks'. Options 'weechat.color.chat_highlight' and 'weechat.color.chat_highlight_bg' will be used as colors.", + "nicks" => "Comma-separater list of nicks (e.g. freenode.cat,*.dog) OR file name starting with '/' (e.g. /file.txt). In the latter case, nicknames will get loaded from that file inside weechat config folder. Nicknames in file are newline-separated (e.g. freenode.dog\\n*.cat)", + "own_lines" => "Apply nickname color to own lines (off/on/only). The latter turns off all other kinds of coloring altogether. This option has an higher priority than alternate_color option.", + "own_lines_color" => "this color will be used for own messages. Set an empty value to use weechat.color.chat_nick_self option", + "tags" => "Comma-separated list of tags to accept (see /debug tags)", + "ignore_tags" => "Comma-separated list of tags to ignore (see /debug tags)", + "highlight_words" => "highlight word(s) in text, matching word(s) in weechat.look.highlight", + "highlight_words_color" => "color for highlight word in text (format: fg,bg)", + "alternate_color" => "alternate between two colors for messages (format: fg,bg:fg,bg)", + "custom_action_text" => "customise the text attributes of ACTION message (note: content is evaluated, see /help eval)", ); my @ignore_tags_array; @@ -156,9 +149,21 @@ sub colorize_cb return $string unless (@tags_found); } -# find buffer pointer - $modifier_data =~ m/([^;]*);([^;]*);/; - my $buf_ptr = weechat::buffer_search($1, $2); + # find buffer pointer and tags + my $buf_ptr = ""; + my $tags = ""; + if ($modifier_data =~ /^0x/) + { + # WeeChat >= 2.9 + $modifier_data =~ m/([^;]*);(.*)/; + $buf_ptr = $1; + $tags = $2; + } else { + # WeeChat <= 2.8 + $modifier_data =~ m/([^;]*);([^;]*);(.*)/; + $buf_ptr = weechat::buffer_search($1, $2); + $tags = $3; + } return $string if ($buf_ptr eq ""); # find buffer name, server name @@ -168,30 +173,39 @@ sub colorize_cb my $servername = weechat::buffer_get_string($buf_ptr, "localvar_server"); # find stuff between \t - $string =~ m/^([^\t]*)\t(.*)/; + $string =~ m/^([^\t]*)\t(.*)/s; my $left = $1; my $right = $2; # find nick of the sender # find out if we are doing an action - my $nick = ($modifier_data =~ m/(^|,)nick_([^,]*)/) ? $2 : weechat::string_remove_color($left, ""); - my $action = ($modifier_data =~ m/\birc_action\b/) ? 1 : 0; + my $nick = ($tags =~ m/(^|,)nick_([^,]*)/) ? $2 : weechat::string_remove_color($left, ""); + my $action = ($tags =~ m/\birc_action\b/) ? 1 : 0; ######################################## get color my $color = ""; my $my_nick = weechat::buffer_get_string($buf_ptr, "localvar_nick"); - my $channel_color = weechat::color( get_localvar_colorize_lines($buf_ptr) ); + my $channel_color = weechat::color( get_localvar($buf_ptr,"localvar_colorize_lines") ); + my $alternate_last = get_localvar($buf_ptr,"localvar_colorize_lines_alternate"); + my ($alternate_color1,$alternate_color2) = split(/:/,$config{alternate_color},2) if ( $config{alternate_color} ne ""); + +# weechat::print("","a: $alternate_color1"); +# weechat::print("","b: $alternate_color2"); if ($my_nick eq $nick) { # it's our own line # process only if own_lines is "on" or "only" (i.e. not "off") - return $string if ($config{own_lines} eq "off") && not ($channel_color); + return $string if ($config{own_lines} eq "off") && not ($channel_color) && ( $config{alternate_color} eq "" ); - $color = weechat::color("chat_nick_self"); + $color = weechat::color($config{own_lines_color}); + $color = weechat::color("chat_nick_self") if ($config{own_lines_color} eq ""); $color = $channel_color if ($channel_color) && ($config{own_lines} eq "off"); + $color = get_alternate_color($buf_ptr,$alternate_last,$alternate_color1,$alternate_color2) if ( $config{alternate_color} ne "" ) && + ( $config{own_lines} eq "off" ); + } else { # it's someone else's line # don't process is own_lines are "only" @@ -209,7 +223,7 @@ sub colorize_cb weechat::string_has_highlight_regex($right_nocolor, weechat::config_string(weechat::config_get("weechat.look.highlight_regex"))) || weechat::string_has_highlight_regex($right_nocolor, weechat::buffer_get_string($buf_ptr, "highlight_regex")) )) { - # that's definitely a highlight! get a hilight color + # that's definitely a highlight! get a highlight color # and replace the first occurance of coloring, that'd be nick color $color = weechat::color('chat_highlight'); $right =~ s/\31[^\31 ]+?\Q$nick/$color$nick/ if ($action); @@ -221,12 +235,44 @@ sub colorize_cb ) { $color = weechat::info_get('irc_nick_color', $nick); $color = $channel_color if ($channel_color); + + $color = get_alternate_color($buf_ptr,$alternate_last,$alternate_color1,$alternate_color2) if ( $config{alternate_color} ne ""); } else { # oh well - return $string; + return $string if ($config{highlight_words} ne "on"); } } - + my $right_nocolor = weechat::string_remove_color($right, ""); + if (( + $config{highlight_words} eq "on" + ) && ($my_nick ne $nick) && ( + weechat::string_has_highlight($right_nocolor, weechat::config_string(weechat::config_get("weechat.look.highlight"))) + )) + { + my $high_word_color = weechat::color(weechat::config_get_plugin("highlight_words_color")); + my $reset = weechat::color('reset'); + my @highlight_array = split(/,/,weechat::config_string(weechat::config_get("weechat.look.highlight"))); + my @line_array = split(/ /,$right); + + foreach my $l (@line_array) { + foreach my $h (@highlight_array) { + my $i = $h; + # check word case insensitiv || check if word matches without "(?-i)" at beginning + if ( lc($l) eq lc($h) || (index($h,"(?-i)") != -1 && ($l eq substr($i,5,length($i)-5,""))) ) { + $right =~ s/\Q$l\E/$high_word_color$l$reset/; + # word starts with (?-i) and has a wildcard ? + } elsif ((index($h,"(?-i)") != -1) && (index($h,"*") != -1) ){ + my $i = $h; + my $t = substr($i,5,length($i)-5,""); + my $regex = weechat::string_mask_to_regex($t); + $right =~ s/\Q$l\E/$high_word_color$l$reset/ if ($l =~ /^$regex$/i); # use * without sensitive + }elsif ((index($h,"*") == 0 || index($h,"*") == length($h)-1)){# wildcard at beginning or end ? + my $regex = weechat::string_mask_to_regex($h); + $right =~ s/\Q$l\E/$high_word_color$l$reset/ if ($l =~ /^$regex$/i); + } + } + } + } ######################################## inject colors and go! my $out = ""; @@ -234,6 +280,7 @@ sub colorize_cb # remove the first color reset - after * nick # make other resets reset to our color $right =~ s/\34//; + $color = weechat::string_eval_expression($config{custom_action_text}, {}, {}, {}) if ( $config{custom_action_text} ne ""); $right =~ s/\34/\34$color/g; $out = $left . "\t" . $right . "\34" } else { @@ -245,11 +292,31 @@ sub colorize_cb return $out; } -sub get_localvar_colorize_lines +sub get_localvar +{ + my ( $buf_ptr,$localvar ) = @_; + return weechat::buffer_get_string($buf_ptr, "$localvar"); +} + +sub set_localvar { - my ( $buf_ptr ) = @_; + my ( $buf_ptr,$value ) = @_; + weechat::buffer_set($buf_ptr, "localvar_set_colorize_lines_alternate", "$value"); +} - return weechat::buffer_get_string($buf_ptr, "localvar_colorize_lines"); +sub get_alternate_color +{ + my ( $buf_ptr, $alternate_last,$alternate_color1,$alternate_color2 ) = @_; + my $color; + if (($alternate_last eq "") or ($alternate_last eq "0")) + { + $color = weechat::color($alternate_color1); + set_localvar($buf_ptr,"1"); + } else { + $color = weechat::color($alternate_color2); + set_localvar($buf_ptr,"0"); + } + return $color; } #################################################################################################### config @@ -259,7 +326,8 @@ sub get_localvar_colorize_lines sub nicklist_read { return if (substr($config{nicks}, 0, 1) ne "/"); - my $file = weechat::info_get("weechat_dir", "") . $config{nicks}; + my $options = { "directory" => "config" }; + my $file = weechat::string_eval_path_home("%h" . $config{nicks}, {}, {}, $options); return unless -e $file; my $nili = ""; open (WL, "<", $file) || DEBUG("$file: $!"); diff --git a/perl/colorize_regex.pl b/perl/colorize_regex.pl new file mode 100644 index 00000000..26d30fd9 --- /dev/null +++ b/perl/colorize_regex.pl @@ -0,0 +1,1378 @@ +# SPDX-FileCopyrightText: 2025 ryoskzypu +# +# SPDX-License-Identifier: MIT-0 +# +# colorize_regex.pl — colorize highlight regex matches in chat messages +# +# Description: +# Colorize regex matches in chat messages from 'weechat.look.highlight_regex' +# option or the 'highlight_regex' buffer property, to make validation of matches +# easier. +# +# Commands: +# /dbgcolor +# Debug weechat's strings color codes. Similar to '/debug unicode', but +# specific to colors, and output is a standard hex dump. Can be used to +# debug other scripts. +# See '/help dbgcolor' for usage. +# +# /rset +# Fast set the script options and regexes. Similar to /fset. +# See '/help rset' for usage. +# +# Bugs: +# https://github.com/ryoskzypu/weechat_scripts +# +# History: +# 2025-03-26, ryoskzypu : +# version 1.0: initial release + +use strict; +use warnings; + +# Debug data structures. +#use Data::Dumper qw< Dumper >; +#$Data::Dumper::Terse = 1; +#$Data::Dumper::Useqq = 1; + +# Global variables + +my %script = ( + prog => 'colorize_regex', + version => '1.0', + author => 'ryoskzypu ', + licence => 'MIT-0', + desc => 'Colorize highlight regex matches in chat messages', +); +my $prog = $script{'prog'}; + +# Config +my %conf; +my $conf_file; + +# Script buffer +my $prog_buff; + +# Return codes +my $ok = weechat::WEECHAT_RC_OK; +my $err = weechat::WEECHAT_RC_ERROR; + +# highlight_regex +my $regex_opt = 'weechat.look.highlight_regex'; # Option +my $re_opt_pat; # Pattern + +# Colors +my $color_match; +my $color_reset = wcolor('reset'); + +# Regexes +my $colors_rgx = qr/ + \o{031} + (?> + \d{2}+ # Fixed 'weechat.color.chat.*' codes + | + (?> # Foreground + [F*] + [*!\/_%.|]?+ + \d{2}+ # IRC colors (00–15) + | + (?> F@ | \*@ ) + [*!\/_%.|]?+ + \d{5}+ # IRC colors (16–99) and WeeChat colors (16–255) + ) + (?> # Background + ~ + (?> \d{2}+ | @\d{5}+) + )?+ + ) + /x; +my $attr_rgx = qr/ + (?> \o{032} | \o{033}) + [\o{001}-\o{006}] + | + \o{031}\o{034} # Reset color and keep attributes + /x; +my $reset_rgx = qr/\o{034}/; +my $split_rgx = qr/ + ($colors_rgx) # Colors + | + ($attr_rgx) # Attributes + | + ($reset_rgx) # Reset all + | + # Bytes + /x; + +# Space hex code. +my $space = "\x{20}"; + +# Utils + +# Get string value of option pointer. +sub wstr +{ + my ($opt) = @_; + return weechat::config_string($opt); +} + +# Get weechat's colors. +sub wcolor +{ + my $code = shift; + my $color = weechat::color($code); + + if ($color eq '') { + chkbuff(1); + wprint('', "wcolor\tfailed to get '${code}' color code"); + } + + return $color; +} + +# Print string on the specific buffer. +sub wprint +{ + my ($buff, $str) = @_; + + # Assign buffer pointer to the script's if conditions met. + if ($buff eq '' && defined $prog_buff) { + $buff = $prog_buff; + } + + return weechat::print($buff, $str); +} + +# Create a dedicated buffer for the script messages. +# Note that all highlights are disabled to avoid duplicated highlights. +sub set_buffer +{ + my $buff_props = { + 'title' => "${prog}.pl — colorize highlight regex matches in chat messages", + 'highlight_disable_regex' => '.+', + }; + my $new_buff = weechat::buffer_new_props($prog, $buff_props, '', '', '', ''); + wprint('', "${prog}\tfailed to create '${prog}' buffer") if $new_buff eq ''; + + return $new_buff; +} + +# Create the script buffer if not opened, then optionally jump to it. +sub chkbuff +{ + my $jump = shift; + + if (weechat::buffer_search('perl', $prog) eq '') { + $prog_buff = set_buffer(); + return if $prog_buff eq ''; + } + + if (defined $jump && $jump == 1) { + if (weechat::command('', "/buffer perl.$prog") == $err) { + wprint('', "chkbuff\tfailed to jump in the buffer"); + return $err; + } + } +} + +# Check the format (od/xxd) that is set, before dumping it. +sub chkdump +{ + my ($buff, $arg, $arg_dump) = @_; + + if ($arg eq 'od') { + sim_od($buff, $arg_dump); + } + elsif ($arg eq 'xxd') { + sim_xxd($buff, $arg_dump); + } +} + +# Debug the colorize_cb() split messages arrays. +sub sdbg +{ + my ($prefix, $var, $array) = @_; + + return if wstr($conf{'debug_mode'}) eq 'off'; + + if (wstr($conf{'debug_mode'}) eq 'on' && defined $array) { + my $count = scalar $array->@*; + + chkbuff(); + + wprint('', "${prefix}\@$var count: ${count}\n"); + wprint('', "\@$var = " . Dumper $array); + + foreach my $i ($array->@*) { + chkdump('', wstr($conf{'debug_fmt'}), $i); + wprint('', ''); + } + } +} + +# Print debug weechat's strings. +sub pdbg +{ + my ($prefix, $var_msg, $hex, $str) = @_; + + return if wstr($conf{'debug_mode'}) eq 'off'; + + if (wstr($conf{'debug_mode'}) eq 'on' && defined $str) { + chkbuff(); + wprint('', "${prefix}${var_msg}:\n '${str}${color_reset}'\n"); + + # Convert message to hex. Useful to decode the string and copy it to some tool. + if ($hex == 1) { + my $hex_str = unpack 'H*', $str; + wprint('', "\$hex_str:\n '${hex_str}'\n"); + } + + chkdump('', wstr($conf{'debug_fmt'}), $str); + } +} + +# Decode IRC colors from string. +sub decode_arg +{ + my ($arg) = @_; + my $decoded = weechat::hook_modifier_exec('irc_color_decode', 1, $arg); + + if ($decoded eq '') { + chkbuff(1); + wprint('', "decode_arg\tfailed to decode '${arg}' argument"); + } + + return $decoded; +} + +# Evaluate weechat's expressions from string. +sub eval_arg +{ + my ($arg) = @_; + my $evaled = weechat::string_eval_expression($arg, {}, {}, {}); + + if ($evaled eq '') { + chkbuff(1); + wprint('', "eval_arg\tfailed to decode '${arg}' argument"); + } + + return $evaled; +} + +# Colorize and/or convert bytes to a dot '.'. +# +# See: +# ascii(7) +# https://github.com/vim/vim/blob/master/src/xxd/xxd.c#L235 +# https://github.com/vim/vim/blob/master/src/xxd/xxd.c#L615 +sub xxd_conv_bytes +{ + my ($type, $byte) = @_; + my $byte_orig = $byte; + my $dot = '.'; + + if ($type eq 'hex') { + $byte = pack 'H*', $byte; # Convert hex byte to a char. + $dot = $byte_orig; # Do not convert the hexes to a dot. + } + + my %colors = ( + 'red' => wcolor('*red'), + 'green' => wcolor('*green'), + 'yellow' => wcolor('*yellow'), + 'blue' => wcolor('*blue'), + 'white' => wcolor('*white'), + ); + + # ASCII printable (7-bit) + if ($byte =~ /\A[\x{20}-\x{7e}]\z/) { + return "$colors{'green'}${byte_orig}$color_reset"; + } + # '\t' (tab), '\n' (newline), '\r' (carriage return) + elsif ($byte =~ /\A[\x{0a}\x{10}\x{0d}]\z/) { + return "$colors{'yellow'}${dot}$color_reset"; + } + # '\0' (null) + elsif ($byte =~/\A\x{00}\z/) { + return "$colors{'white'}${dot}$color_reset"; + } + # 255 (decimal) + elsif ($byte =~/\A\x{ff}\z/) { + return "$colors{'blue'}${dot}$color_reset"; + } + # non-printable ASCII and non-ASCII + else { + return "$colors{'red'}${dot}$color_reset"; + } +} + +# Construct xxd's hexes and bytes rows. +sub xxd_get_rows +{ + my ($len_pad, $arr) = @_; + my $len_arr = scalar $arr->@* - 1; + my $pad = $space; + my @rows; + my $row; + + for (my $i = 0; $i <= $len_arr; $i++) { + $row .= sprintf '%*s', $len_pad, $arr->[$i]; + + # Fold and capture row if at the 16th byte. + if (($i + 1) % 16 == 0) { + if ($len_pad == 0 && $i != $len_arr) { # bytes array + $row .= "\n"; + } else { + $row .= $pad x 2; + } + + push @rows, $row; + $row = ''; + } + # Align hex row to the right if at last index. + elsif ($i == $len_arr) { + if ($len_pad == 9) { # hex array + $row .= sprintf '%*s', (16 - ($i + 1) % 16) * 3 + 2, $pad; + } else { + $row .= "\n"; + } + + push @rows, $row; + } + } + + return @rows; +} + +# Print xxd's table. +sub xxd_print +{ + my ($buff, $hex_rows, $byte_rows) = @_; + my $rows_len = scalar $hex_rows->@* - 1; + my $prefix = "xxd\t"; + my $out; + + for (my $i = 0; $i <= $rows_len; $i++) { + $out .= "$hex_rows->[$i]$byte_rows->[$i]"; + } + + wprint($buff, "${prefix}$out"); +} + +# Simulate 'xxd -g1 -R always' output. +sub sim_xxd +{ + my ($buff, $str) = @_; + my @bytes = split //, $str; + my @hexes = map { unpack 'H*', $_ } @bytes; + my @byte_rows; + my @hex_rows; + + @hexes = map { xxd_conv_bytes('hex', $_) } @hexes; + @bytes = map { xxd_conv_bytes('', $_) } @bytes; + + @hex_rows = xxd_get_rows('9', \@hexes); # 9 length because of the weechat's colors. + @byte_rows = xxd_get_rows('0', \@bytes); + + xxd_print($buff, \@hex_rows, \@byte_rows); +} + +# Convert non-printable chars to their escaped or octal representation. +# See ascii(7). +sub od_conv_chars +{ + my $char = shift; + + # Decimal to escaped chars table + my %esc_chars = ( + '0' => '\0', + '7' => '\a', + '8' => '\b', + '9' => '\t', + '10' => '\n', + '11' => '\v', + '12' => '\f', + '13' => '\r', + ); + + # Escaped + if ($char =~ /\A[\x{00}\x{07}-\x{0d}]\z/) { + return sprintf '%s', $esc_chars{ord $char}; + } + # Non-printable ASCII and non-ASCII + elsif ($char =~ /\A[^\x{20}-\x{7e}]\z/) { + return sprintf "%03o", ord $char; # Octal + } + # Printable ASCII (7-bit) + else { + return $char; + } +} + +# Construct od's hexes and bytes rows. +sub od_get_rows +{ + my ($arr) = @_; + my $len_arr = scalar $arr->@* - 1; + my @rows; + my $row; + + for (my $i = 0; $i <= $len_arr; $i++) { + $row .= sprintf '%4s', $arr->[$i]; + + # Fold and capture row if at the 16th byte or last index. + if (($i + 1) % 16 == 0 || $i == $len_arr) { + $row .= "\n"; + push @rows, $row; + $row = ''; + } + } + + return @rows; +} + +# Print od's table. +sub od_print +{ + my ($buff, $hex_rows, $byte_rows) = @_; + my $rows_len = scalar $hex_rows->@* - 1; + my $prefix = "od\t"; + my $color = wcolor('darkgray'); + my $out; + + for (my $i = 0; $i <= $rows_len; $i++) { + $out .= $hex_rows->[$i]; + + # Colorize escapes. + $out .= $byte_rows->[$i] =~ s/(?> |^)\K03[1-5](?= |$)/${color}${&}$color_reset/gr; + } + + wprint($buff, "${prefix}$out"); +} + +# Simulate 'od -An -tx1c' output. +sub sim_od +{ + my ($buff, $str) = @_; + my @bytes = split //, $str; + my @hexes = map { unpack 'H*', $_ } @bytes; + my @byte_rows; + my @hex_rows; + + @bytes = map { od_conv_chars($_) } @bytes; + + @hex_rows = od_get_rows(\@hexes); + @byte_rows = od_get_rows(\@bytes); + + od_print($buff, \@hex_rows, \@byte_rows); +} + +# Insert command in weechat's input. +sub set_input +{ + my ($is_option, $arg) = @_; + my $command = "/command -s /fset ${arg}; /fset -set"; + my $close = 0; + + # Buffer property + if (! $is_option) { + $arg =~ s/\\/\\$&/g; # Avoid /input interpretation of backslashes. + $command = qq{/input insert /buffer setauto highlight_regex "${arg}"}; + } + + # Option + if ($is_option) { + $close = 1 if weechat::buffer_search('', 'fset') eq ''; + + # /fset commands jump to its buffer, so if current buffer is different, + # jump back to it. + if (weechat::buffer_get_string(weechat::current_buffer(), 'name') ne 'fset') { + $command .= '; /buffer jump last_displayed'; + + # Close the 'fset' buffer if it was not already opened. + $command .= '; /buffer close fset' if $close; + } + } + + # Insert + if (weechat::command('', $command) == $err) { + chkbuff(1); + wprint('', "set_input\tfailed to insert command in weechat's input"); + + return $err; + } +} + +# Format /dbgcolor and /rset commands descriptions. +sub fmt_desc +{ + # dbgcolor + + my $pad = $space x 34; + + my $dbg_fmt = <<~"END"; + [-buffer ] od [-eval|-no] + ${pad}[-buffer ] xxd [-eval|-no] + END + + my $dbg_arg = <<~'END'; + -buffer: show hex dump on this buffer + od: hex dump string in 'od -An -tx1c' format + xxd: " 'xxd -g1 -R always' format + -eval: evaluate string before dumping it (see /help eval) + -no: do not decode IRC colors + + Without argument, 'od' format is used unless '*.debug.fmt' option is set to 'xxd'. + IRC colors are decoded from string unless -eval or -no is set. + Hex dump is shown on script buffer unless -buffer is set. + + Examples: + dump string 'hi <3 WeeChat' in italic and '<3' in bold red (Ctrl = ^C): + /dbgcolor ^Cihi ^Ci^Cb^Cc05<3 ^Cc^CbWeeChat + + dump string 'hello' underlined in xxd format: + /dbgcolor xxd ^C_hello + + dump string 'hello' in bold blue with weechat colors: + /dbgcolor -eval ${color:*blue}hello + END + + # rset + my $hl_arg = <<~"END"; + fmt: ${prog}.debug.fmt + debug: " .debug.mode + fg: " .color.match_fg + bg: " .color.match_bg + filter: " .look.colorize_filter + regex: $regex_opt + prop: weechat.buffer.plugin.server.#channel.highlight_regex + + Without argument, all options and regexes are shown. + + Examples: + insert '/set $regex_opt "regex"' on weechat's input: + /rset regex + + insert '/buffer setauto highlight_regex "regex"': + /rset prop + END + + return $dbg_fmt, $dbg_arg, $hl_arg; +} + +# Callbacks + +# Regex set callback +# +# Quickly check the script options and regexes, and edit by inserting them in the +# weechat's input. +# Note that it depends on the 'fset' plugin. +sub regex_set_cb +{ + my ($data, $buff, $args) = @_; + my $prefix = "rset\t"; + my $is_opt = 1; + + # Options + my $fmt_opt = "${prog}.debug.fmt"; + my $dbg_opt = "${prog}.debug.mode"; + my $fg_opt = "${prog}.color.match_fg"; + my $bg_opt = "${prog}.color.match_bg"; + my $filter_opt = "${prog}.look.colorize_filter"; + + # Values + my $fmt = wstr($conf{'debug_fmt'}); + my $debug = wstr($conf{'debug_mode'}); + my $fg = wstr($conf{'color_match_fg'}); + my $bg = wstr($conf{'color_match_bg'}); + my $filter = wstr($conf{'colorize_filter'}); + + # Buffer property + + my $bufname = weechat::buffer_get_string($buff, 'full_name'); + my $re_prop_pat = weechat::buffer_get_string($buff, 'highlight_regex'); + my $buf_opt = "weechat.buffer.${bufname}.highlight_regex"; + my $is_opt_set = weechat::config_get("$buf_opt"); + my $buf_prop = qq{$buf_opt "${re_prop_pat}"}; + + # The buffer property was set with '/buffer set', meaning it is not saved in + # configuration and will not show in /fset. + # Thus replace /fset command with '/buffer setauto' in set_input(). + if ($is_opt_set eq '') { + $buf_prop = qq{$bufname "${re_prop_pat}"}; + $buf_opt = $re_prop_pat; + $is_opt = 0; + } + + # Just show the options. + if ($args eq '') { + my $opts = <<~"END"; + fmt + $fmt_opt "${fmt}" + + debug + $dbg_opt "${debug}" + + fg + $fg_opt "${fg}" + + bg + $bg_opt "${bg}" + + filter + $filter_opt "${filter}" + + regex + $regex_opt "${re_opt_pat}" + + prop + $buf_prop + END + + chkbuff(1); + wprint('', "${prefix}$opts"); + + return $ok; + } + + # Check if 'fset' plugin is loaded. + if (weechat::info_get('plugin_loaded', 'fset') eq '') { + chkbuff(1); + wprint('', "${prefix}fset plugin is not loaded"); + + return $ok; + } + + # Set options + if ($args eq 'fmt') { + set_input(1, $fmt_opt); + } + elsif ($args eq 'debug') { + set_input(1, $dbg_opt); + } + elsif ($args eq 'fg') { + set_input(1, $fg_opt); + } + elsif ($args eq 'bg') { + set_input(1, $bg_opt); + } + elsif ($args eq 'filter') { + set_input(1, $filter_opt); + } + elsif ($args eq 'regex') { + set_input(1, $regex_opt); + } + # Set regex buffer property. + elsif ($args eq 'prop') { + set_input($is_opt, $buf_opt); + } + else { + chkbuff(1); + wprint('', "${prefix}wrong '${args}' argument"); + + return $err; + } + + return $ok; +} + +# Debug color callback +# +# References: +# https://weechat.org/files/doc/weechat/stable/weechat_user.en.html#colors_support +# https://weechat.org/files/doc/stable/weechat_user.en.html#command_line_colors +# https://weechat.org/files/doc/weechat/stable/weechat_user.en.html#colors +# https://weechat.org/files/doc/stable/weechat_plugin_api.en.html#_color +# https://github.com/weechat/weechat/blob/main/src/gui/gui-color.h +# https://github.com/weechat/weechat/blob/main/src/gui/gui-color.c +# https://github.com/weechat/weechat/blob/main/src/plugins/irc/irc-color.h +# https://github.com/weechat/weechat/blob/main/src/plugins/irc/irc-color.c +# +# Notes: +# - WeeChat supports 256 colors with 32767 color pairs (fg,bg combinations). +# - IRC input color (Ctrl+c+c+color) is limited to 100 colors. +# Also it sets the 'keep attributes' pipe '|' by default and can use RGB hex colors. +# Plain Ctr+c+c resets colors while keepping the attributes. +# It seems it cannot set blink, dim, and emphasis attributes. +# +# - IRC input attributes (Ctrl+c+attr) can be removed when repeated. +# The emphasis attribute overrides the normal colors and only its code resets +# itself, so it should not be used. +# +# - Most of 'weechat.color.chat.*' color codes are fixed (e.g. \03127 is 'chat_nick' +# and \03128 is 'chat_delimiters'), that is, its code never changes when modifying +# the color option. +# Some tags like irc_join/part/quit use these codes. +# +# WeeChat's color codes patterns: +# +# WeeChat color codes sequences start with the \031 escape, followed by a F, +# optional attributes codes (*, !, /, _, %, ., |), and 2 to 5 digits (color codes). +# E.g. +# \031F|00 default color + past attributes +# +# The F is replaced by a * if there is a background color, and a ~ separator +# appears separating the fg,bg colors. +# E.g. +# \031*_08~09 underlined yellow on blue +# +# If an IRC color between 16–98 is inserted with the Ctrl+c+c keys or a RGB hex +# color is inserted with the Ctrl+c+d keys, the colors are prefixed with @. +# +# E.g. +# \031F@|00009 red color (FF0000) +# \031*|01~@00127 IRC 01 color on IRC 50 color +# \031*@|00005~@00014 purple (800080) on cyan (00FFFF) +# +# Attributes: +# \032\001 * bold +# \032\002 ! reverse +# \032\003 / italic +# \032\004 _ underline +# \032\005 % blink +# \032\006 . dim +# | keep attributes +# +# \033\001 remove bold +# \033\002 " reverse +# \033\003 " italic +# \033\004 " underline +# \033\005 " blink +# \033\006 " dim +# +# \031\034 reset color and keep attributes +# \034 reset color and attributes +# +# \031E emphasis +sub debug_color_cb +{ + my ($data, $buffer, $args) = @_; + my $prefix = "dbgcolor\t"; + + if ($args eq '') { + chkbuff(1); + wprint('', "${prefix}missing argument"); + + return $err; + } + + # Parse /dbgcolor arguments by first occurrence. + + my @args = split / /, $args; + my $len_args = scalar @args - 1; + my $buff = ''; + my $args_action; + + for (my $i = 0; $i <= $len_args; $i++) { + # Get buffer. + if ($i == 0 && $args[$i] eq '-buffer') { + # Name + if (defined $args[$i + 1]) { + ++$i; + $buff = weechat::buffer_search ('==', "$args[$i]"); + + if ($buff eq '') { + chkbuff(1); + wprint($buff, "${prefix}failed to get '${args[$i]}' buffer name"); + + return $err; + } + # od/xxd + if (! defined $args[$i + 1]) { + chkbuff(1) if $buff eq ''; + wprint($buff, "${prefix}missing format argument"); + + return $err; + } + + next; + } + else { + chkbuff(1); + wprint($buff, "${prefix}missing buffer argument"); + + return $err; + } + } + # od/xxd + elsif ($args[$i] =~ /\A(?> od | xxd)\z/x) { + my $fmt = $args[$i]; + + # -eval/no + if (defined $args[$i + 1]) { + ++$i; + + if ($args[$i] =~ /\A-(?> (eval) | (no))\z/x) { + if (defined $args[$i + 1]) { + ++$i; + + # Evaluate string. + if (defined $1) { + $args_action = eval_arg("@args[$i .. $len_args]"); + } + # Do not decode IRC colors. + else { + $args_action = "@args[$i .. $len_args]"; + } + } + else { + chkbuff(1) if $buff eq ''; + wprint($buff, "${prefix}missing string argument"); + + return $err; + } + } + # Decode IRC colors. + else { + $args_action = decode_arg("@args[$i .. $len_args]"); + } + + # Run command. + chkbuff(1) if $buff eq ''; + wprint($buff, "${prefix}'${args_action}${color_reset}'"); + chkdump($buff, $fmt, $args_action); + } + else { + chkbuff(1) if $buff eq ''; + wprint($buff, "${prefix}missing string argument"); + + return $err; + } + + last; + } + # Config format (od/xxd) + else { + if ($buff ne '') { + wprint($buff, "${prefix}wrong format argument"); + return $err; + } + + my $args_decode = decode_arg("@args[$i .. $len_args]"); + + # Run command. + chkbuff(1) if $buff eq ''; + wprint($buff, "${prefix}'${args_decode}${color_reset}'"); + chkdump($buff, wstr($conf{'debug_fmt'}), $args_decode); + + last; + } + } + + return $ok; +} + +# Completion callbacks + +sub comp_fmt_cb +{ + my ($data, $comp_item, $buff, $comp) = @_; + + weechat::completion_list_add($comp, 'od', 0, weechat::WEECHAT_LIST_POS_SORT); + weechat::completion_list_add($comp, 'xxd', 0, weechat::WEECHAT_LIST_POS_SORT); + + return $ok +} + +sub comp_action_cb +{ + my ($data, $comp_item, $buff, $comp) = @_; + + weechat::completion_list_add($comp, '-eval', 0, weechat::WEECHAT_LIST_POS_SORT); + weechat::completion_list_add($comp, '-no', 0, weechat::WEECHAT_LIST_POS_SORT); + + return $ok +} + +# Notify callback +# +# Get a message notification (brown) in the script buffer when there is a print +# (only useful in debug mode). +sub notify_cb +{ + my ($data, $hashref) = @_; + + $hashref->{'notify_level'} = 1; # Message + return $hashref; +} + +# Colorize callback +# +# Notes: +# - WeeChat regexes are case insensitive by default, so the script's regexes +# must set the /i modifier. +# Also they can be set to case sensitive with (?-i) at the start of the pattern +# *only*, otherwise the regex will fail. +# +# - WeeChat sets word boundaries by default in 'weechat.look.word_chars_highlight' +# option, so it has to be empty for strings such as -WeeChat- to match correctly +# in regex word boundaries, or substrings i.e. textWeeChat. +# +# Note that editing 'word_chars_highlight' option affects the user's IRC $nick +# mentions in channel/private/server buffers (see irc.look.highlight_{channel,pv,server}). +# +# - To avoid mismatches, the 'highlight_regex' from buffer property has a higher +# priority than the global option. +# +# Testing the 'preserve colors' algorithm: +# 1. Set debug mode to 'on': +# /rset debug +# +# 2. Set highlight_regex option to '\bweechat\b': +# /rset regex +# +# 3. Open another weechat instance, connect it to the same server and send this +# private message to the first instance nick, so it can be highlighted: +# /input insert /msg nick \x0305<\x03043 \x02\x0307WeeChat is awesome\x02 \x0314[0 user] \x0399\x1fhttps://github.com/\x0305w\x0355e\x0384e\x0302c\x0f\x0392h\x0309a\x0338t/weechat/ https\x1f\x16://weechat.org/ +# +# The string is inspired by ##hntop messages and modified to cover some corner +# cases. It should colorize the matches and preserve all colors. +sub colorize_cb +{ + my ($data, $hashref) = @_; + + my $prefix = "colorize_cb\t"; + my $buffer = $hashref->{'buffer'}; + my $filtered = $hashref->{'displayed'}; + my $highlight = $hashref->{'highlight'}; + my $message = $hashref->{'message'}; + my $msg_nocolor; + + # Do not colorize when a message is filtered. + return $hashref if $filtered eq '0' && wstr($conf{'colorize_filter'}) eq 'off'; + + # Remove any color codes from message in order to match and colorize the + # strings correctly. + $msg_nocolor = weechat::string_remove_color($message, ''); + + # Assert that the message string has any match from 'highlight_regex' option. + my $hl_opt = weechat::string_has_highlight_regex($msg_nocolor, $re_opt_pat); + + # Get 'highlight_regex' pattern from buffer property and assert that there is + # a match in the message. + my $re_prop_pat = weechat::buffer_get_string($buffer, 'highlight_regex'); + my $hl_prop = weechat::string_has_highlight_regex($msg_nocolor, $re_prop_pat); + + # Start processing if the message has a highlight. + if ($highlight eq '1') { + # and has a regex match. + return $hashref if ! $hl_opt && ! $hl_prop; + + my $bufname = $hashref->{'buffer_name'}; + my $tags = $hashref->{'tags'}; + my ($nick) = $tags =~ /,nick_([^,]++),/; + my $new_msg; + + my $info = <<~_; + ${prefix}buffer: $bufname + nick: $nick + _ + + # Print buffer and nick information in debug mode. + if (wstr($conf{'debug_mode'}) eq 'on') { + chkbuff(); + wprint('', $info); + } + + # Debug the pre-colorized messages. + pdbg($prefix, '$message', 1, $message); + pdbg($prefix, '$msg_nocolor', 1, $msg_nocolor); + + # Preserve colors + # + # If the line string is already colored, capture every color code before + # the regex match, for restoration after regex colorizing. Otherwise string + # colors after the match are reset. + + # Check if message has any color codes. + if ($message =~ /$colors_rgx | $attr_rgx/x) { + my $color_codes = ''; + my $idx = 0; + my $match = 0; + my $uniq_esc = "\o{035}"; + + # Mark the uncolored message with unique escapes to idenfity the matches + # positions. + $msg_nocolor =~ s/$re_opt_pat/${uniq_esc}${&}$uniq_esc/gi if ! $hl_prop && $hl_opt; + $msg_nocolor =~ s/$re_prop_pat/${uniq_esc}${&}$uniq_esc/gi if $hl_prop; + + # Remove double sequence of unique escapes from sequential matches. + $msg_nocolor =~ s/${uniq_esc}{2}+//g; + pdbg($prefix, '$msg_nocolor', 1, $msg_nocolor); + + # Split all color codes and bytes from the messages. + my @split_msg = grep { defined $_ && $_ ne '' } split /$split_rgx/, $message; + my @split_msg_nc = grep { defined $_ && $_ ne '' } split /$split_rgx/, $msg_nocolor; + + # Debug the split arrays. + #sdbg($prefix, 'split_msg', \@split_msg); + #sdbg($prefix, 'split_msg_nc', \@split_msg_nc); + + # Iterate through the original split array, comparing every byte against + # the uncolored array; while reconstructing the new message with saved + # color codes. + foreach my $i (@split_msg) { + #pdbg($prefix, '$i', 0, $i); + #pdbg($prefix, "\$split_msg_nc[$idx]", 0, $split_msg_nc[$idx]); + #wprint('', ''); + + # It is a color code, so append its codes to be restored. + if ($i =~ /\A(?> $colors_rgx | $attr_rgx)\z/x) { + $color_codes .= $i; + #pdbg($prefix, '$color_codes', 0, $color_codes); + + # Append the codes if not inside a regex match. + $new_msg .= $i unless $match; + + next; + } + # Remove saved codes if a reset code is found. + elsif ($i eq $color_reset) { + $new_msg .= $i unless $match; + $color_codes = ''; + + next; + } + elsif (defined $split_msg_nc[$idx]) { + # It is a char, so compare it against the uncolored's char. + if ($i eq $split_msg_nc[$idx]) { + $new_msg .= $i; + ++$idx; + + next; + } + # If the char is in a regex match and uncolored's is a unique + # escape, restore the saved codes, then advance the index. + elsif ($match && $split_msg_nc[$idx] eq $uniq_esc) { + #pdbg($prefix, "\$split_msg_nc[$idx + 1]", 0, $split_msg_nc[$idx + 1]); + + # If the chars match, advance the index. + if ($split_msg_nc[$idx + 1] eq $i) { + $new_msg .= "${color_reset}${color_codes}${i}"; + $idx += 2; + $match = 0; + + next; + } + } + # It is the start of a colorized regex match (\035), so colorize + # the new msg, then advance uncolored's index to the current char. + elsif ($split_msg_nc[$idx] eq $uniq_esc) { + #pdbg($prefix, "\$split_msg_nc[$idx + 1]", 0, $split_msg_nc[$idx + 1]); + + ++$idx; + $new_msg .= "${color_reset}$color_match" . $split_msg_nc[$idx]; + $match = 1; + + # If the chars match, advance the index. + if ($i eq $split_msg_nc[$idx]) { + ++$idx; + next; + } + } + } + } + } + # Uncolored message, so colorize it normally. + else { + $msg_nocolor =~ s/$re_opt_pat/${color_match}${&}$color_reset/gi if ! $hl_prop && $hl_opt; + $msg_nocolor =~ s/$re_prop_pat/${color_match}${&}$color_reset/gi if $hl_prop; + + $new_msg = $msg_nocolor; + } + + # Debug the colorized message. + pdbg($prefix, '$new_msg', 1, $new_msg); + + # Update the hashtable. + $hashref->{'message'} = $new_msg; + } + + # Debug the hashtable. + #wprint('', "${prefix}\$hashref = " . Dumper $hashref); + + return $hashref; +} + +# Update colors callback +sub upd_colors_cb +{ + my ($data, $option, $value) = @_; + + # Get the option name and update its new value. + my ($prog, $section, $opt) = split /\./, $option; + set_colors() if $opt =~ /\Amatch_[bf]g\z/; + + return $ok; +} + +# Get 'highlight_regex' callback +sub get_regex_cb +{ + $re_opt_pat = wstr(weechat::config_get($regex_opt)); + + if ($re_opt_pat eq '') { + chkbuff(); + wprint('', "get_regex_cb\tfailed to get or empty '${regex_opt}' option"); + return $err; + } + + return $ok; +} + +# Init and configuration + +# Set colors of the regex matches. +# +# Notes: +# - The format is 'foreground,background' and it must be a valid weechat color. +# - 'default' value uses the terminal colors. +sub set_colors +{ + my $fg = wstr($conf{'color_match_fg'}); + my $bg = wstr($conf{'color_match_bg'}); + + $color_match = wcolor("${fg},$bg"); +} + +# Read config file from disk and update the $conf_file pointers. +sub config_read +{ + my $rc = weechat::config_read($conf_file); + + if ($rc != 0) { + if ($rc == weechat::WEECHAT_CONFIG_READ_MEMORY_ERROR) { + wprint('', "${prog}\tnot enough memory to read config file"); + } + elsif ($rc == weechat::WEECHAT_CONFIG_READ_FILE_NOT_FOUND) { + wprint('', "${prog}\tconfig file was not found"); + } + } + + return $rc; +} + +# Handle config errors. +sub chkconf +{ + my ($conf_ptr, $ptr, $type) = @_; + + if ($ptr eq '') { + wprint('', "${prog}\tfailed to create config $type"); + + weechat::config_free($conf_ptr) if $conf_ptr ne ''; + return 1; + } +} + +# Create config file options of a section. +sub set_opts +{ + my ($conf, $sect, $options) = @_; + my $opt; + + foreach my $i ($options->@*) { + $opt = $i->{'option'}; + + $conf{$opt} = weechat::config_new_option( + $conf, + $sect, + $i->{'name'}, + $i->{'opt_type'}, + $i->{'desc'}, + $i->{'str_val'}, + $i->{'min_val'}, + $i->{'max_val'}, + $i->{'default'}, + $i->{'value'}, + $i->{'null_val'}, + '', '', '', '', '', '', + ); + } + + return 1 if chkconf($conf_file, $conf{$opt}, "'${opt}' option"); +} + +sub config_init +{ + $conf_file = weechat::config_new($prog, '', ''); + return 1 if chkconf('', $conf_file, 'file'); + + # Color section + { + my $sect = 'color'; + my $sect_color = weechat::config_new_section($conf_file, $sect, 0, 0, '', '', '', '', '', '', '', '', '', ''); + return 1 if chkconf($conf_file, $sect_color, "'${sect}' section"); + + # Options + my @opt = ( + { + 'option' => 'color_match_fg', + 'name' => 'match_fg', + 'opt_type' => 'color', + 'desc' => 'foreground WeeChat color that colorizes the regex matches', + 'str_val' => '', + 'min_val' => 0, + 'max_val' => 0, + 'default' => 'black', + 'value' => 'black', + 'null_val' => 0, + }, + { + 'option' => 'color_match_bg', + 'name' => 'match_bg', + 'opt_type' => 'color', + 'desc' => 'background WeeChat color that colorizes the regex matches', + 'str_val' => '', + 'min_val' => 0, + 'max_val' => 0, + 'default' => '153', + 'value' => '153', + 'null_val' => 0, + }, + ); + return 1 if set_opts($conf_file, $sect_color, \@opt); + } + + # Debug section + { + my $sect = 'debug'; + my $sect_dbg = weechat::config_new_section($conf_file, $sect, 0, 0, '', '', '', '', '', '', '', '', '', ''); + return 1 if chkconf($conf_file, $sect_dbg, "'${sect}' section"); + + # Options + my @opt = ( + { + 'option' => 'debug_fmt', + 'name' => 'fmt', + 'opt_type' => 'enum', + 'desc' => 'hex dump format used by /dbgcolor: od = simulate "od -An -tx1c", xxd = simulate "xxd -g1 -R always"', + 'str_val' => 'od|xxd', + 'min_val' => 0, + 'max_val' => 0, + 'default' => 'od', + 'value' => 'od', + 'null_val' => 0, + }, + { + 'option' => 'debug_mode', + 'name' => 'mode', + 'opt_type' => 'boolean', + 'desc' => 'show debug information', + 'str_val' => '', + 'min_val' => 0, + 'max_val' => 0, + 'default' => 'off', + 'value' => 'off', + 'null_val' => 0, + } + ); + return 1 if set_opts($conf_file, $sect_dbg, \@opt); + } + + # Look section + { + my $sect = 'look'; + my $sect_color = weechat::config_new_section($conf_file, $sect, 0, 0, '', '', '', '', '', '', '', '', '', ''); + return 1 if chkconf($conf_file, $sect_color, "'${sect}' section"); + + # Options + my @opt = ( + { + 'option' => 'colorize_filter', + 'name' => 'colorize_filter', + 'opt_type' => 'boolean', + 'desc' => 'colorize regex matches in filtered messages from /filter', + 'str_val' => '', + 'min_val' => 0, + 'max_val' => 0, + 'default' => 'off', + 'value' => 'off', + 'null_val' => 0, + }, + ); + return 1 if set_opts($conf_file, $sect_color, \@opt); + } + + return 0; +} + +# Main +# +# Notes: +# - The colorize_cb hook priority needs to be lower than the colorize_nicks.py +# script, otherwise if a nick matches a highlight regex, the *_nicks.py +# script will colorize it and replace the match colors. +# +# Also the priority is lower than the colorize_lines.pl script, but it does +# not matter since *_lines.pl only replaces colors after reset codes. +if (weechat::register( + $script{'prog'}, + $script{'author'}, + $script{'version'}, + $script{'licence'}, + $script{'desc'}, + '', + '' + )) { + # Initialize the script settings. + { + # Configuration file. + return if config_init(); + return if config_read() != 0; + + # Set regex match colors. + set_colors(); + + # Get highlight_regex pattern. + get_regex_cb(); + } + + # Hooks + { + # Update an option when it changes. + weechat::hook_config("${prog}.color.*", 'upd_colors_cb', ''); # Regex match color + weechat::hook_config($regex_opt, 'get_regex_cb', ''); # highlight_regex + + weechat::hook_line('400|', '', '', 'colorize_cb', ''); # Colorize + weechat::hook_line('', "perl.$prog", '', 'notify_cb', ''); # Notify + + # Commands + { + # Argument completions + weechat::hook_completion('plugin_fmt', 'fmt args completion', 'comp_fmt_cb', ''); # 'od' and 'xxd' + weechat::hook_completion('plugin_action', 'action args completion', 'comp_action_cb', ''); # '-eval' and '-no' + + # Format commands descriptions. + my ($dbg_fmt, $dbg_arg, $hl_arg) = fmt_desc(); + + # dbgcolor + weechat::hook_command( + 'dbgcolor', + "debug weechat's strings color codes", + $dbg_fmt, + $dbg_arg, + '%(plugin_fmt) |%(plugin_action) %(eval_variables) %- + || -buffer %(buffers_plugins_names) %(plugin_fmt) |%(plugin_action) %(eval_variables) %-', + 'debug_color_cb', + '' + ); + + # rset + weechat::hook_command( + 'rset', + "fast set $prog options and regexes", + '[fmt|debug|fg|bg|filter|regex|prop]', + $hl_arg, + 'fmt %- + || debug %- + || fg %- + || bg %- + || filter %- + || regex %- + || prop %-', + 'regex_set_cb', + '' + ); + } + } +} diff --git a/perl/coords.pl b/perl/coords.pl index 43104af4..d10c8570 100644 --- a/perl/coords.pl +++ b/perl/coords.pl @@ -322,7 +322,7 @@ =head1 FUNCTION DESCRIPTION use MIME::Base64; use constant SCRIPT_NAME => 'coords'; -weechat::register(SCRIPT_NAME, 'Nei ', '0.7.3.1', 'GPL3', 'copy text and urls', 'stop_coords', '') || return; +weechat::register(SCRIPT_NAME, 'Nei ', '0.7.3.2', 'GPL3', 'copy text and urls', 'stop_coords', '') || return; sub SCRIPT_FILE() { my $infolistptr = weechat::infolist_get('perl_script', '', SCRIPT_NAME); my $filename = weechat::infolist_string($infolistptr, 'filename') if weechat::infolist_next($infolistptr); @@ -898,7 +898,7 @@ sub read_manpage { map { $_ => 'hsignal:'.SCRIPT_NAME } '@chat:button1*', '@chat:button1-event-*', '@chat(perl.[*):button1' }); -weechat::command('', '/alias copywin '.CMD_COPYWIN) +weechat::command('', '/alias add copywin '.CMD_COPYWIN) if 'copywin' ne CMD_COPYWIN && !Nlib::i2h('alias', '', 'copywin') && Nlib::i2h('hook', '', 'command,alias'); # downloaded line fields diff --git a/perl/ctrl_w.pl b/perl/ctrl_w.pl new file mode 100644 index 00000000..d748bc82 --- /dev/null +++ b/perl/ctrl_w.pl @@ -0,0 +1,43 @@ +use strict; +use Encode qw(encode_utf8); +weechat::register( + 'ctrl_w', + 'Juerd <#####@juerd.nl>', + '1.02', + 'PD', + 'Implement readline-like ^W', + '', + '' +); + +sub ctrl_w { + my ($data, $buffer, $args) = @_; + + my $pos = weechat::buffer_get_integer($buffer, 'input_pos'); + my $input = weechat::buffer_get_string($buffer, 'input'); + + utf8::decode($input); + substr($input, 0, $pos) =~ s/((?:^|\S+)\s*)\z// and $pos -= length $1; + utf8::encode($input); + + weechat::buffer_set($buffer, "input", $input); + weechat::buffer_set($buffer, "input_pos", $pos); + + return weechat::WEECHAT_RC_OK; +} + +weechat::hook_command("ctrl_w", "Delete previous word like readline ^W", "", "", "", "ctrl_w", ""); + +# Print helpful message if ctrl-W is still bound to the default function. +my $i = weechat::infolist_get("key", "", "default"); +weechat::infolist_reset_item_cursor($i); +while (weechat::infolist_next($i)) { + my $k = weechat::infolist_string($i, "key"); + my $c = weechat::infolist_string($i, "command"); + $k =~ m[^ctrl-w$]i or next; + $c =~ m[^/input delete_previous_word]i or next; + + weechat::print("", "$k is still bound to $c; to use the ctrl_w script, use /key bind $k /ctrl_w"); + last; +} +weechat::infolist_free($i); diff --git a/perl/curiousignore.pl b/perl/curiousignore.pl index 68381e2f..d868881e 100644 --- a/perl/curiousignore.pl +++ b/perl/curiousignore.pl @@ -24,6 +24,7 @@ # # this scripts needs weechat 0.3.2 or higher # +# v0.4 : add compatibility with new weechat_print modifier data (WeeChat >= 2.9) # v0.3 : add: option cloaked_text_reply (suggested by dAnjou) # : add: option rapid_fire # : add: description for options @@ -36,7 +37,7 @@ use POSIX qw(strftime); my $SCRIPT_NAME = "curiousignore"; -my $SCRIPT_VERSION = "0.3"; +my $SCRIPT_VERSION = "0.4"; my $SCRIPT_DESC = "suppresses messages from specified nick and only prints his nickname in channel"; my $save_to_log = "on"; @@ -63,11 +64,25 @@ sub colorize_cb { my $message = $2; if (not defined $nick) {return $string;} # no nickname - #irc;freenode.#weechat; - $modifier_data =~ (m/irc;(.+?)\.(.+?)\;/); - my $server = $1; - my $channel = $2; - if (not defined $channel) {return $string;} # no channel + my $server = ""; + my $channel = ""; + if ($modifier_data =~ /0x/) + { + # WeeChat >= 2.9 + $modifier_data =~ (m/([^;]*);/); + my $buf_ptr = $1; + $server = weechat::buffer_get_string($buf_ptr, "localvar_server"); + $channel = weechat::buffer_get_string($buf_ptr, "localvar_channel"); + } + else + { + # WeeChat <= 2.8 + #irc;freenode.#weechat; + $modifier_data =~ (m/irc;(.+?)\.(.+?)\;/); + $server = $1; + $channel = $2; + } + if ($server eq "" or $channel eq "") {return $string;} # no channel my $server_chan = $server . "." . $channel; $nick = weechat::string_remove_color($nick,""); # remove colour-codes from nick diff --git a/perl/echo.pl b/perl/echo.pl deleted file mode 100644 index b0b812ee..00000000 --- a/perl/echo.pl +++ /dev/null @@ -1,92 +0,0 @@ -# echo.pl by ArZa : Print a line and additionally set activity level - -# This program is free software: you can modify/redistribute it under the terms of -# GNU General Public License by Free Software Foundation, either version 3 or later -# which you can get from . -# This program is distributed in the hope that it will be useful, but without any warranty. - -weechat::register("echo", "ArZa ", "0.1", "GPL3", "Print a line and additionally set activity level", "", ""); -weechat::hook_command( - "echo", - "Print a line and additionally set activity level. Local variables are expanded when starting with \$ and can be escaped with \\.", - "[ -p/-plugin ] [ -b/-buffer | -c/-core ] [ -l/-level ] [text]", -"-plugin: plugin where printed, default: current plugin --buffer: buffer where printed, default: current buffer, e.g. #weechat or freenode.#weechat) - -core: print to the core buffer - -level: number of the activity level, default: low: - 0=low, 1=message, 2=private, 3=highlight -Examples: - /echo This is a test message - /echo -b freenode.#weechat -level 3 Highlight! - /echo -core This goes to the core buffer - /echo -buffer #weechat -l 1 My variable \\\$name is \$name on \$channel", - "-buffer %(buffer_names) || -core || -level 1|2|3 || -plugin %(plugins_names)", "echo", "" - -); - -sub echo { - - my @args=split(/ /, $_[2]); - my $i=0; - my ($plugin, $buffer, $level) = ("", "", ""); - - while($i<=$#args){ # go through command options - if($args[$i] eq "-b" || $args[$i] eq "-buffer"){ - $i++; - $buffer=$args[$i] if $args[$i]; - }elsif($args[$i] eq "-p" || $args[$i] eq "-plugin"){ - $i++; - $plugin=$args[$i] if $args[$i]; - }elsif($args[$i] eq "-c" || $args[$i] eq "-core"){ - $buffer=weechat::buffer_search_main(); - }elsif($args[$i] eq "-l" || $args[$i] eq "-level"){ - $i++; - $level=$args[$i] if $args[$i]; - }else{ - last; - } - $i++; - } - - if($plugin ne ""){ # use specific plugin if set - $buffer=weechat::buffer_search($plugin, $buffer); - }elsif($buffer ne ""){ - if($buffer=~/^\d+$/){ # if got a number - my $infolist = weechat::infolist_get("buffer", "", ""); - while(weechat::infolist_next($infolist)){ # find the buffer for the number - if(weechat::infolist_integer($infolist, "number") eq $buffer){ - $buffer=weechat::buffer_search( weechat::infolist_string($infolist, "plugin"), weechat::infolist_string($infolist, "name") ); - last; - } - } - weechat::infolist_free($infolist); - }elsif( weechat::buffer_search ( weechat::buffer_get_string( weechat::current_buffer(), "plugin" ), $buffer ) ){ # if buffer found in current plugin - $buffer=weechat::buffer_search ( weechat::buffer_get_string( weechat::current_buffer(), "plugin" ), $buffer ); - }else{ # search even more to find the correct buffer - my $infolist = weechat::infolist_get("buffer", "", ""); - while(weechat::infolist_next($infolist)){ # find the buffer for a short_name - if(lc(weechat::infolist_string($infolist, "short_name")) eq lc($buffer)){ - $buffer=weechat::buffer_search( weechat::infolist_string($infolist, "plugin"), weechat::infolist_string($infolist, "name") ); - last; - } - } - weechat::infolist_free($infolist); - } - } - $buffer=weechat::current_buffer() if $buffer eq "" || $buffer eq $args[$i-1]; # otherwise use the current buffer - - my $j=$i; - $args[$j]=~s/^\\\-/-/ if $args[$j]; # "\-" -> "-" in the beginning - while($j<=$#args){ # go through text - if($args[$j]=~/^\$/){ # replace variables - $args[$j]=weechat::buffer_string_replace_local_var($buffer, $args[$j]); - }elsif($args[$j]=~/^\\[\$\\]/){ # escape variables - $args[$j]=~s/^\\//; - } - $j++; - } - - weechat::print($buffer, join(' ', @args[$i..$#args])); # print the text - weechat::buffer_set($buffer, "hotlist", $level); # set hotlist level - -} diff --git a/perl/expand_url.pl b/perl/expand_url.pl index 3c89ebe2..cc650487 100644 --- a/perl/expand_url.pl +++ b/perl/expand_url.pl @@ -1,5 +1,5 @@ # -# Copyright (c) 2011-2014 by Nils Görs +# Copyright (c) 2011-2018 by Nils Görs # # Get information on a short URL. Find out where it goes. # @@ -16,6 +16,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +# 0.7 : use eval_expression() for option "prefix" +# : removed unused options # 0.6 : fix regex for tag "nick_xxx" # 0.5 : fix expand_own() tag "prefix_nick_ccc" (thanks roughnecks) # : add item "%nick" for prefix (idea by roughnecks) @@ -39,16 +41,14 @@ # Development is currently hosted at # https://github.com/weechatter/weechat-scripts # -# This Script needs WeeChat 0.3.7 or higher +# This Script needs WeeChat 0.4.2 or higher # -# You will find version 0.4: -# http://git.savannah.gnu.org/gitweb/?p=weechat/scripts.git;a=snapshot;h=7bb8ac448c25cf50829ff88d554765a4ff9470cd;sf=tgz use strict; use URI::Find; my $PRGNAME = "expand_url"; -my $version = "0.6"; +my $version = "0.7"; my $AUTHOR = "Nils Görs "; my $LICENSE = "GPL3"; my $DESC = "Get information on a short URL. Find out where it goes."; @@ -56,17 +56,15 @@ my %options = ( "shortener" => "t.co/|goo.gl|tiny.cc|bit.ly|is.gd|tinyurl.com|ur1.ca", "expander" => "http://untiny.me/api/1.0/extract?url= http://api.longurl.org/v1/expand?url= http://expandurl.com/api/v1/?url=", "color" => "blue", - "prefix" => "[url]", - "color_prefix" => "blue", + "prefix" => "\${color:blue}[url]", "expand_own" => "off", ); my %option_desc = ( "shortener" => "list of know shortener. \"|\" separated list", - "expander" => "list of expander to use in script. Please use a space \" \" to separate expander", + "expander" => "list of expander to use in script. This is a space \" \" separate list from expander", "color" => "color to use for expanded url in buffer", - "color_prefix" => "color for prefix", - "prefix" => "displayed prefix. You can use item \"\%nick\" to display nick in prefix (default: [url])", - "expand_own" => "own shortened urls will be expanded (on|off)", + "prefix" => "displayed prefix. You can use item \"\%nick\" to display nick in prefix (note: content is evaluated, see /help eval) (default: \${color:blue}[url]", + "expand_own" => "own shortened urls will be expand (on|off)", ); my %uris; @@ -79,8 +77,9 @@ sub hook_print_cb my ( $data, $buffer, $date, $tags, $displayed, $highlight, $prefix, $message ) = @_; my $tags2 = ",$tags,"; #return weechat::WEECHAT_RC_OK if ( not $tags2 =~ /,notify_[^,]+,/ ); # return if message is not from a nick. - #weechat::print("",$tags); + # get own nick + my $my_nick = ""; if ( lc($options{expand_own}) eq "off" ) { # get servername from buffer @@ -88,21 +87,14 @@ sub hook_print_cb weechat::infolist_next($infolist); my ($servername, undef) = split( /\./, weechat::infolist_string($infolist,"name") ); weechat::infolist_free($infolist); - - my $my_nick = weechat::info_get( "irc_nick", $servername ); # get own nick + $my_nick = weechat::info_get( "irc_nick", $servername ); # get own nick } -# if ( $tags2 =~ /,nick_[$my_nick,]+,/ ){ -# if ( $tags2 =~ m/(^|,)nick_[$my_nick,]+,/ ){ -# return weechat::WEECHAT_RC_OK; -# } -#} - + # get nick from message my $nick_wo_suffix = ($tags2 =~ m/(^|,)nick_([^,]*)/) ? $2 : ""; return weechat::WEECHAT_RC_OK if ($nick_wo_suffix eq ""); + return weechat::WEECHAT_RC_OK if ( lc($options{expand_own}) eq "off" ) and ( $nick_wo_suffix eq $my_nick); -# $tags =~ m/(^|,)nick_(.*),/; -# my $nick_wo_suffix = $2; # nickname without nick_suffix # search uri in message. result in %uris %uris = (); my $finder = URI::Find->new( \&uri_find_cb ); @@ -131,7 +123,8 @@ sub hook_process_cb { if ($out ne ""){ my $how_many_found = 0; my @array = split(/\n/,$out); # split output to single raw lines - foreach ( @array ){ + foreach ( @array ) + { my $uri_only = ""; my $finder = URI::Find->new(sub { my($uri, $orig_uri) = @_; @@ -140,19 +133,12 @@ sub hook_process_cb { # my $finder = URI::Find->new( \&uri_find_one_cb ); $how_many_found = $finder->find(\$_); - my $print_suffix = weechat::color($options{color_prefix}). - $options{prefix}; - - if ( $how_many_found >= 1 ){ # does message contains at least one an url? - if ( grep /$options{prefix}/,"\%nick" ){ - my $nick_color = weechat::info_get('irc_nick_color', $nick_wo_suffix);# get nick-color - $print_suffix = $options{prefix}; - my $nick_prefix = $nick_color. - $nick_wo_suffix. - weechat::color($options{color_prefix}); - $print_suffix =~ s/%nick/$nick_prefix/; - $print_suffix = weechat::color($options{color_prefix}).$print_suffix; - } + + if ( $how_many_found >= 1 )# message contains at least one url? + { + my $print_suffix = my_eval_expression($options{prefix}); # use eval expression() + my $nick_color = weechat::info_get('irc_nick_color', $nick_wo_suffix); # get nick-color + $print_suffix =~ s/%nick/$nick_color$nick_wo_suffix/; # replace %nick with nick weechat::print($buffer, $print_suffix. "\t". weechat::color($options{color}). @@ -169,6 +155,11 @@ sub hook_process_cb { } } +sub my_eval_expression{ + my $value = $_[0]; + return weechat::string_eval_expression($value,{},{},{}); +} + # callback from URI::Find sub uri_find_cb { my ( $uri_url, $uri ) = @_; @@ -176,12 +167,6 @@ sub uri_find_cb { return ""; } -#sub uri_find_one_cb { -#my ( $uri_url, $uri ) = @_; -# $uri_only = $uri; -#return ""; -#} - # get settings or set them if they do not exists. sub init_config{ foreach my $option (keys %options){ @@ -221,8 +206,8 @@ sub toggle_config_by_set{ weechat::register($PRGNAME, $AUTHOR, $version,$LICENSE, $DESC, "", ""); $weechat_version = weechat::info_get("version_number", ""); -if (( $weechat_version eq "" ) or ( $weechat_version < 0x00030700 )){ - weechat::print("",weechat::prefix("error")."$PRGNAME: needs WeeChat >= 0.3.7. Please upgrade: http://www.weechat.org/"); +if (( $weechat_version eq "" ) or ( $weechat_version < 0x00040200 )){ + weechat::print("",weechat::prefix("error")."$PRGNAME: needs WeeChat >= 0.4.2. Please upgrade: http://www.weechat.org/"); weechat::command("","/wait 1ms /perl unload $PRGNAME"); } diff --git a/perl/foo.pl b/perl/foo.pl new file mode 100644 index 00000000..9167b200 --- /dev/null +++ b/perl/foo.pl @@ -0,0 +1,69 @@ +use strict; +use Encode qw(encode_utf8); +weechat::register( + 'foo', + 'Juerd <#####@juerd.nl>', + '3.00', + 'PD', + 'Rot n+i encryption and decryption', + '', + '' +); + +# This is a port of the irssi script foo.pl that has existed since 2001. +# It was originally written as a simple scripting example, but is still +# sometimes used for fun. + + +# Didn't port the non-ascii stuff to weechat, because it assumes Windows-1252 +# or latin1, which nobody uses anymore. Some UTF-8 thing would be better. +#my $char1 = "\xC0-\xCF\xD2-\xD6\xD8-\xDD"; +#my $char2 = "\xE0-\xF6\xF8-\xFF"; + +sub rot { + my ($dir, $rotABC, $rot123, $rotshift, $msg) = @_; + my $i = 0; + for (0 .. length $msg) { + my $char = \substr $msg, $_, 1; + $i += $rotshift; + $$char =~ tr/a-zA-Z/b-zaB-ZA/ for 1..abs $dir *26 - ($rotABC + $i) % 26; + $$char =~ tr/0-9/1-90/ for 1..abs $dir *10 - ($rot123 + $i) % 10; + } + return $msg; +} + +# weechat encodes ^O, ^B, and ^_ differently. +my $O = "\x1c"; +my $B = "(?:[\x1a\x1b]\x01)"; # \x1a is on, \x1b is off. +my $U = "(?:[\x1a\x1b]\x04)"; + +sub hook_print_cb { + my ($data, $buffer, $date, $tags, $displayed, $highlight, $prefix, $msg) = @_; + return weechat::WEECHAT_RC_OK unless $msg =~ s/^$O($B+)$O($B+)$O($O*)//; + $msg = rot 1, length($1)/2, length($2)/2, length $3, $msg; + + weechat::print_date_tags($buffer, $date, $tags, "$prefix\t\x1a\x01$msg"); + return weechat::WEECHAT_RC_OK; +} + +sub hook_cmd_rot_cb { + my ($data, $buffer, $args) = @_; + + my $rotABC = 1 + int rand 13; + my $rot123 = 1 + 2 * int rand 4; + my $rotshift = 1 + int rand 10; + weechat::command( + $buffer, + encode_utf8(sprintf "/say \cO%s\cO%s\cO%s%s", + "\cB" x $rotABC, + "\cB" x $rot123, + "\cO" x $rotshift, + rot 0, $rotABC, $rot123, $rotshift, $args + ) + ); +} + + +# Yuck, symbolic references to subs instead of actual CODE refs... +weechat::hook_print("", "notify_none,notify_message,notify_private,notify_highlight", "", 0, "hook_print_cb", ""); +weechat::hook_command("rot", "Sends via UeberRot", "", "", "", "hook_cmd_rot_cb", ""); diff --git a/perl/format_lines.pl b/perl/format_lines.pl index 17ca13a6..5f425325 100644 --- a/perl/format_lines.pl +++ b/perl/format_lines.pl @@ -17,6 +17,8 @@ # # Changelog: +# 2020-05-09, FlashCode +# version 1.6: add compatibility with new weechat_print modifier data (WeeChat >= 2.9) # 2013-01-16, R1cochet # version 1.5: Fixed error where filtered lines would still show blank lines (found by Nei). Added option to hide prefix on formatted lines (suggested by Nei). # 2013-01-15, R1cochet @@ -27,7 +29,7 @@ use warnings; use Text::Format; -my $VERSION = "1.5"; +my $VERSION = "1.6"; my $SCRIPT_DESC = "format the output of each line."; weechat::register("format_lines", "R1cochet", $VERSION, "GPL3", $SCRIPT_DESC, "", ""); @@ -216,9 +218,25 @@ sub format_lines_cb { return $string if ($string eq ""); } - my ($plugin, $buffer_name, $tags) = split ";", $modifier_data; - my $buffer = weechat::buffer_search($plugin, $buffer_name); - my ($server, $channel) = split /\./, $buffer_name, 2; + my $buffer = ""; + my $tags = ""; + if ($modifier_data =~ /0x/) + { + # WeeChat >= 2.9 + $modifier_data =~ m/([^;]*);(.*)/; + $buffer = $1; + $tags = $2; + } + else { + # WeeChat <= 2.8 + $modifier_data =~ m/([^;]*);([^;]*);(.*)/; + $buffer = weechat::buffer_search($1, $2); + $tags = $3; + } + my $plugin = weechat::buffer_get_string($buffer, "plugin"); + my $server = weechat::buffer_get_string($buffer, "localvar_server"); + my $channel = weechat::buffer_get_string($buffer, "localvar_channel"); + my ($prefix, $msg) = split /\t/, $string; # whitelists diff --git a/perl/highmon.pl b/perl/highmon.pl index 1c077125..f843cade 100644 --- a/perl/highmon.pl +++ b/perl/highmon.pl @@ -1,6 +1,5 @@ # # highmon.pl - Highlight Monitoring for weechat 0.3.0 -# Version 2.5 # # Add 'Highlight Monitor' buffer/bar to log all highlights in one spot # @@ -73,6 +72,10 @@ # Bugs and feature requests at: https://github.com/KenjiE20/highmon # History: +# 2020-06-21, Sebastien Helleu : +# v2.7: make call to bar_new compatible with WeeChat >= 2.9 +# 2019-05-13, HubbeKing +# v2.6: -add: send "logger_backlog" signal on buffer open if logging is enabled # 2014-08-16, KenjiE20 : # v2.5: -add: clearbar command to clear bar output # -add: firstrun output prompt to check the help text for set up hints as they were being missed @@ -264,7 +267,14 @@ sub highmon_bar_open # Make the bar item weechat::bar_item_new("highmon", "highmon_bar_build", ""); - $highmon_bar = weechat::bar_new ("highmon", "off", 100, "root", "", "bottom", "vertical", "vertical", 0, 0, "default", "cyan", "default", "on", "highmon"); + if (weechat::info_get("version_number", "") >= 0x02090000) + { + $highmon_bar = weechat::bar_new ("highmon", "off", 100, "root", "", "bottom", "vertical", "vertical", 0, 0, "default", "cyan", "default", "default", "on", "highmon"); + } + else + { + $highmon_bar = weechat::bar_new ("highmon", "off", 100, "root", "", "bottom", "vertical", "vertical", 0, 0, "default", "cyan", "default", "on", "highmon"); + } return weechat::WEECHAT_RC_OK; } @@ -306,7 +316,7 @@ sub highmon_buffer_open # Turn off notify, highlights if ($highmon_buffer ne "") { - if (weechat::config_get_plugin("hotlist_show" eq "off")) + if (weechat::config_get_plugin("hotlist_show") eq "off") { weechat::buffer_set($highmon_buffer, "notify", "0"); } @@ -317,6 +327,11 @@ sub highmon_buffer_open { weechat::buffer_set($highmon_buffer, "localvar_set_no_log", "1"); } + # send "logger_backlog" signal if logging is enabled to display backlog + if (weechat::config_get_plugin("logging") eq "on") + { + weechat::hook_signal_send("logger_backlog", weechat::WEECHAT_HOOK_SIGNAL_POINTER, $highmon_buffer) + } } return weechat::WEECHAT_RC_OK; } @@ -710,7 +725,7 @@ sub highmon_new_message if ($cb_high == "1" || (weechat::config_get_plugin("merge_private") eq "on" && $cb_tags =~ /notify_private/)) { # Pre bug #29618 (0.3.3) away detect - if (weechat::info_get("version_number", "") <= 197120) + if (weechat::info_get("version_number", "") <= 0x00030200) { $away = ''; # Get infolist for this server @@ -1124,7 +1139,7 @@ sub format_buffer_name } # Check result of register, and attempt to behave in a sane manner -if (!weechat::register("highmon", "KenjiE20", "2.5", "GPL3", "Highlight Monitor", "", "")) +if (!weechat::register("highmon", "KenjiE20", "2.7", "GPL3", "Highlight Monitor", "", "")) { # Double load weechat::print ("", "\tHighmon is already loaded"); diff --git a/perl/hotlist2extern.pl b/perl/hotlist2extern.pl index 3d99e238..c2cf8c63 100644 --- a/perl/hotlist2extern.pl +++ b/perl/hotlist2extern.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2009-2010 by Nils Görs +# Copyright (c) 2009-2018 by Nils Görs # # waiting for hotlist to change and then execute a user specified command # or writes the hotlist to screen title. @@ -22,6 +22,8 @@ # # Script inspirated and tested by LaoLang_cool # +# 1.0 : add compatibility with WeeChat >= 3.2 (XDG directories) +# 0.9 : add eval_expression() for format options # 0.8 : escape special characters in hotlist (arza) # 0.7 : using %h for weechat-dir instead of hardcoded path in script (flashcode) # 0.6 : new option "use_title" to print hotlist in screen title. @@ -32,69 +34,27 @@ # 0.3 : usersettings won't be loaded, sorry! :-( # : added a more complex sort routine (from important to unimportant and also numeric) # : added options: "delimiter", "priority_remove" and "hotlist_remove_format" -# -# -# use the following settings for hotlist_format: -# %h = weechat-dir (~/.weechat) -# %H = filled with highlight_char if a highlight message was written in channel. For example: * -# %N = filled with buffer number: 1 2 3 .... -# %S = filled with short name of channel: #weechat -# -# export the hotlist_format to your external command. -# %X -# -# Usage: -# template to use for display (for example: "1:freenode *2:#weechat"): -# /set plugins.var.perl.hotlist2extern.hotlist_format "%H%N:%S" -# -# Output (for example: "WeeChat Act: %H%N:%S"): -# /set plugins.var.perl.hotlist2extern.external_command_hotlist "echo WeeChat Act: %X >%h/hotlist_output.txt" -# -# Output if there is no activity (for example: "WeeChat: no activity"): -# /set plugins.var.perl.hotlist2extern.external_command_hotlist_empty "echo 'WeeChat: no activity ' >%h/hotlist_output.txt" -# -# charset for a highlight message: -# /set plugins.var.perl.hotlist2extern.highlight_char "*" -# -# template that shall be remove when message priority is low. (for example, the buffer name will be removed and only the buffer -# number will be display instead! (1 *2:#weechat): -# /set plugins.var.perl.hotlist2extern.hotlist_remove_format ":%S" -# -# message priority when using hotlist_remove_format (-1 means off) -# /set plugins.var.perl.hotlist2extern.priority_remove 0 -# -# display messages with level: -# 0=crappy msg (join/part) and core buffer informations, 1=msg, 2=pv, 3=nick highlight -# /set plugins.var.perl.hotlist2extern.lowest_priority 0 -# -# delimiter to use: -# /set plugins.var.perl.hotlist2extern.delimiter "," -# -# hotlist will be printed to screen title: -# /set plugins.var.perl.hotlist2extern.use_title "on" use strict; -my $hotlist_format = "%H%N:%S"; -my $hotlist_remove_format = ":%S"; -my $external_command_hotlist = "echo WeeChat Act: %X >%h/hotlist_output.txt"; -my $external_command_hotlist_empty = "echo \'WeeChat: no activity \' >%h/hotlist_output.txt"; -my $highlight_char = "*"; -my $lowest_priority = 0; -my $priority_remove = 0; -my $delimiter = ","; -my $use_title = "on"; - -my $prgname = "hotlist2extern"; -my $version = "0.8"; -my $description = "Give hotlist to an external file/program/screen title"; -my $current_buffer = ""; +my $SCRIPT_NAME = "hotlist2extern"; +my $SCRIPT_VERSION = "1.0"; +my $SCRIPT_DESC = "Give hotlist to an external file/program/screen title"; +my $SCRIPT_AUTHOR = "Nils Görs "; + +# default values +my %options = ( + "hotlist_format" => "%H%N:%S", + "hotlist_remove_format" => ":%S", + "external_command_hotlist" => "echo WeeChat Act: %X >%h/hotlist_output.txt", + "external_command_hotlist_empty" => "echo \'WeeChat: no activity \' >%h/hotlist_output.txt", + "highlight_char" => "*", + "lowest_priority" => "0", + "priority_remove" => "0", + "delimiter" => ",", + "use_title" => "on", +); -my $plugin_name = ""; my $weechat_dir = ""; -my $buffer_name = ""; -my $buffer_number = 0; -my $buffer_pointer = 0; -my $short_name = ""; my $res = ""; my $res2 = ""; my $priority = 0; @@ -106,36 +66,36 @@ sub hotlist_changed{ @table = (); $table = ""; - $current_buffer = weechat::current_buffer; # get current buffer + my $current_buffer = weechat::current_buffer; # get current buffer my $hotlist = weechat::infolist_get("hotlist","",""); # Pointer to Infolist while (weechat::infolist_next($hotlist)) { $priority = weechat::infolist_integer($hotlist, "priority"); - $res = $hotlist_format; # save hotlist format - $res2 = $external_command_hotlist; # save external_hotlist format + $res = $options{hotlist_format}; # save hotlist format + $res2 = $options{external_command_hotlist}; # save external_hotlist format - $plugin_name = weechat::infolist_string($hotlist,"plugin_name"); - $buffer_name = weechat::infolist_string($hotlist,"buffer_name"); - $buffer_number = weechat::infolist_integer($hotlist,"buffer_number"); # get number of buffer - $buffer_pointer = weechat::infolist_pointer($hotlist, "buffer_pointer"); # Pointer to buffer - $short_name = weechat::buffer_get_string($buffer_pointer, "short_name"); # get short_name of buffer + my $plugin_name = weechat::infolist_string($hotlist,"plugin_name"); + my $buffer_name = weechat::infolist_string($hotlist,"buffer_name"); + my $buffer_number = weechat::infolist_integer($hotlist,"buffer_number"); # get number of buffer + my $buffer_pointer = weechat::infolist_pointer($hotlist, "buffer_pointer"); # Pointer to buffer + my $short_name = weechat::buffer_get_string($buffer_pointer, "short_name"); # get short_name of buffer - unless ($priority < $lowest_priority){ - create_output(); + unless ($priority < $options{lowest_priority}){ + create_output($buffer_number, $short_name); } } weechat::infolist_free($hotlist); $table = @table; if ($table eq 0){ - unless ($external_command_hotlist_empty eq ""){ # does we have a command for empty string? - if ($use_title eq "on"){ - weechat::window_set_title($external_command_hotlist_empty); + unless ($options{external_command_hotlist_empty} eq ""){ # does we have a command for empty string? + if ($options{use_title} eq "on"){ + weechat::window_set_title(eval_expression($options{external_command_hotlist_empty})); }else{ - if (grep (/\%h/,$external_command_hotlist_empty)){ # does %h is in string? - $external_command_hotlist_empty =~ s/%h/$weechat_dir/; # add weechat-dir + if (grep (/\%h/,$options{external_command_hotlist_empty})){ # does %h is in string? + $options{external_command_hotlist_empty} =~ s/%h/$weechat_dir/; # add weechat-dir } - system($external_command_hotlist_empty); + system(eval_expression($options{external_command_hotlist_empty})); } } } @@ -143,33 +103,34 @@ sub hotlist_changed{ } sub create_output{ - $res = $hotlist_format; # save hotlist format - $res2 = $external_command_hotlist; # save external_hotlist format + my ($buffer_number, $short_name) = @_; + $res = eval_expression($options{hotlist_format}); # save hotlist format + $res2 = eval_expression($options{external_command_hotlist}); # save external_hotlist format if ($priority == 3){ # priority is highlight - if (grep (/\%H/,$hotlist_format)){ # check with original!!! - $res =~ s/\%H/$highlight_char/g; + if (grep (/\%H/,$options{hotlist_format})){ # check with original!!! + $res =~ s/\%H/$options{highlight_char}/g; } }else{ # priority != 3 $res =~ s/\%H//g; # remove all %H } - if ($priority <= $priority_remove){ - $res =~ s/$hotlist_remove_format//; # remove hotlist_remove_format - if (grep (/\%S/,$hotlist_format)){ # does %S is in string? (check with original!!!) + if ($priority <= $options{priority_remove}){ + $res =~ s/$options{hotlist_remove_format}//; # remove hotlist_remove_format + if (grep (/\%S/,$options{hotlist_format})){ # does %S is in string? (check with original!!!) $res =~ s/%S/$short_name/; # add short_name } - if (grep (/\%N/,$hotlist_format)){ + if (grep (/\%N/,$options{hotlist_format})){ $res =~ s/%N/$buffer_number/; # add buffer_number } }else{ - if (grep (/\%S/,$hotlist_format)){ # does %S is in string? (check with original!!!) + if (grep (/\%S/,$options{hotlist_format})){ # does %S is in string? (check with original!!!) $res =~ s/%S/$short_name/; # add short_name } - if (grep (/\%N/,$hotlist_format)){ + if (grep (/\%N/,$options{hotlist_format})){ $res =~ s/%N/$buffer_number/; # add buffer_number } } - if ($res ne $hotlist_format and $res ne ""){ # did $res changed? + if ($res ne $options{hotlist_format} and $res ne ""){ # did $res changed? my $res2 = $res; # save search string. $res2=qq(\Q$res2); # kill metachars, for searching first unless (grep /^$res2$/, @table){ # does we have added $res to @table? @@ -179,16 +140,16 @@ sub create_output{ $res=qq(\Q$res); # kill metachars first if (grep /^$res$/, @table){ # does we have added $res to @table? - my $export = join("$delimiter", sort_routine(@table)); + my $export = join("$options{delimiter}", sort_routine(@table)); $export = qq(\Q$export); # escape special characters - if (grep (/\%X/,$external_command_hotlist)){ # check for %X option. + if (grep (/\%X/,$options{external_command_hotlist})){ # check for %X option. $res2 =~ s/%X/$export/; - if (grep (/\%h/,$external_command_hotlist)){ # does %h is in string? + if (grep (/\%h/,$options{external_command_hotlist})){ # does %h is in string? $res2 =~ s/%h/$weechat_dir/; # add weechat-dir } - if ($use_title eq "on"){ + if ($options{use_title} eq "on"){ weechat::window_set_title($res2); }else{ system($res2); @@ -199,7 +160,7 @@ sub create_output{ } # first sort channels with highlight, then channels with -# action and the rest will be put it at the end of list +# action and the rest will be placed at the end of list sub sort_routine { my @zeilen = @_; my @sortiert = map { $_->[0] } @@ -214,102 +175,61 @@ sub _extern{ return weechat::WEECHAT_RC_OK; } -sub init{ -# set value of script (for example starting script the first time) - if (!weechat::config_is_set_plugin("external_command_hotlist")){ - weechat::config_set_plugin("external_command_hotlist", $external_command_hotlist); - }else{ - $external_command_hotlist = weechat::config_get_plugin("external_command_hotlist"); - } - if (!weechat::config_is_set_plugin("external_command_hotlist_empty")){ - weechat::config_set_plugin("external_command_hotlist_empty", $external_command_hotlist_empty); - }else{ - $external_command_hotlist_empty = weechat::config_get_plugin("external_command_hotlist_empty"); - } - if (!weechat::config_is_set_plugin("highlight_char")){ - weechat::config_set_plugin("highlight_char", $highlight_char); - }else{ - $highlight_char = weechat::config_get_plugin("highlight_char"); - } - if (!weechat::config_is_set_plugin("lowest_priority")){ - weechat::config_set_plugin("lowest_priority", $lowest_priority); - }else{ - $lowest_priority = weechat::config_get_plugin("lowest_priority"); - } - if (!weechat::config_is_set_plugin("hotlist_format")){ - weechat::config_set_plugin("hotlist_format", $hotlist_format); - }else{ - $hotlist_format = weechat::config_get_plugin("hotlist_format"); - } - if (!weechat::config_is_set_plugin("hotlist_remove_format")){ - weechat::config_set_plugin("hotlist_remove_format", $hotlist_remove_format); - }else{ - $hotlist_remove_format = weechat::config_get_plugin("hotlist_remove_format"); - } - if (!weechat::config_is_set_plugin("priority_remove")){ - weechat::config_set_plugin("priority_remove", $priority_remove); - }else{ - $priority_remove = weechat::config_get_plugin("priority_remove"); - } - if (!weechat::config_is_set_plugin("delimiter")){ - weechat::config_set_plugin("delimiter", $delimiter); - }else{ - $delimiter = weechat::config_get_plugin("delimiter"); - } - if (!weechat::config_is_set_plugin("use_title")){ - weechat::config_set_plugin("use_title", $use_title); - }else{ - $use_title = weechat::config_get_plugin("use_title"); - } - $weechat_dir = weechat::info_get("weechat_dir", ""); +sub eval_expression{ + my ( $string ) = @_; + $string = weechat::string_eval_expression($string, {}, {},{}); + return $string; } -sub toggle_config_by_set{ -my ( $pointer, $name, $value ) = @_; +sub init_config{ + foreach my $option(keys %options){ + if (!weechat::config_is_set_plugin($option)){ + weechat::config_set_plugin($option, $options{$option}); + } + else{ + $options{$option} = weechat::config_get_plugin($option); + } + } +} - if ($name eq "plugins.var.perl.$prgname.external_command_hotlist"){ - $external_command_hotlist = $value; - return weechat::WEECHAT_RC_OK; - } - if ($name eq "plugins.var.perl.$prgname.external_command_hotlist_empty"){ - $external_command_hotlist_empty = $value; - return weechat::WEECHAT_RC_OK; - } - if ($name eq "plugins.var.perl.$prgname.highlight_char"){ - $highlight_char = $value; - return weechat::WEECHAT_RC_OK; - } - if ($name eq "plugins.var.perl.$prgname.lowest_priority"){ - $lowest_priority = $value; - return weechat::WEECHAT_RC_OK; - } - if ($name eq "plugins.var.perl.$prgname.hotlist_format"){ - $hotlist_format = $value; - return weechat::WEECHAT_RC_OK; - } - if ($name eq "plugins.var.perl.$prgname.hotlist_remove_format"){ - $hotlist_remove_format = $value; - return weechat::WEECHAT_RC_OK; - } - if ($name eq "plugins.var.perl.$prgname.priority_remove"){ - $priority_remove = $value; - return weechat::WEECHAT_RC_OK; - } - if ($name eq "plugins.var.perl.$prgname.delimiter"){ - $delimiter = $value; - return weechat::WEECHAT_RC_OK; - } - if ($name eq "plugins.var.perl.$prgname.use_title"){ - $use_title = $value; +sub toggle_config_by_set{ + my ( $pointer, $name, $value ) = @_; + $name = substr($name,length("plugins.var.perl.$SCRIPT_NAME."),length($name)); + $options{$name} = $value; return weechat::WEECHAT_RC_OK; - } } # first function called by a WeeChat-script. -weechat::register($prgname, "Nils Görs ", $version, - "GPL3", $description, "", ""); - - init(); # get user settings - - weechat::hook_signal("hotlist_changed", "hotlist_changed", ""); - weechat::hook_config( "plugins.var.perl.$prgname.*", "toggle_config_by_set", "" ); +weechat::register($SCRIPT_NAME, $SCRIPT_AUTHOR, $SCRIPT_VERSION, + "GPL3", $SCRIPT_DESC, "", ""); + +weechat::hook_command($SCRIPT_NAME, $SCRIPT_DESC, + "", + "This script allows you to export the hotlist to a file or screen title.\n". + "use the following intern variables for the hotlist_format:\n". + " %h = weechat_data_dir, better use \${weechat_data_dir} (or \${info:weechat_dir} with WeeChat < 3.2) \n". + " %H = replaces with highlight_char, if a highlight message was received. For example: *\n". + " %N = replaces with buffer number: 1 2 3 ....\n". + " %S = replaces with short name of channel: #weechat\n". + " %X = export the whole hotlist_format to your external command.\n". + "\n". + "configure script with: /fset plugins.var.perl.hotlist2extern\n". + "print hotlist to screen title: plugins.var.perl.hotlist2extern.use_title\n". + "delimiter to use : plugins.var.perl.hotlist2extern.delimiter\n". + "charset for highlight message: plugins.var.perl.hotlist2extern.highlight_char\n". + "message priority for hotlist_remove_format (-1 means off): plugins.var.perl.hotlist2extern.priority_remove\n". + "display messages level : plugins.var.perl.hotlist2extern.lowest_priority\n". + "following options are evaluated:\n". + "template for display : plugins.var.perl.hotlist2extern.hotlist_format\n". + "template for low priority : plugins.var.perl.hotlist2extern.hotlist_remove_format\n". + "Output format : plugins.var.perl.hotlist2extern.external_command_hotlist\n". + "Output format 'no activity' : plugins.var.perl.hotlist2extern.external_command_hotlist_empty\n". + "", + "", "", ""); + + +init_config(); # /set +$weechat_dir = weechat::info_get("weechat_data_dir", ""); +$weechat_dir = weechat::info_get("weechat_dir", "") if (!$weechat_dir); +weechat::hook_signal("hotlist_changed", "hotlist_changed", ""); +weechat::hook_config( "plugins.var.perl.$SCRIPT_NAME.*", "toggle_config_by_set", "" ); diff --git a/perl/iset.pl b/perl/iset.pl deleted file mode 100644 index 163dfb5e..00000000 --- a/perl/iset.pl +++ /dev/null @@ -1,1624 +0,0 @@ -# -# Copyright (C) 2008-2014 Sebastien Helleu -# Copyright (C) 2010-2015 Nils Görs -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# Set WeeChat and plugins options interactively. -# -# History: -# -# 2016-07-08, nils_2 -# version 4.2: add diff function -# 2016-02-06, Sebastien Helleu : -# version 4.1: remove debug print -# 2015-12-24, Sebastien Helleu : -# version 4.0: add support of parent options (inherited values in irc servers) -# with WeeChat >= 1.4 -# 2015-05-16, Sebastien Helleu : -# version 3.9: fix cursor position when editing an option with WeeChat >= 1.2 -# 2015-05-02, arza : -# version 3.8: don't append "null" to /set when setting an undefined setting -# 2015-05-01, nils_2 : -# version 3.7: fix two perl warnings (reported by t3chguy) -# 2014-09-30, arza : -# version 3.6: fix current line counter when options aren't found -# 2014-06-03, nils_2 : -# version 3.5: add new option "use_mute" -# 2014-01-30, stfn : -# version 3.4: add new options "color_value_diff" and "color_value_diff_selected" -# 2014-01-16, luz : -# version 3.3: fix bug with column alignment in iset buffer when option -# name contains unicode characters -# 2013-08-03, Sebastien Helleu : -# version 3.2: allow "q" as input in iset buffer to close it -# 2013-07-14, Sebastien Helleu : -# version 3.1: remove unneeded calls to iset_refresh() in mouse callback -# (faster mouse actions when lot of options are displayed), -# fix bug when clicking on a line after the last option displayed -# 2013-04-30, arza : -# version 3.0: simpler title, fix refresh on unset -# 2012-12-16, nils_2 : -# version 2.9: fix focus window with iset buffer on mouse click -# 2012-08-25, nils_2 : -# version 2.8: most important key and mouse bindings for iset buffer added to title-bar (idea The-Compiler) -# 2012-07-31, nils_2 : -# version 2.7: add combined option and value search (see /help iset) -# : add exact value search (see /help iset) -# : fix problem with metacharacter in value search -# : fix use of uninitialized value for unset option and reset value of option -# 2012-07-25, nils_2 : -# version 2.6: switch to iset buffer (if existing) when command /iset is called with arguments -# 2012-03-17, Sebastien Helleu : -# version 2.5: fix check of sections when creating config file -# 2012-03-09, Sebastien Helleu : -# version 2.4: fix reload of config file -# 2012-02-02, nils_2 : -# version 2.3: fixed: refresh problem with new search results and cursor was outside window. -# : add: new option "current_line" in title bar -# version 2.2: fixed: refresh error when toggling plugins description -# 2011-11-05, nils_2 : -# version 2.1: use own config file (iset.conf), fix own help color (used immediately) -# 2011-10-16, nils_2 : -# version 2.0: add support for left-mouse-button and more sensitive mouse gesture (for integer/color options) -# add help text for mouse support -# 2011-09-20, Sebastien Helleu : -# version 1.9: add mouse support, fix iset buffer, fix errors on first load under FreeBSD -# 2011-07-21, nils_2 : -# version 1.8: added: option "show_plugin_description" (alt+p) -# fixed: typos in /help iset (lower case for alt+'x' keys) -# 2011-05-29, nils_2 : -# version 1.7: added: version check for future needs -# added: new option (scroll_horiz) and usage of scroll_horiz function (weechat >= 0.3.6 required) -# fixed: help_bar did not pop up immediately using key-shortcut -# 2011-02-19, nils_2 : -# version 1.6: added: display of all possible values in help bar (show_help_extra_info) -# fixed: external user options never loaded when starting iset first time -# 2011-02-13, Sebastien Helleu : -# version 1.5: use new help format for command arguments -# 2011-02-03, nils_2 : -# version 1.4: fixed: restore value filter after /upgrade using buffer local variable. -# 2011-01-14, nils_2 : -# version 1.3: added function to search for values (option value_search_char). -# code optimization. -# 2010-12-26, Sebastien Helleu : -# version 1.2: improve speed of /upgrade when iset buffer is open, -# restore filter used after /upgrade using buffer local variable, -# use /iset filter argument if buffer is open. -# 2010-11-21, drubin : -# version 1.1.1: fix bugs with cursor position -# 2010-11-20, nils_2 : -# version 1.1: cursor position set to value -# 2010-08-03, Sebastien Helleu : -# version 1.0: move misplaced call to infolist_free() -# 2010-02-02, rettub : -# version 0.9: turn all the help stuff off if option 'show_help_bar' is 'off', -# new key binding - to toggle help_bar and help stuff on/off -# 2010-01-30, nils_2 : -# version 0.8: fix error when option does not exist -# 2010-01-24, Sebastien Helleu : -# version 0.7: display iset bar only on iset buffer -# 2010-01-22, nils_2 and drubin: -# version 0.6: add description in a bar, fix singular/plural bug in title bar, -# fix selected line when switching buffer -# 2009-06-21, Sebastien Helleu : -# version 0.5: fix bug with iset buffer after /upgrade -# 2009-05-02, Sebastien Helleu : -# version 0.4: sync with last API changes -# 2009-01-04, Sebastien Helleu : -# version 0.3: open iset buffer when /iset command is executed -# 2009-01-04, Sebastien Helleu : -# version 0.2: use null values for options, add colors, fix refresh bugs, -# use new keys to reset/unset options, sort options by name, -# display number of options in buffer's title -# 2008-11-05, Sebastien Helleu : -# version 0.1: first official version -# 2008-04-19, Sebastien Helleu : -# script creation - -use strict; - -my $PRGNAME = "iset"; -my $VERSION = "4.2"; -my $DESCR = "Interactive Set for configuration options"; -my $AUTHOR = "Sebastien Helleu "; -my $LICENSE = "GPL3"; -my $LANG = "perl"; -my $ISET_CONFIG_FILE_NAME = "iset"; - -my $iset_config_file; -my $iset_buffer = ""; -my $wee_version_number = 0; -my @iset_focus = (); -my @options_names = (); -my @options_parent_names = (); -my @options_types = (); -my @options_values = (); -my @options_default_values = (); -my @options_parent_values = (); -my @options_is_null = (); -my $option_max_length = 0; -my $current_line = 0; -my $filter = "*"; -my $description = ""; -my $options_name_copy = ""; -my $iset_filter_title = ""; -# search modes: 0 = index() on value, 1 = grep() on value, 2 = grep() on option, 3 = grep on option & value, 4 = diff all, 5 = diff parts -my $search_mode = 2; -my $search_value = ""; -my $help_text_keys = "alt + space: toggle, +/-: increase/decrease, enter: change, ir: reset, iu: unset, v: toggle help bar"; -my $help_text_mouse = "Mouse: left: select, right: toggle/set, right + drag left/right: increase/decrease"; -my %options_iset; - -my %mouse_keys = ("\@chat(perl.$PRGNAME):button1" => "hsignal:iset_mouse", - "\@chat(perl.$PRGNAME):button2*" => "hsignal:iset_mouse", - "\@chat(perl.$PRGNAME):wheelup" => "/repeat 5 /iset **up", - "\@chat(perl.$PRGNAME):wheeldown" => "/repeat 5 /iset **down"); - - -sub iset_title -{ - if ($iset_buffer ne "") - { - my $current_line_counter = ""; - if (weechat::config_boolean($options_iset{"show_current_line"}) == 1) - { - if (@options_names eq 0) - { - $current_line_counter = "0/"; - } - else - { - $current_line_counter = ($current_line + 1) . "/"; - } - } - my $show_filter = ""; - if ($search_mode eq 0) - { - $iset_filter_title = "(value) "; - $show_filter = $search_value; - if ( substr($show_filter,0,1) eq weechat::config_string($options_iset{"value_search_char"}) ) - { - $show_filter = substr($show_filter,1,length($show_filter)); - } - } - elsif ($search_mode eq 1) - { - $iset_filter_title = "(value) "; - $show_filter = "*".$search_value."*"; - } - elsif ($search_mode eq 2) - { - $iset_filter_title = ""; - $filter = "*" if ($filter eq ""); - $show_filter = $filter; - } - elsif ($search_mode == 4 or $search_mode == 5) - { - $iset_filter_title = "diff: "; - $show_filter = "all"; - $show_filter = $search_value if $search_mode == 5; - } - elsif ($search_mode eq 3) - { - $iset_filter_title = "(option) "; - $show_filter = $filter - .weechat::color("default") - ." / (value) " - .weechat::color("yellow") - ."*".$search_value."*"; - } - weechat::buffer_set($iset_buffer, "title", - $iset_filter_title - .weechat::color("yellow") - .$show_filter - .weechat::color("default")." | " - .$current_line_counter - .@options_names - ." | " - .$help_text_keys - ." | " - .$help_text_mouse); - } -} - -sub iset_create_filter -{ - $filter = $_[0]; - if ( $search_mode == 3 ) - { - my @cmd_array = split(/ /,$filter); - my $array_count = @cmd_array; - $filter = $cmd_array[0]; - $filter = $cmd_array[0] . " " . $cmd_array[1] if ( $array_count >2 ); - } - $filter = "$1.*" if ($filter =~ /f (.*)/); # search file - $filter = "*.$1.*" if ($filter =~ /s (.*)/); # search section - if ((substr($filter, 0, 1) ne "*") && (substr($filter, -1, 1) ne "*")) - { - $filter = "*".$filter."*"; - } - if ($iset_buffer ne "") - { - weechat::buffer_set($iset_buffer, "localvar_set_iset_filter", $filter); - } -} - -sub iset_buffer_input -{ - my ($data, $buffer, $string) = ($_[0], $_[1], $_[2]); - - # string begins with space? - return weechat::WEECHAT_RC_OK if (substr($string, 0, 1 ) eq " "); - - if ($string eq "q") - { - weechat::buffer_close($buffer); - return weechat::WEECHAT_RC_OK; - } - $search_value = ""; - my @cmd_array = split(/ /,$string); - my $array_count = @cmd_array; - my $string2 = substr($string, 0, 1); - if ($string2 eq weechat::config_string($options_iset{"value_search_char"}) - or (defined $cmd_array[0] and $cmd_array[0] eq weechat::config_string($options_iset{"value_search_char"}).weechat::config_string($options_iset{"value_search_char"})) ) - { - $search_mode = 1; - $search_value = substr($string, 1); - iset_get_values($search_value); - if ($iset_buffer ne "") - { - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $search_value); - } - } - # show all diff values - elsif ($string eq "d") - { - $search_mode = 4; -# iset_title(); - iset_create_filter("*"); - iset_get_options("*"); - } - elsif ( $array_count >= 2 and $cmd_array[0] eq "d") - { - $search_mode = 5; - $search_value = substr($cmd_array[1], 0); # cut value_search_char - $search_value = substr($cmd_array[2], 0) if ( $array_count > 2); # cut value_search_char - iset_create_filter($search_value); - iset_get_options($search_value); - - } - else - { - $search_mode = 2; - if ( $array_count >= 2 and $cmd_array[0] ne "f" or $cmd_array[0] ne "s" ) - { - if ( defined $cmd_array[1] and substr($cmd_array[1], 0, 1) eq weechat::config_string($options_iset{"value_search_char"}) - or defined $cmd_array[2] and substr($cmd_array[2], 0, 1) eq weechat::config_string($options_iset{"value_search_char"}) ) - { - $search_mode = 3; - $search_value = substr($cmd_array[1], 1); # cut value_search_char - $search_value = substr($cmd_array[2], 1) if ( $array_count > 2); # cut value_search_char - } - } - if ( $search_mode == 3) - { - iset_create_filter($string); - iset_get_options($search_value); - } - else - { - iset_create_filter($string); - iset_get_options(""); - } - } - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_mode", $search_mode); - weechat::buffer_clear($buffer); - $current_line = 0; - iset_refresh(); - return weechat::WEECHAT_RC_OK; -} - -sub iset_buffer_close -{ - $iset_buffer = ""; - - return weechat::WEECHAT_RC_OK; -} - -sub iset_init -{ - $current_line = 0; - $iset_buffer = weechat::buffer_search($LANG, $PRGNAME); - if ($iset_buffer eq "") - { - $iset_buffer = weechat::buffer_new($PRGNAME, "iset_buffer_input", "", "iset_buffer_close", ""); - } - else - { - my $new_filter = weechat::buffer_get_string($iset_buffer, "localvar_iset_filter"); - $search_mode = weechat::buffer_get_string($iset_buffer, "localvar_iset_search_mode"); - $search_value = weechat::buffer_get_string($iset_buffer, "localvar_iset_search_value"); - $filter = $new_filter if ($new_filter ne ""); - } - if ($iset_buffer ne "") - { - weechat::buffer_set($iset_buffer, "type", "free"); - iset_title(); - weechat::buffer_set($iset_buffer, "key_bind_ctrl-L", "/iset **refresh"); - weechat::buffer_set($iset_buffer, "key_bind_meta2-A", "/iset **up"); - weechat::buffer_set($iset_buffer, "key_bind_meta2-B", "/iset **down"); - weechat::buffer_set($iset_buffer, "key_bind_meta2-23~", "/iset **left"); - weechat::buffer_set($iset_buffer, "key_bind_meta2-24~" , "/iset **right"); - weechat::buffer_set($iset_buffer, "key_bind_meta- ", "/iset **toggle"); - weechat::buffer_set($iset_buffer, "key_bind_meta-+", "/iset **incr"); - weechat::buffer_set($iset_buffer, "key_bind_meta--", "/iset **decr"); - weechat::buffer_set($iset_buffer, "key_bind_meta-imeta-r", "/iset **reset"); - weechat::buffer_set($iset_buffer, "key_bind_meta-imeta-u", "/iset **unset"); - weechat::buffer_set($iset_buffer, "key_bind_meta-ctrl-J", "/iset **set"); - weechat::buffer_set($iset_buffer, "key_bind_meta-ctrl-M", "/iset **set"); - weechat::buffer_set($iset_buffer, "key_bind_meta-meta2-1~", "/iset **scroll_top"); - weechat::buffer_set($iset_buffer, "key_bind_meta-meta2-4~", "/iset **scroll_bottom"); - weechat::buffer_set($iset_buffer, "key_bind_meta-v", "/iset **toggle_help"); - weechat::buffer_set($iset_buffer, "key_bind_meta-p", "/iset **toggle_show_plugin_desc"); - weechat::buffer_set($iset_buffer, "localvar_set_iset_filter", $filter); - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_mode", $search_mode); - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $search_value); - } -} - -sub iset_get_options -{ - my $var_value = $_[0]; - $var_value = "" if (not defined $var_value); - $var_value = lc($var_value); - $search_value = $var_value; - @iset_focus = (); - @options_names = (); - @options_parent_names = (); - @options_types = (); - @options_values = (); - @options_default_values = (); - @options_parent_values = (); - @options_is_null = (); - $option_max_length = 0; - my %options_internal = (); - my $i = 0; - my $key; - my $iset_struct; - my %iset_struct; - - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $var_value) if ($search_mode == 3); - - my $infolist = weechat::infolist_get("option", "", $filter); - while (weechat::infolist_next($infolist)) - { - $key = sprintf("%08d", $i); - my $name = weechat::infolist_string($infolist, "full_name"); - my $parent_name = weechat::infolist_string($infolist, "parent_name"); - next if (weechat::config_boolean($options_iset{"show_plugin_description"}) == 0 and index ($name, "plugins.desc.") != -1); - my $type = weechat::infolist_string($infolist, "type"); - my $value = weechat::infolist_string($infolist, "value"); - my $default_value = weechat::infolist_string($infolist, "default_value"); - my $parent_value; - if ($parent_name && (($wee_version_number < 0x00040300) || (weechat::infolist_search_var($infolist, "parent_value")))) - { - $parent_value = weechat::infolist_string($infolist, "parent_value"); - } - my $is_null = weechat::infolist_integer($infolist, "value_is_null"); - - if ($search_mode == 3) - { - my $value = weechat::infolist_string($infolist, "value"); - if ( grep /\Q$var_value/,lc($value) ) - { - $options_internal{$name}{"parent_name"} = $parent_name; - $options_internal{$name}{"type"} = $type; - $options_internal{$name}{"value"} = $value; - $options_internal{$name}{"default_value"} = $default_value; - $options_internal{$name}{"parent_value"} = $parent_value; - $options_internal{$name}{"is_null"} = $is_null; - $option_max_length = length($name) if (length($name) > $option_max_length); - $iset_struct{$key} = $options_internal{$name}; - push(@iset_focus, $iset_struct{$key}); - } - } - # search for diff? - elsif ( $search_mode == 4 or $search_mode == 5) - { - if ($value ne $default_value ) - { - $options_internal{$name}{"parent_name"} = $parent_name; - $options_internal{$name}{"type"} = $type; - $options_internal{$name}{"value"} = $value; - $options_internal{$name}{"default_value"} = $default_value; - $options_internal{$name}{"parent_value"} = $parent_value; - $options_internal{$name}{"is_null"} = $is_null; - $option_max_length = length($name) if (length($name) > $option_max_length); - $iset_struct{$key} = $options_internal{$name}; - push(@iset_focus, $iset_struct{$key}); - } - } - else - { - $options_internal{$name}{"parent_name"} = $parent_name; - $options_internal{$name}{"type"} = $type; - $options_internal{$name}{"value"} = $value; - $options_internal{$name}{"default_value"} = $default_value; - $options_internal{$name}{"parent_value"} = $parent_value; - $options_internal{$name}{"is_null"} = $is_null; - $option_max_length = length($name) if (length($name) > $option_max_length); - $iset_struct{$key} = $options_internal{$name}; - push(@iset_focus, $iset_struct{$key}); - } - $i++; - } - weechat::infolist_free($infolist); - - foreach my $name (sort keys %options_internal) - { - push(@options_names, $name); - push(@options_parent_names, $options_internal{$name}{"parent_name"}); - push(@options_types, $options_internal{$name}{"type"}); - push(@options_values, $options_internal{$name}{"value"}); - push(@options_default_values, $options_internal{$name}{"default_value"}); - push(@options_parent_values, $options_internal{$name}{"parent_value"}); - push(@options_is_null, $options_internal{$name}{"is_null"}); - } -} - -sub iset_get_values -{ - my $var_value = $_[0]; - $var_value = lc($var_value); - if (substr($var_value,0,1) eq weechat::config_string($options_iset{"value_search_char"}) and $var_value ne weechat::config_string($options_iset{"value_search_char"})) - { - $var_value = substr($var_value,1,length($var_value)); - $search_mode = 0; - } - iset_search_values($var_value,$search_mode); - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_mode", $search_mode); - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $var_value); - $search_value = $var_value; -} -sub iset_search_values -{ - my ($var_value,$search_mode) = ($_[0],$_[1]); - @options_names = (); - @options_parent_names = (); - @options_types = (); - @options_values = (); - @options_default_values = (); - @options_parent_values = (); - @options_is_null = (); - $option_max_length = 0; - my %options_internal = (); - my $i = 0; - my $infolist = weechat::infolist_get("option", "", "*"); - while (weechat::infolist_next($infolist)) - { - my $name = weechat::infolist_string($infolist, "full_name"); - my $parent_name = weechat::infolist_string($infolist, "parent_name"); - next if (weechat::config_boolean($options_iset{"show_plugin_description"}) == 0 and index ($name, "plugins.desc.") != -1); - my $type = weechat::infolist_string($infolist, "type"); - my $is_null = weechat::infolist_integer($infolist, "value_is_null"); - my $value = weechat::infolist_string($infolist, "value"); - my $default_value = weechat::infolist_string($infolist, "default_value"); - my $parent_value; - if ($parent_name && (($wee_version_number < 0x00040300) || (weechat::infolist_search_var($infolist, "parent_value")))) - { - $parent_value = weechat::infolist_string($infolist, "parent_value"); - } - if ($search_mode) - { - if ( grep /\Q$var_value/,lc($value) ) - { - $options_internal{$name}{"parent_name"} = $parent_name; - $options_internal{$name}{"type"} = $type; - $options_internal{$name}{"value"} = $value; - $options_internal{$name}{"default_value"} = $default_value; - $options_internal{$name}{"parent_value"} = $parent_value; - $options_internal{$name}{"is_null"} = $is_null; - $option_max_length = length($name) if (length($name) > $option_max_length); - } - } - else - { -# if ($value =~ /\Q$var_value/si) - if (lc($value) eq $var_value) - { - $options_internal{$name}{"parent_name"} = $parent_name; - $options_internal{$name}{"type"} = $type; - $options_internal{$name}{"value"} = $value; - $options_internal{$name}{"default_value"} = $default_value; - $options_internal{$name}{"parent_value"} = $parent_value; - $options_internal{$name}{"is_null"} = $is_null; - $option_max_length = length($name) if (length($name) > $option_max_length); - } - } - $i++; - } - weechat::infolist_free($infolist); - foreach my $name (sort keys %options_internal) - { - push(@options_names, $name); - push(@options_parent_names, $options_internal{$name}{"parent_name"}); - push(@options_types, $options_internal{$name}{"type"}); - push(@options_values, $options_internal{$name}{"value"}); - push(@options_default_values, $options_internal{$name}{"default_value"}); - push(@options_parent_values, $options_internal{$name}{"parent_value"}); - push(@options_is_null, $options_internal{$name}{"is_null"}); - } -} - -sub iset_refresh_line -{ - if ($iset_buffer ne "") - { - my $y = $_[0]; - if ($y <= $#options_names) - { - return if (! defined($options_types[$y])); - my $format = sprintf("%%s%%s%%s %%s %%-7s %%s %%s%%s%%s"); - my $padding; - if ($wee_version_number >= 0x00040200) - { - $padding = " " x ($option_max_length - weechat::strlen_screen($options_names[$y])); - } - else - { - $padding = " " x ($option_max_length - length($options_names[$y])); - } - my $around = ""; - $around = "\"" if ((!$options_is_null[$y]) && ($options_types[$y] eq "string")); - - my $color1 = weechat::color(weechat::config_color($options_iset{"color_option"})); - my $color2 = weechat::color(weechat::config_color($options_iset{"color_type"})); - my $color3 = ""; - my $color4 = ""; - if ($options_is_null[$y]) - { - $color3 = weechat::color(weechat::config_color($options_iset{"color_value_undef"})); - $color4 = weechat::color(weechat::config_color($options_iset{"color_value"})); - } - elsif ($options_values[$y] ne $options_default_values[$y]) - { - $color3 = weechat::color(weechat::config_color($options_iset{"color_value_diff"})); - } - else - { - $color3 = weechat::color(weechat::config_color($options_iset{"color_value"})); - } - if ($y == $current_line) - { - $color1 = weechat::color(weechat::config_color($options_iset{"color_option_selected"}).",".weechat::config_color($options_iset{"color_bg_selected"})); - $color2 = weechat::color(weechat::config_color($options_iset{"color_type_selected"}).",".weechat::config_color($options_iset{"color_bg_selected"})); - if ($options_is_null[$y]) - { - $color3 = weechat::color(weechat::config_color($options_iset{"color_value_undef_selected"}).",".weechat::config_color($options_iset{"color_bg_selected"})); - $color4 = weechat::color(weechat::config_color($options_iset{"color_value_selected"}).",".weechat::config_color($options_iset{"color_bg_selected"})); - } - elsif ($options_values[$y] ne $options_default_values[$y]) - { - $color3 = weechat::color(weechat::config_color($options_iset{"color_value_diff_selected"}).",".weechat::config_color($options_iset{"color_bg_selected"})); - } - else - { - $color3 = weechat::color(weechat::config_color($options_iset{"color_value_selected"}).",".weechat::config_color($options_iset{"color_bg_selected"})); - } - } - my $value = $options_values[$y]; - if ($options_is_null[$y]) - { - $value = "null"; - if ($options_parent_names[$y]) - { - if (defined $options_parent_values[$y]) - { - my $around_parent = ""; - $around_parent = "\"" if ($options_types[$y] eq "string"); - $value .= $color1." -> ".$color4.$around_parent.$options_parent_values[$y].$around_parent; - } - else - { - $value .= $color1." -> ".$color3."null"; - } - } - } - my $strline = sprintf($format, - $color1, $options_names[$y], $padding, - $color2, $options_types[$y], - $color3, $around, $value, $around); - weechat::print_y($iset_buffer, $y, $strline); - } - } -} - -sub iset_refresh -{ - iset_title(); - if (($iset_buffer ne "") && ($#options_names >= 0)) - { - foreach my $y (0 .. $#options_names) - { - iset_refresh_line($y); - } - } - - weechat::bar_item_update("isetbar_help") if (weechat::config_boolean($options_iset{"show_help_bar"}) == 1); -} - -sub iset_full_refresh -{ - $iset_buffer = weechat::buffer_search($LANG, $PRGNAME); - if ($iset_buffer ne "") - { - weechat::buffer_clear($iset_buffer) unless defined $_[0]; # iset_full_refresh(1) does a full refresh without clearing buffer - # search for "*" in $filter. - if ($filter =~ m/\*/ and $search_mode == 2) - { - iset_get_options(""); - } - else - { - if ($search_mode == 0) - { - $search_value = "=" . $search_value; - iset_get_values($search_value); - } - elsif ($search_mode == 1) - { - iset_get_values($search_value); - } - elsif ($search_mode == 3) - { - iset_create_filter($filter); - iset_get_options($search_value); - } - } - if (weechat::config_boolean($options_iset{"show_plugin_description"}) == 1) - { - iset_set_current_line($current_line); - }else - { - $current_line = $#options_names if ($current_line > $#options_names); - } - iset_refresh(); - weechat::command($iset_buffer, "/window refresh"); - } -} - -sub iset_set_current_line -{ - my $new_current_line = $_[0]; - if ($new_current_line >= 0) - { - my $old_current_line = $current_line; - $current_line = $new_current_line; - $current_line = $#options_names if ($current_line > $#options_names); - if ($old_current_line != $current_line) - { - iset_refresh_line($old_current_line); - iset_refresh_line($current_line); - weechat::bar_item_update("isetbar_help") if (weechat::config_boolean($options_iset{"show_help_bar"}) == 1); - } - } -} - -sub iset_signal_window_scrolled_cb -{ - my ($data, $signal, $signal_data) = ($_[0], $_[1], $_[2]); - if ($iset_buffer ne "") - { - my $infolist = weechat::infolist_get("window", $signal_data, ""); - if (weechat::infolist_next($infolist)) - { - if (weechat::infolist_pointer($infolist, "buffer") eq $iset_buffer) - { - my $old_current_line = $current_line; - my $new_current_line = $current_line; - my $start_line_y = weechat::infolist_integer($infolist, "start_line_y"); - my $chat_height = weechat::infolist_integer($infolist, "chat_height"); - $new_current_line += $chat_height if ($new_current_line < $start_line_y); - $new_current_line -= $chat_height if ($new_current_line >= $start_line_y + $chat_height); - $new_current_line = $start_line_y if ($new_current_line < $start_line_y); - $new_current_line = $start_line_y + $chat_height - 1 if ($new_current_line >= $start_line_y + $chat_height); - iset_set_current_line($new_current_line); - } - } - weechat::infolist_free($infolist); - } - - return weechat::WEECHAT_RC_OK; -} - -sub iset_get_window_number -{ - if ($iset_buffer ne "") - { - my $window = weechat::window_search_with_buffer($iset_buffer); - return "-window ".weechat::window_get_integer ($window, "number")." " if ($window ne ""); - } - return ""; -} - -sub iset_check_line_outside_window -{ - if ($iset_buffer ne "") - { - undef my $infolist; - if ($wee_version_number >= 0x00030500) - { - my $window = weechat::window_search_with_buffer($iset_buffer); - $infolist = weechat::infolist_get("window", $window, "") if $window; - } - else - { - $infolist = weechat::infolist_get("window", "", "current"); - } - if ($infolist) - { - if (weechat::infolist_next($infolist)) - { - my $start_line_y = weechat::infolist_integer($infolist, "start_line_y"); - my $chat_height = weechat::infolist_integer($infolist, "chat_height"); - my $window_number = ""; - if ($wee_version_number >= 0x00030500) - { - $window_number = "-window ".weechat::infolist_integer($infolist, "number")." "; - } - if ($start_line_y > $current_line) - { - weechat::command($iset_buffer, "/window scroll ".$window_number."-".($start_line_y - $current_line)); - } - else - { - if ($start_line_y <= $current_line - $chat_height) - { - weechat::command($iset_buffer, "/window scroll ".$window_number."+".($current_line - $start_line_y - $chat_height + 1)); - - } - } - } - weechat::infolist_free($infolist); - } - } -} - -sub iset_get_option_name_index -{ - my $option_name = $_[0]; - my $index = 0; - while ($index <= $#options_names) - { - return -1 if ($options_names[$index] gt $option_name); - return $index if ($options_names[$index] eq $option_name); - $index++; - } - return -1; -} - -sub iset_refresh_option -{ - my $option_name = $_[0]; - my $index = $_[1]; - my $infolist = weechat::infolist_get("option", "", $option_name); - if ($infolist) - { - weechat::infolist_next($infolist); - if (weechat::infolist_fields($infolist)) - { - $options_parent_names[$index] = weechat::infolist_string($infolist, "parent_name"); - $options_types[$index] = weechat::infolist_string($infolist, "type"); - $options_values[$index] = weechat::infolist_string($infolist, "value"); - $options_default_values[$index] = weechat::infolist_string($infolist, "default_value"); - $options_is_null[$index] = weechat::infolist_integer($infolist, "value_is_null"); - $options_parent_values[$index] = undef; - if ($options_parent_names[$index] - && (($wee_version_number < 0x00040300) || (weechat::infolist_search_var($infolist, "parent_value")))) - { - $options_parent_values[$index] = weechat::infolist_string($infolist, "parent_value"); - } - iset_refresh_line($index); - iset_title() if ($option_name eq "iset.look.show_current_line"); - } - else - { - iset_full_refresh(1); # if not found, refresh fully without clearing buffer - weechat::print_y($iset_buffer, $#options_names + 1, ""); - } - weechat::infolist_free($infolist); - } -} - -sub iset_config_cb -{ - my ($data, $option_name, $value) = ($_[0], $_[1], $_[2]); - - if ($iset_buffer ne "") - { - return weechat::WEECHAT_RC_OK if (weechat::info_get("weechat_upgrading", "") eq "1"); - - my $index = iset_get_option_name_index($option_name); - if ($index >= 0) - { - # refresh info about changed option - iset_refresh_option($option_name, $index); - # refresh any other option having this changed option as parent - foreach my $i (0 .. $#options_names) - { - if ($options_parent_names[$i] eq $option_name) - { - iset_refresh_option($options_names[$i], $i); - } - } - } - else - { - iset_full_refresh() if ($option_name ne "weechat.bar.isetbar.hidden"); - } - } - - return weechat::WEECHAT_RC_OK; -} - -sub iset_set_option -{ - my ($option, $value) = ($_[0],$_[1]); - if (defined $option and defined $value) - { - $option = weechat::config_get($option); - weechat::config_option_set($option, $value, 1) if ($option ne ""); - } -} - -sub iset_reset_option -{ - my $option = $_[0]; - if (defined $option) - { - $option = weechat::config_get($option); - weechat::config_option_reset($option, 1) if ($option ne ""); - } -} - -sub iset_unset_option -{ - my $option = $_[0]; - if (defined $option) - { - $option = weechat::config_get($option); - weechat::config_option_unset($option) if ($option ne ""); - } -} - - -sub iset_cmd_cb -{ - my ($data, $buffer, $args) = ($_[0], $_[1], $_[2]); - my $filter_set = 0; -# $search_value = ""; - if (($args ne "") && (substr($args, 0, 2) ne "**")) - { - my @cmd_array = split(/ /,$args); - my $array_count = @cmd_array; - if (substr($args, 0, 1) eq weechat::config_string($options_iset{"value_search_char"}) - or (defined $cmd_array[0] and $cmd_array[0] eq weechat::config_string($options_iset{"value_search_char"}).weechat::config_string($options_iset{"value_search_char"})) ) - { - $search_mode = 1; - my $search_value = substr($args, 1); # cut value_search_char - if ($iset_buffer ne "") - { - weechat::buffer_clear($iset_buffer); - weechat::command($iset_buffer, "/window refresh"); - } - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_mode", $search_mode); - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $search_value); - iset_init(); - iset_get_values($search_value); - iset_refresh(); - weechat::buffer_set($iset_buffer, "display", "1"); -# $filter = $var_value; - return weechat::WEECHAT_RC_OK; - } - else - { - # f/s option =value - # option =value - $search_mode = 2; # grep on option - if ( $array_count >= 2 and $cmd_array[0] ne "f" or $cmd_array[0] ne "s") - { - if ( defined $cmd_array[1] and substr($cmd_array[1], 0, 1) eq weechat::config_string($options_iset{"value_search_char"}) - or defined $cmd_array[2] and substr($cmd_array[2], 0, 1) eq weechat::config_string($options_iset{"value_search_char"}) ) - { - $search_mode = 3; # grep on option and value - $search_value = substr($cmd_array[1], 1); # cut value_search_char - $search_value = substr($cmd_array[2], 1) if ( $array_count > 2); # cut value_search_char - } - } - - # show all diff values - if ( $args eq "d") - { - $search_mode = 4; - $search_value = "*"; - $args = $search_value; - } - if ( $array_count >= 2 and $cmd_array[0] eq "d") - { - $search_mode = 5; - $search_value = substr($cmd_array[1], 0); # cut value_search_char - $search_value = substr($cmd_array[2], 0) if ( $array_count > 2); # cut value_search_char - $args = $search_value; - } - - iset_create_filter($args); - $filter_set = 1; - my $ptrbuf = weechat::buffer_search($LANG, $PRGNAME); - - if ($ptrbuf eq "") - { - iset_init(); - iset_get_options($search_value); - iset_full_refresh(); - weechat::buffer_set(weechat::buffer_search($LANG, $PRGNAME), "display", "1"); - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $search_value); - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_mode", $search_mode); - return weechat::WEECHAT_RC_OK; - } - else - { - iset_get_options($search_value); - iset_full_refresh(); - weechat::buffer_set($ptrbuf, "display", "1"); - } - } - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_mode", $search_mode); - weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $search_value); - } - if ($iset_buffer eq "") - { - iset_init(); - iset_get_options(""); - iset_refresh(); - } - else - { -# iset_get_options($search_value); - iset_full_refresh() if ($filter_set); - } - - if ($args eq "") - { - weechat::buffer_set($iset_buffer, "display", "1"); - } - else - { - if ($args eq "**refresh") - { - iset_full_refresh(); - } - if ($args eq "**up") - { - if ($current_line > 0) - { - $current_line--; - iset_refresh_line($current_line + 1); - iset_refresh_line($current_line); - iset_check_line_outside_window(); - } - } - if ($args eq "**down") - { - if ($current_line < $#options_names) - { - $current_line++; - iset_refresh_line($current_line - 1); - iset_refresh_line($current_line); - iset_check_line_outside_window(); - } - } - if ($args eq "**left" && $wee_version_number >= 0x00030600) - { - weechat::command($iset_buffer, "/window scroll_horiz ".iset_get_window_number()."-".weechat::config_integer($options_iset{"scroll_horiz"})."%"); - } - if ($args eq "**right" && $wee_version_number >= 0x00030600) - { - weechat::command($iset_buffer, "/window scroll_horiz ".iset_get_window_number().weechat::config_integer($options_iset{"scroll_horiz"})."%"); - } - if ($args eq "**scroll_top") - { - my $old_current_line = $current_line; - $current_line = 0; - iset_refresh_line ($old_current_line); - iset_refresh_line ($current_line); - iset_title(); - weechat::command($iset_buffer, "/window scroll_top ".iset_get_window_number()); - } - if ($args eq "**scroll_bottom") - { - my $old_current_line = $current_line; - $current_line = $#options_names; - iset_refresh_line ($old_current_line); - iset_refresh_line ($current_line); - iset_title(); - weechat::command($iset_buffer, "/window scroll_bottom ".iset_get_window_number()); - } - if ($args eq "**toggle") - { - if ($options_types[$current_line] eq "boolean") - { - iset_set_option($options_names[$current_line], "toggle"); - } - } - if ($args eq "**incr") - { - if (($options_types[$current_line] eq "integer") - || ($options_types[$current_line] eq "color")) - { - iset_set_option($options_names[$current_line], "++1"); - } - } - if ($args eq "**decr") - { - if (($options_types[$current_line] eq "integer") - || ($options_types[$current_line] eq "color")) - { - iset_set_option($options_names[$current_line], "--1"); - } - } - if ($args eq "**reset") - { - iset_reset_option($options_names[$current_line]); - } - if ($args eq "**unset") - { - iset_unset_option($options_names[$current_line]); - } - if ($args eq "**toggle_help") - { - if (weechat::config_boolean($options_iset{"show_help_bar"}) == 1) - { - weechat::config_option_set($options_iset{"show_help_bar"},0,1); - iset_show_bar(0); - } - else - { - weechat::config_option_set($options_iset{"show_help_bar"},1,1); - iset_show_bar(1); - } - } - if ($args eq "**toggle_show_plugin_desc") - { - if (weechat::config_boolean($options_iset{"show_plugin_description"}) == 1) - { - weechat::config_option_set($options_iset{"show_plugin_description"},0,1); - iset_full_refresh(); - iset_check_line_outside_window(); - iset_title(); - } - else - { - weechat::config_option_set($options_iset{"show_plugin_description"},1,1); - iset_full_refresh(); - iset_check_line_outside_window(); - iset_title(); - } - } - if ($args eq "**set") - { - my $quote = ""; - my $value = $options_values[$current_line]; - if ($options_is_null[$current_line]) - { - $value = ""; - } - else - { - $quote = "\"" if ($options_types[$current_line] eq "string"); - } - $value = " ".$quote.$value.$quote if ($value ne "" or $quote ne ""); - - my $set_command = "/set"; - my $start_index = 5; - if (weechat::config_boolean($options_iset{"use_mute"}) == 1) - { - $set_command = "/mute ".$set_command; - $start_index += 11; - } - $set_command = $set_command." ".$options_names[$current_line].$value; - my $pos_space = index($set_command, " ", $start_index); - if ($pos_space < 0) - { - $pos_space = 9999; - } - else - { - $pos_space = $pos_space + 1; - $pos_space = $pos_space + 1 if ($quote ne ""); - } - weechat::buffer_set($iset_buffer, "input", $set_command); - weechat::buffer_set($iset_buffer, "input_pos", "".$pos_space); - } - } - weechat::bar_item_update("isetbar_help") if (weechat::config_boolean($options_iset{"show_help_bar"}) == 1); - return weechat::WEECHAT_RC_OK; -} - -sub iset_get_help -{ - my ($redraw) = ($_[0]); - - return '' if (weechat::config_boolean($options_iset{"show_help_bar"}) == 0); - - if (not defined $options_names[$current_line]) - { - return "No option selected. Set a new filter using command line (use '*' to see all options)"; - } - if ($options_name_copy eq $options_names[$current_line] and not defined $redraw) - { - return $description; - } - $options_name_copy = $options_names[$current_line]; - my $optionlist =""; - $optionlist = weechat::infolist_get("option", "", $options_names[$current_line]); - weechat::infolist_next($optionlist); - my $full_name = weechat::infolist_string($optionlist,"full_name"); - my $option_desc = ""; - my $option_default_value = ""; - my $option_range = ""; - my $possible_values = ""; - my $re = qq(\Q$full_name); - if (grep (/^$re$/,$options_names[$current_line])) - { - $option_desc = weechat::infolist_string($optionlist, "description_nls"); - $option_desc = weechat::infolist_string($optionlist, "description") if ($option_desc eq ""); - $option_desc = "No help found" if ($option_desc eq ""); - $option_default_value = weechat::infolist_string($optionlist, "default_value"); - $possible_values = weechat::infolist_string($optionlist, "string_values") if (weechat::infolist_string($optionlist, "string_values") ne ""); - if ((weechat::infolist_string($optionlist, "type") eq "integer") && ($possible_values eq "")) - { - $option_range = weechat::infolist_integer($optionlist, "min") - ." .. ".weechat::infolist_integer($optionlist, "max"); - } - } - weechat::infolist_free($optionlist); - iset_title(); - - $description = weechat::color(weechat::config_color($options_iset{"color_help_option_name"})).$options_names[$current_line] - .weechat::color("bar_fg").": " - .weechat::color(weechat::config_color($options_iset{"color_help_text"})).$option_desc; - - # show additional infos like default value and possible values - - if (weechat::config_boolean($options_iset{"show_help_extra_info"}) == 1) - { - $description .= - weechat::color("bar_delim")." [" - .weechat::color("bar_fg")."default: " - .weechat::color("bar_delim")."\"" - .weechat::color(weechat::config_color($options_iset{"color_help_default_value"})).$option_default_value - .weechat::color("bar_delim")."\""; - if ($option_range ne "") - { - $description .= weechat::color("bar_fg").", values: ".$option_range; - } - if ($possible_values ne "") - { - $possible_values =~ s/\|/", "/g; # replace '|' to '", "' - $description .= weechat::color("bar_fg").", values: ". "\"" . $possible_values . "\""; - - } - $description .= weechat::color("bar_delim")."]"; - } - return $description; -} - -sub iset_check_condition_isetbar_cb -{ - my ($data, $modifier, $modifier_data, $string) = ($_[0], $_[1], $_[2], $_[3]); - my $buffer = weechat::window_get_pointer($modifier_data, "buffer"); - if ($buffer ne "") - { - if ((weechat::buffer_get_string($buffer, "plugin") eq $LANG) - && (weechat::buffer_get_string($buffer, "name") eq $PRGNAME)) - { - return "1"; - } - } - return "0"; -} - -sub iset_show_bar -{ - my $show = $_[0]; - my $barhidden = weechat::config_get("weechat.bar.isetbar.hidden"); - if ($barhidden) - { - if ($show) - { - if (weechat::config_boolean($options_iset{"show_help_bar"}) == 1) - { - if (weechat::config_boolean($barhidden)) - { - weechat::config_option_set($barhidden, 0, 1); - } - } - } - else - { - if (!weechat::config_boolean($barhidden)) - { - weechat::config_option_set($barhidden, 1, 1); - } - } - } -} - -sub iset_signal_buffer_switch_cb -{ - my $buffer_pointer = $_[2]; - my $show_bar = 0; - $show_bar = 1 if (weechat::buffer_get_integer($iset_buffer, "num_displayed") > 0); - iset_show_bar($show_bar); - iset_check_line_outside_window() if ($buffer_pointer eq $iset_buffer); - return weechat::WEECHAT_RC_OK; -} - -sub iset_item_cb -{ - return iset_get_help(); -} - -sub iset_upgrade_ended -{ - iset_full_refresh(); -} - -sub iset_end -{ - # when script is unloaded, we hide bar - iset_show_bar(0); -} - -# -------------------------------[ mouse support ]------------------------------------- - -sub hook_focus_iset_cb -{ - my %info = %{$_[1]}; - my $bar_item_line = int($info{"_bar_item_line"}); - undef my $hash; - if (($info{"_buffer_name"} eq $PRGNAME) && $info{"_buffer_plugin"} eq $LANG && ($bar_item_line >= 0) && ($bar_item_line <= $#iset_focus)) - { - $hash = $iset_focus[$bar_item_line]; - } - else - { - $hash = {}; - my $hash_focus = $iset_focus[0]; - foreach my $key (keys %$hash_focus) - { - $hash->{$key} = "?"; - } - } - return $hash; -} - -# _chat_line_y contains selected line -sub iset_hsignal_mouse_cb -{ - my ($data, $signal, %hash) = ($_[0], $_[1], %{$_[2]}); - - return weechat::WEECHAT_RC_OK unless (@options_types); - - if ($hash{"_buffer_name"} eq $PRGNAME && ($hash{"_buffer_plugin"} eq $LANG)) - { - if ($hash{"_key"} eq "button1") - { - iset_set_current_line($hash{"_chat_line_y"}); - } - elsif ($hash{"_key"} eq "button2") - { - if ($options_types[$hash{"_chat_line_y"}] eq "boolean") - { - iset_set_option($options_names[$hash{"_chat_line_y"}], "toggle"); - iset_set_current_line($hash{"_chat_line_y"}); - } - elsif ($options_types[$hash{"_chat_line_y"}] eq "string") - { - iset_set_current_line($hash{"_chat_line_y"}); - weechat::command("", "/$PRGNAME **set"); - } - } - elsif ($hash{"_key"} eq "button2-gesture-left" or $hash{"_key"} eq "button2-gesture-left-long") - { - if ($options_types[$hash{"_chat_line_y"}] eq "integer" or ($options_types[$hash{"_chat_line_y"}] eq "color")) - { - iset_set_current_line($hash{"_chat_line_y"}); - my $distance = distance($hash{"_chat_line_x"},$hash{"_chat_line_x2"}); - weechat::command("", "/repeat $distance /$PRGNAME **decr"); - } - } - elsif ($hash{"_key"} eq "button2-gesture-right" or $hash{"_key"} eq "button2-gesture-right-long") - { - if ($options_types[$hash{"_chat_line_y"}] eq "integer" or ($options_types[$hash{"_chat_line_y"}] eq "color")) - { - iset_set_current_line($hash{"_chat_line_y"}); - my $distance = distance($hash{"_chat_line_x"},$hash{"_chat_line_x2"}); - weechat::command("", "/repeat $distance /$PRGNAME **incr"); - } - } - } - window_switch(); -} - -sub window_switch -{ - my $current_window = weechat::current_window(); - my $dest_window = weechat::window_search_with_buffer(weechat::buffer_search("perl","iset")); - return 0 if ($dest_window eq "" or $current_window eq $dest_window); - - my $infolist = weechat::infolist_get("window", $dest_window, ""); - weechat::infolist_next($infolist); - my $number = weechat::infolist_integer($infolist, "number"); - weechat::infolist_free($infolist); - weechat::command("","/window " . $number); -} - -sub distance -{ - my ($x1,$x2) = ($_[0], $_[1]); - my $distance; - $distance = $x1 - $x2; - $distance = abs($distance); - if ($distance > 0) - { - use integer; - $distance = $distance / 3; - $distance = 1 if ($distance == 0); - } - elsif ($distance == 0) - { - $distance = 1; - } - return $distance; -} - -# -----------------------------------[ config ]--------------------------------------- - -sub iset_config_init -{ - $iset_config_file = weechat::config_new($ISET_CONFIG_FILE_NAME,"iset_config_reload_cb",""); - return if ($iset_config_file eq ""); - - # section "color" - my $section_color = weechat::config_new_section($iset_config_file,"color", 0, 0, "", "", "", "", "", "", "", "", "", ""); - if ($section_color eq "") - { - weechat::config_free($iset_config_file); - return; - } - $options_iset{"color_option"} = weechat::config_new_option( - $iset_config_file, $section_color, - "option", "color", "Color for option name in iset buffer", "", 0, 0, - "default", "default", 0, "", "", "full_refresh_cb", "", "", ""); - $options_iset{"color_option_selected"} = weechat::config_new_option( - $iset_config_file, $section_color, - "option_selected", "color", "Color for selected option name in iset buffer", "", 0, 0, - "white", "white", 0, "", "", "full_refresh_cb", "", "", ""); - $options_iset{"color_type"} = weechat::config_new_option( - $iset_config_file, $section_color, - "type", "color", "Color for option type (integer, boolean, string)", "", 0, 0, - "brown", "brown", 0, "", "", "full_refresh_cb", "", "", ""); - $options_iset{"color_type_selected"} = weechat::config_new_option( - $iset_config_file, $section_color, - "type_selected", "color", "Color for selected option type (integer, boolean, string)", "", 0, 0, - "yellow", "yellow", 0, "", "", "full_refresh_cb", "", "", ""); - $options_iset{"color_value"} = weechat::config_new_option( - $iset_config_file, $section_color, - "value", "color", "Color for option value", "", 0, 0, - "cyan", "cyan", 0, "", "", "full_refresh_cb", "", "", ""); - $options_iset{"color_value_selected"} = weechat::config_new_option( - $iset_config_file, $section_color, - "value_selected", "color", "Color for selected option value", "", 0, 0, - "lightcyan", "lightcyan", 0, "", "", "full_refresh_cb", "", "", ""); - $options_iset{"color_value_diff"} = weechat::config_new_option( - $iset_config_file, $section_color, - "value_diff", "color", "Color for option value different from default", "", 0, 0, - "magenta", "magenta", 0, "", "", "full_refresh_cb", "", "", ""); - $options_iset{"color_value_diff_selected"} = weechat::config_new_option( - $iset_config_file, $section_color, - "value_diff_selected", "color", "Color for selected option value different from default", "", 0, 0, - "lightmagenta", "lightmagenta", 0, "", "", "full_refresh_cb", "", "", ""); - $options_iset{"color_value_undef"} = weechat::config_new_option( - $iset_config_file, $section_color, - "value_undef", "color", "Color for option value undef", "", 0, 0, - "green", "green", 0, "", "", "full_refresh_cb", "", "", ""); - $options_iset{"color_value_undef_selected"} = weechat::config_new_option( - $iset_config_file, $section_color, - "value_undef_selected", "color", "Color for selected option value undef", "", 0, 0, - "lightgreen", "lightgreen", 0, "", "", "full_refresh_cb", "", "", ""); - $options_iset{"color_bg_selected"} = weechat::config_new_option( - $iset_config_file, $section_color, - "bg_selected", "color", "Background color for current selected option", "", 0, 0, - "red", "red", 0, "", "", "full_refresh_cb", "", "", ""); - $options_iset{"color_help_option_name"} = weechat::config_new_option( - $iset_config_file, $section_color, - "help_option_name", "color", "Color for option name in help-bar", "", 0, 0, - "white", "white", 0, "", "", "bar_refresh", "", "", ""); - $options_iset{"color_help_text"} = weechat::config_new_option( - $iset_config_file, $section_color, - "help_text", "color", "Color for option description in help-bar", "", 0, 0, - "default", "default", 0, "", "", "bar_refresh", "", "", ""); - $options_iset{"color_help_default_value"} = weechat::config_new_option( - $iset_config_file, $section_color, - "help_default_value", "color", "Color for default option value in help-bar", "", 0, 0, - "green", "green", 0, "", "", "bar_refresh", "", "", ""); - - # section "help" - my $section_help = weechat::config_new_section($iset_config_file,"help", 0, 0, "", "", "", "", "", "", "", "", "", ""); - if ($section_help eq "") - { - weechat::config_free($iset_config_file); - return; - } - $options_iset{"show_help_bar"} = weechat::config_new_option( - $iset_config_file, $section_help, - "show_help_bar", "boolean", "Show help bar", "", 0, 0, - "on", "on", 0, "", "", "toggle_help_cb", "", "", ""); - $options_iset{"show_help_extra_info"} = weechat::config_new_option( - $iset_config_file, $section_help, - "show_help_extra_info", "boolean", "Show additional information in help bar (default value, max./min. value) ", "", 0, 0, - "on", "on", 0, "", "", "", "", "", ""); - $options_iset{"show_plugin_description"} = weechat::config_new_option( - $iset_config_file, $section_help, - "show_plugin_description", "boolean", "Show plugin description in iset buffer", "", 0, 0, - "off", "off", 0, "", "", "full_refresh_cb", "", "", ""); - - # section "look" - my $section_look = weechat::config_new_section($iset_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "", ""); - if ($section_look eq "") - { - weechat::config_free($iset_config_file); - return; - } - $options_iset{"value_search_char"} = weechat::config_new_option( - $iset_config_file, $section_look, - "value_search_char", "string", "Trigger char to tell iset to search for value instead of option (for example: =red)", "", 0, 0, - "=", "=", 0, "", "", "", "", "", ""); - $options_iset{"scroll_horiz"} = weechat::config_new_option( - $iset_config_file, $section_look, - "scroll_horiz", "integer", "scroll content of iset buffer n%", "", 1, 100, - "10", "10", 0, "", "", "", "", "", ""); - $options_iset{"show_current_line"} = weechat::config_new_option( - $iset_config_file, $section_look, - "show_current_line", "boolean", "show current line in title bar.", "", 0, 0, - "on", "on", 0, "", "", "", "", "", ""); - $options_iset{"use_mute"} = weechat::config_new_option( - $iset_config_file, $section_look, - "use_mute", "boolean", "/mute command will be used in input bar", "", 0, 0, - "off", "off", 0, "", "", "", "", "", ""); -} - -sub iset_config_reload_cb -{ - my ($data,$config_file) = ($_[0], $_[1]); - return weechat::config_reload($config_file) -} - -sub iset_config_read -{ - return weechat::config_read($iset_config_file) if ($iset_config_file ne ""); -} - -sub iset_config_write -{ - return weechat::config_write($iset_config_file) if ($iset_config_file ne ""); -} - -sub full_refresh_cb -{ - iset_full_refresh(); - return weechat::WEECHAT_RC_OK; -} - -sub bar_refresh -{ - iset_get_help(1); - weechat::bar_item_update("isetbar_help") if (weechat::config_boolean($options_iset{"show_help_bar"}) == 1); - return weechat::WEECHAT_RC_OK; -} - -sub toggle_help_cb -{ - my $value = weechat::config_boolean($options_iset{"show_help_bar"}); - iset_show_bar($value); - return weechat::WEECHAT_RC_OK; -} - -# -----------------------------------[ main ]----------------------------------------- - -weechat::register($PRGNAME, $AUTHOR, $VERSION, $LICENSE, - $DESCR, "iset_end", ""); - -$wee_version_number = weechat::info_get("version_number", "") || 0; - -iset_config_init(); -iset_config_read(); - -weechat::hook_command($PRGNAME, "Interactive set", "d || f || s
|| [=][=]", - "d : show only changed options\n". - "f file : show options for a file\n". - "s section: show options for a section\n". - "text : show options with 'text' in name\n". - weechat::config_string($options_iset{"value_search_char"})."text : show options with 'text' in value\n". - weechat::config_string($options_iset{"value_search_char"}).weechat::config_string($options_iset{"value_search_char"})."text : show options with exact 'text' in value\n\n". - "Keys for iset buffer:\n". - "f11,f12 : move iset content left/right\n". - "up,down : move one option up/down\n". - "pgup,pdwn : move one page up/down\n". - "home,end : move to first/last option\n". - "ctrl+'L' : refresh options and screen\n". - "alt+space : toggle boolean on/off\n". - "alt+'+' : increase value (for integer or color)\n". - "alt+'-' : decrease value (for integer or color)\n". - "alt+'i',alt+'r': reset value of option\n". - "alt+'i',alt+'u': unset option\n". - "alt+enter : set new value for option (edit it with command line)\n". - "text,enter : set a new filter using command line (use '*' to see all options)\n". - "alt+'v' : toggle help bar on/off\n". - "alt+'p' : toggle option \"show_plugin_description\" on/off\n". - "q : as input in iset buffer to close it\n". - "\n". - "Mouse actions:\n". - "wheel up/down : move cursor up/down\n". - "left button : select an option from list\n". - "right button : toggle boolean (on/off) or set a new value for option (edit it with command line)\n". - "right button + drag left/right: increase/decrease value (for integer or color)\n". - "\n". - "Examples:\n". - " show changed options in 'aspell' plugin\n". - " /iset d aspell\n". - " show options for file 'irc'\n". - " /iset f irc\n". - " show options for section 'look'\n". - " /iset s look\n". - " show all options with text 'nicklist' in name\n". - " /iset nicklist\n". - " show all values which contain 'red'. ('" . weechat::config_string($options_iset{"value_search_char"}) . "' is a trigger char).\n". - " /iset ". weechat::config_string($options_iset{"value_search_char"}) ."red\n". - " show all values which hit 'off'. ('" . weechat::config_string($options_iset{"value_search_char"}) . weechat::config_string($options_iset{"value_search_char"}) . "' is a trigger char).\n". - " /iset ". weechat::config_string($options_iset{"value_search_char"}) . weechat::config_string($options_iset{"value_search_char"}) ."off\n". - " show options for file 'weechat' which contains value 'off'\n". - " /iset f weechat ".weechat::config_string($options_iset{"value_search_char"})."off\n". - "", - "", "iset_cmd_cb", ""); -weechat::hook_signal("upgrade_ended", "iset_upgrade_ended", ""); -weechat::hook_signal("window_scrolled", "iset_signal_window_scrolled_cb", ""); -weechat::hook_signal("buffer_switch", "iset_signal_buffer_switch_cb",""); -weechat::bar_item_new("isetbar_help", "iset_item_cb", ""); -weechat::bar_new("isetbar", "on", "0", "window", "", "top", "horizontal", - "vertical", "3", "3", "default", "cyan", "default", "1", - "isetbar_help"); -weechat::hook_modifier("bar_condition_isetbar", "iset_check_condition_isetbar_cb", ""); -weechat::hook_config("*", "iset_config_cb", ""); -$iset_buffer = weechat::buffer_search($LANG, $PRGNAME); -iset_init() if ($iset_buffer ne ""); - -if ($wee_version_number >= 0x00030600) -{ - weechat::hook_focus("chat", "hook_focus_iset_cb", ""); - weechat::hook_hsignal($PRGNAME."_mouse", "iset_hsignal_mouse_cb", ""); - weechat::key_bind("mouse", \%mouse_keys); -} diff --git a/perl/jnotify.pl b/perl/jnotify.pl index 060be7ba..2be0444c 100644 --- a/perl/jnotify.pl +++ b/perl/jnotify.pl @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +# 1.2: add compatibility with WeeChat >= 3.2 (XDG directories) # 1.1: fix: invalid pointer for function infolist_get() # 1.0: added: allow internal WeeChat command(s) # v0.9: fixed: mem leak, infolist not removed with infolist_free() @@ -44,7 +45,7 @@ # /set plugins.var.perl.jnotify.blacklist = "jn-blacklist.txt" # /set plugins.var.perl.jnotify.whitelist = "jn-whitelist.txt" # /set plugins.var.perl.jnotify.block_current_buffer = "on" -# /set plugins.var.perl.jnotify.cmd = "echo -en "\a"" +# /set plugins.var.perl.jnotify.cmd = "echo -en "\a"" # /set plugins.var.perl.jnotify.status = "on" # # Development is currently hosted at @@ -67,7 +68,7 @@ # default values in setup file (~/.weechat/plugins.conf) -my $version = "1.1"; +my $version = "1.2"; my $prgname = "jnotify"; my $description = "starts an internal command or external program if a user or one of your buddies JOIN a channel you are in"; my $status = "status"; @@ -89,7 +90,7 @@ # commands used by jnotify. Type: /help jnotify weechat::hook_command($prgname, $description, - " | | | | | / / / [nick_1 [... nick_n]]", + " | | | | | / / / [nick_1 [... nick_n]]", " $prgname between on and off\n". " tells you if $prgname is on or off\n". @@ -132,7 +133,7 @@ init(); weechat::hook_config( "plugins.var.perl.$prgname.$status", 'toggled_by_set', "" ); -# create hook_signal for IRC command JOIN +# create hook_signal for IRC command JOIN hook() if (weechat::config_get_plugin($status) eq "on"); # return 0 on error @@ -430,12 +431,14 @@ sub init{ if (weechat::config_get_plugin($status) eq ""); if ( weechat::config_get_plugin($whitelist) eq '' ) { - my $wd = weechat::info_get( "weechat_dir", "" ); + my $wd = weechat::info_get("weechat_config_dir", ""); + $wd = weechat::info_get("weechat_dir", "") if (!$wd); $wd =~ s/\/$//; weechat::config_set_plugin($whitelist, $wd . "/" . $default_whitelist ); } if ( weechat::config_get_plugin($blacklist) eq '' ) { - my $wd = weechat::info_get( "weechat_dir", "" ); + my $wd = weechat::info_get("weechat_config_dir", ""); + $wd = weechat::info_get("weechat_dir", "") if (!$wd); $wd =~ s/\/$//; weechat::config_set_plugin($blacklist, $wd . "/" . $default_blacklist ); } diff --git a/perl/jump_smart_closest.pl b/perl/jump_smart_closest.pl new file mode 100644 index 00000000..16616a73 --- /dev/null +++ b/perl/jump_smart_closest.pl @@ -0,0 +1,69 @@ +# jump_smart_closest.pl for WeeChat by arza , distributed freely and without any warranty, licensed under GPL3 + +# Jump to the numerically closest buffer with the highest activity, similar to /input jump_smart (alt-a) but jump to the buffer that has the highest activity level and is next/previous buffer by number regardless of weechat.look.hotlist_sort + +# Changelog: +# 2013-10-22 0.1 initial release +# 2014-10-23 0.2 don't try to change to current buffer +# 2018-07-07 0.3 reverse direction jumping, new commands /jump_smart_previous and /jump_smart_next, renamed from jump_smart_higher.pl to jump_smart_closest.pl + +weechat::register('jump_smart_closest', 'arza ', '0.3', 'GPL3', 'Jump to next/previous buffer with highest activity', '', ''); + +weechat::hook_command('jump_smart_higher', 'See jump_smart_next and jump_smart_previous', '', '', '', 'command_next', ''); # compatibility + +weechat::hook_command('jump_smart_previous', 'Jump to previous buffer with highest activity', '', +'Jump to the buffer that + 1. has the highest activity level + 2. is before current buffer if possible + 3. has the highest number', +'', 'command_previous', ''); + +weechat::hook_command('jump_smart_next', 'Jump to next buffer with activity', '', +'Jump to the buffer that + 1. has the highest activity level + 2. is after current buffer if possible + 3. has the lowest number', +'', 'command_next', ''); + + +sub command_previous { my $buffer=$_[1]; + my $max_priority = 0; + my $max_number = -1000000; + my $current_number = weechat::buffer_get_integer($buffer, 'number'); + my $number = 0; + my $priority = 0; + my $infolist = weechat::infolist_get('hotlist', '', ''); + while(weechat::infolist_next($infolist)){ + $number = weechat::infolist_integer($infolist, 'buffer_number'); + if($number == $current_number){ next; } + $priority = weechat::infolist_integer($infolist, 'priority'); + if($priority > $max_priority){ $max_priority = $priority; $max_number = -1000000; } + elsif($priority < $max_priority){ next; } + if($number > $current_number){ $number -= 10000; } + if($number > $max_number){ $max_number = $number; } + } + weechat::infolist_free($infolist); + + weechat::command($buffer, "/buffer " . $max_number % 10000); +} + +sub command_next { my $buffer=$_[1]; + my $max_priority = 0; + my $min_number = 1000000; + my $current_number = weechat::buffer_get_integer($buffer, 'number'); + my $number = 0; + my $priority = 0; + my $infolist = weechat::infolist_get('hotlist', '', ''); + while(weechat::infolist_next($infolist)){ + $number = weechat::infolist_integer($infolist, 'buffer_number'); + if($number == $current_number){ next; } + $priority = weechat::infolist_integer($infolist, 'priority'); + if($priority > $max_priority){ $max_priority = $priority; $min_number = 1000000; } + elsif($priority < $max_priority){ next; } + if($number < $current_number){ $number += 10000; } + if($number < $min_number){ $min_number = $number; } + } + weechat::infolist_free($infolist); + + weechat::command($buffer, "/buffer " . $min_number % 10000); +} diff --git a/perl/jump_smart_higher.pl b/perl/jump_smart_higher.pl deleted file mode 100644 index d3da7149..00000000 --- a/perl/jump_smart_higher.pl +++ /dev/null @@ -1,36 +0,0 @@ -# jump_smart_higher.pl for WeeChat by arza , distributed freely and without any warranty, licensed under GPL3 - -# Jump to a higher buffer with activity, similar to /input smart_jump (alt-a) but jump to a buffer with higher number if possible - -# Changelog: -# 2013-10-22 0.1 initial release -# 2014-10-23 0.2 don't try to change to current buffer - -weechat::register('jump_smart_higher', 'arza ', '0.2', 'GPL3', 'Jump to a higher buffer with activity', '', ''); -weechat::hook_command('jump_smart_higher', 'Jump to a higher buffer with activity', '', -'Jump to the buffer that - 1. has the highest activity level - 2. is after current buffer if possible - 3. has the lowest number', -'', 'command', ''); - -sub command { my $buffer=$_[1]; - my $max_priority = 0; - my $min_number = 1000000; - my $current_number = weechat::buffer_get_integer($buffer, 'number'); - my $number = 0; - my $priority = 0; - my $infolist = weechat::infolist_get('hotlist', '', ''); - while(weechat::infolist_next($infolist)){ - $number = weechat::infolist_integer($infolist, 'buffer_number'); - if($number == $current_number){ next; } - $priority = weechat::infolist_integer($infolist, 'priority'); - if($priority > $max_priority){ $max_priority = $priority; $min_number = 1000000; } - elsif($priority < $max_priority){ next; } - if($number < $current_number){ $number += 10000; } - if($number < $min_number){ $min_number = $number; } - } - weechat::infolist_free($infolist); - - weechat::command($buffer, "/buffer " . $min_number % 10000); -} diff --git a/perl/launcher.pl b/perl/launcher.pl index 2ced3d28..047d4e10 100644 --- a/perl/launcher.pl +++ b/perl/launcher.pl @@ -22,6 +22,10 @@ # # History: # +# 2017-09-07, Alex Xu (Hello71) : +# version 0.7: properly fix escaping of "signal_data" +# 2017-08-29, Alex Xu (Hello71) : +# version 0.6: fix escaping of "signal_data" # 2011-02-13, Sebastien Helleu : # version 0.5: use new help format for command arguments # 2010-05-29, Sebastien Helleu : @@ -36,7 +40,7 @@ use strict; -my $version = "0.5"; +my $version = "0.7"; my $command_suffix = " &"; weechat::register("launcher", "FlashCode ", $version, "GPL3", @@ -109,9 +113,8 @@ sub signal my $command = weechat::config_get_plugin("signal.$_[1]"); if ($command ne "") { - $signal_data =~ s/([\$`"])/\\$1/g; - $signal_data =~ s/\n/ /g; - $command =~ s/\$signal_data/"$signal_data"/g; + $signal_data =~ s/'/'\\''/g; + $command =~ s/\$signal_data/'$signal_data'/g; system($command.$command_suffix); } return weechat::WEECHAT_RC_OK; diff --git a/perl/luanma.pl b/perl/luanma.pl index 14e4371c..a870159f 100644 --- a/perl/luanma.pl +++ b/perl/luanma.pl @@ -141,7 +141,7 @@ =head2 parser =cut use constant SCRIPT_NAME => 'luanma'; -weechat::register(SCRIPT_NAME, 'Nei ', '0.2', 'GPL3', +weechat::register(SCRIPT_NAME, 'Nei ', '0.3', 'GPL3', 'more flexibility with incoming charset', 'stop_luanma', '') || return; sub SCRIPT_FILE() { my $infolistptr = weechat::infolist_get('perl_script', '', SCRIPT_NAME); @@ -279,7 +279,9 @@ sub get_settings_from_pod { our $nag_tag; our %nag_modifiers; -our $CFG_FILE_NAME = weechat::info_get('weechat_dir', '').weechat::info_get('dir_separator', '').SCRIPT_NAME.'.conf'; +our $weechat_dir = weechat::info_get('weechat_config_dir', ''); +$weechat_dir = weechat::info_get('weechat_dir', '') if (!$weechat_dir); +our $CFG_FILE_NAME = $weechat_dir.weechat::info_get('dir_separator', '').SCRIPT_NAME.'.conf'; our (@CFG_TABLE, @CFG_TABLE_2); our @STO = (\(our (%BYTE_MSGS, %ESC_MSG, %MSG_TIME, %MSG_BUF, %MSG_NICK, %MSG_ENC, %MSG_FLT, %MSG_COLOR))); diff --git a/perl/mass_hl_blocker.pl b/perl/mass_hl_blocker.pl index 0b50c561..d2c21189 100644 --- a/perl/mass_hl_blocker.pl +++ b/perl/mass_hl_blocker.pl @@ -1,6 +1,10 @@ # Mass highlight blocker for WeeChat by arza , distributed freely and without any warranty, licensed under GPL3 +# History: +# 2020-07-02, Pascal Poitras Dubois : +# v0.3: -add: add a tag, mass_hl, to the message +# -change: remove leading channel membership prefixes (~&@%+) -weechat::register('mass_hl_blocker', 'arza ', '0.1', 'GPL3', 'Block mass highlights', '', ''); +weechat::register('mass_hl_blocker', 'arza ', '0.3', 'GPL3', 'Block mass highlights', '', ''); my $version=weechat::info_get('version_number', '') || 0; @@ -16,19 +20,40 @@ sub block { my $message=$_[3]; - $_[2]=~/(\S+);(\S+)\.(\S+);(\S+)/ || return $message; - my ($plugin, $server, $channel, $tags) = ($1, $2, $3, $4); + my $buffer = ""; + my $tags = ""; + if ($_[2] =~ /0x/) + { + # WeeChat >= 2.9 + $_[2] =~ m/([^;]*);(.*)/; + $buffer = $1; + $tags = $2; + } + else + { + # WeeChat <= 2.8 + $_[2] =~ m/([^;]*);([^;]*);(.*)/; + $buffer = weechat::buffer_search($1, $2); + $tags = $3; + } + my $plugin = weechat::buffer_get_string($buffer, "plugin"); + my $server = weechat::buffer_get_string($buffer, "localvar_server"); + my $channel = weechat::buffer_get_string($buffer, "localvar_channel"); + + return $message if ($server eq "" or $channel eq ""); + index($message, weechat::info_get('irc_nick', $server)) != -1 && index($tags, 'notify_message') != -1 && index($tags, 'no_highlight') == -1 || return $message; my $count=0; foreach my $word (split(' ', $message)){ + $word =~ s/^[~&@%+]//; my $infolist=weechat::infolist_get('irc_nick', '', "$server,$channel,$word"); if($infolist){ $count++; } weechat::infolist_free($infolist); } if($count>=$limit){ - weechat::print_date_tags(weechat::buffer_search($plugin, "$server.$channel"), 0, "$tags,no_highlight", $message); + weechat::print_date_tags(weechat::buffer_search($plugin, "$server.$channel"), 0, "$tags,no_highlight,mass_hl", $message); return ''; } diff --git a/perl/menu.pl b/perl/menu.pl index 4afc1405..5be49138 100644 --- a/perl/menu.pl +++ b/perl/menu.pl @@ -2,9 +2,9 @@ $INC{'Encode/ConfigLocal.pm'}=1; require Encode; -# menu.pl is written by Nei -# and licensed under the under GNU General Public License v3 -# or any later version +# SPDX-FileCopyrightText: 2011-2013 Nei +# +# SPDX-License-Identifier: GPL-3.0-or-later # to read the following docs, you can use "perldoc menu.pl" @@ -251,7 +251,7 @@ =head1 FUNCTION DESCRIPTION =cut use constant SCRIPT_NAME => 'menu'; -our $VERSION = '0.9'; +our $VERSION = '1.0.1'; #$$$ autoloaded{ BEGIN { { package WeeP::Tie::hash_accessor; @@ -661,6 +661,7 @@ =head1 FUNCTION DESCRIPTION } @fields } }; } + # this calls the infolist_free function $infptr->free; !wantarray && @infolist ? ($$I->{o} && @infolist == 1) ? $infolist[0] : \@infolist : @infolist } @@ -1681,24 +1682,42 @@ sub setup_menu_bar { $bar->{items} = '*,main_menu' unless $bar->_infolist->{items} =~ /\bmain_menu\b/; } else { - W->bar_new('main_menu', 'off', 10000, 'root', '', 'top', 'horizontal', 'vertical', - 0, 0, 'gray', 'lightblue', 'darkgray', 'off', '*,main_menu'); + if ((W->info_get('version_number', '') || 0) >= 0x02090000) { + W->bar_new('main_menu', 'off', 10000, 'root', '', 'top', 'horizontal', 'vertical', + 0, 0, 'gray', 'lightblue', 'darkgray', 'darkgray', 'off', '*,main_menu'); + } + else { + W->bar_new('main_menu', 'off', 10000, 'root', '', 'top', 'horizontal', 'vertical', + 0, 0, 'gray', 'lightblue', 'darkgray', 'off', '*,main_menu'); + } } if (my $bar = W->bar_search('sub_menu')) { $bar->{hidden} = 1; $bar->{items} = '*sub_menu' unless $bar->_infolist->{items} =~ /\bsub_menu\b/; } else { - W->bar_new('sub_menu', 'on', 9999, 'root', '', 'top', 'columns_vertical', 'vertical', - 0, 0, 'black', 'lightmagenta', 'gray', 'on', '*sub_menu'); + if ((W->info_get('version_number', '') || 0) >= 0x02090000) { + W->bar_new('sub_menu', 'on', 9999, 'root', '', 'top', 'columns_vertical', 'vertical', + 0, 0, 'black', 'lightmagenta', 'gray', 'gray', 'on', '*sub_menu'); + } + else { + W->bar_new('sub_menu', 'on', 9999, 'root', '', 'top', 'columns_vertical', 'vertical', + 0, 0, 'black', 'lightmagenta', 'gray', 'on', '*sub_menu'); + } } if (my $bar = W->bar_search('menu_help')) { $bar->{hidden} = 1; $bar->{items} = 'menu_help' unless $bar->_infolist->{items} =~ /\bmenu_help\b/; } else { - W->bar_new('menu_help', 'on', 9998, 'root', '', 'top', 'horizontal', 'vertical', - 0, 0, 'darkgray', 'default', 'gray', 'on', 'menu_help'); + if ((W->info_get('version_number', '') || 0) >= 0x02090000) { + W->bar_new('menu_help', 'on', 9998, 'root', '', 'top', 'horizontal', 'vertical', + 0, 0, 'darkgray', 'default', 'gray', 'gray', 'on', 'menu_help'); + } + else { + W->bar_new('menu_help', 'on', 9998, 'root', '', 'top', 'horizontal', 'vertical', + 0, 0, 'darkgray', 'default', 'gray', 'on', 'menu_help'); + } } if (my $bar = W->bar_search('window_popup_menu')) { @@ -1706,8 +1725,14 @@ sub setup_menu_bar { $bar->{items} = '*window_popup_menu' unless $bar->_infolist->{items} =~ /\bwindow_popup_menu\b/; } else { - W->bar_new('window_popup_menu', 'on', 0, 'window', 'active', 'bottom', 'columns_vertical', 'vertical', - 0, 0, 'black', 'lightmagenta', 'gray', 'on', '*window_popup_menu'); + if ((W->info_get('version_number', '') || 0) >= 0x02090000) { + W->bar_new('window_popup_menu', 'on', 0, 'window', 'active', 'bottom', 'columns_vertical', 'vertical', + 0, 0, 'black', 'lightmagenta', 'gray', 'gray', 'on', '*window_popup_menu'); + } + else { + W->bar_new('window_popup_menu', 'on', 0, 'window', 'active', 'bottom', 'columns_vertical', 'vertical', + 0, 0, 'black', 'lightmagenta', 'gray', 'on', '*window_popup_menu'); + } } WC->RC_OK diff --git a/perl/mnick.pl b/perl/mnick.pl index dfea3dfd..50bd15d8 100644 --- a/perl/mnick.pl +++ b/perl/mnick.pl @@ -18,7 +18,7 @@ # This script allows to change your nick on the different networks you are # connected, by appending or removing a suffix to your current nick on the # network, based on the defined mask -# Command: /mnick [suffix] +# Command: /mnick [suffix] [away reason] # * if suffix is set and script enabled on network, will do a # /nick # * if suffix is not set, will do a @@ -27,21 +27,35 @@ # Settings: # * plugins.var.perl.mnick._enabled : on/off # * plugins.var.perl.mnick._mask : default [%s] +# * plugins.var.perl.mnick._away : on/off # # Example # I'm CrazyCat on net1 and net3, GatoLoco on net2 # * plugins.var.perl.mnick.net1_enabled : on # * plugins.var.perl.mnick.net1_mask : [%s] +# * plugins.var.perl.mnick.net1_away : off # * plugins.var.perl.mnick.net2_enabled : on # * plugins.var.perl.mnick.net2_mask : |%s -# * plugins.var.perl.mnick.net3_enabled : off +# * plugins.var.perl.mnick.net1_away : off +# * plugins.var.perl.mnick.net3_enabled : on # * plugins.var.perl.mnick.net3_mask : [%s] -# /mnick AFK +# * plugins.var.perl.mnick.net1_away : on +# /mnick Test +# => CrazyCat[Test] on net1, GatoLoco|Test on net2, CrazyCat on net3 +# => Away status won't change +# /mnick AFK I'm no more here # => CrazyCat[AFK] on net1, GatoLoco|AFK on net2, CrazyCat on net3 +# => I'll be turned away on net2 and net3 with "I'm no more here" reason +# /mnick Test +# => CrazyCat[Test] on net1, GatoLoco|Test on net2, CrazyCat on net3 +# => Away status won't change (keep the previous one) # /mnick # => CrazyCat on net1 and net3, GatoLoco on net2 +# => away status is removed on net2 and net3 # # History: +# 2019-09-12, CrazyCat +# version 0.4 : add an optionnal away reason # 2016-05-23, CrazyCat : # version 0.3 : now, you can use alternate nick without doing # a /mnick before @@ -51,11 +65,12 @@ # 2014-04-01, CrazyCat : # version 0.1 : first official version -weechat::register("mnick", "CrazyCat", "0.3", "GPL", "Multi Nick Changer", "", ""); +weechat::register("mnick", "CrazyCat", "0.4", "GPL", "Multi Nick Changer", "", ""); + weechat::hook_command( "mnick", "Multi Nick Changer", - "mnick [extension]", + "mnick [extension] [away reason]", "", "", "mnick_change", @@ -76,21 +91,33 @@ sub mnick_setup { weechat::config_set_plugin($name."_enabled", "off"); } + if (!weechat::config_is_set_plugin($name."_away")) + { + weechat::config_set_plugin($name."_away", "off"); + } } weechat::infolist_free($infolist); } sub mnick_change { - my ($data, $buffer, $text) = @_; + my ($data, $buffer, $args) = @_; + my $ext; + my @reason; + if ($args ne "") { + ($ext, @reason) = split(" ", $args); + } my $newnick; my $nick; + my @nicks; + my $name; + my $hasreason = @reason; $infolist = weechat::infolist_get("irc_server", "", ""); - if ($text) + if ($ext) { while (weechat::infolist_next($infolist)) { - my $name = weechat::infolist_string($infolist, "name"); + $name = weechat::infolist_string($infolist, "name"); if (weechat::config_is_set_plugin($name."_enabled") && weechat::config_get_plugin($name."_enabled") eq "on" && weechat::infolist_integer($infolist, "is_connected")==1) @@ -100,17 +127,25 @@ sub mnick_change { $nick = weechat::info_get('irc_nick', $name); weechat::config_set_plugin($name."_backnick", $nick); + } else { $nick = weechat::config_get_plugin($name."_backnick"); } - $newnick = sprintf($nick . weechat::config_get_plugin($name."_mask"), $text); + $newnick = sprintf($nick . weechat::config_get_plugin($name."_mask"), $ext); weechat::command($name, "/quote -server ".$name." nick ".$newnick); } + if ( $hasreason != 0 + && weechat::infolist_integer($infolist, "is_connected")==1 + && weechat::config_is_set_plugin($name."_away") + && weechat::config_get_plugin($name."_away") eq "on") + { + weechat::command($name, "/quote -server ".$name. " away :".join(" ", @reason)); + } } } else { while (weechat::infolist_next($infolist)) { - my $name = weechat::infolist_string($infolist, "name"); + $name = weechat::infolist_string($infolist, "name"); $nick = weechat::info_get('irc_nick', $name); if (weechat::config_is_set_plugin($name."_enabled") && weechat::config_get_plugin($name."_enabled") eq "on" @@ -126,6 +161,12 @@ sub mnick_change weechat::command($name, "/quote -server ".$name." nick ".$newnick); weechat::config_set_plugin($name."_backnick", ""); } + if ( weechat::infolist_integer($infolist, "is_connected")==1 + && weechat::config_is_set_plugin($name."_away") + && weechat::config_get_plugin($name."_away") eq "on") + { + weechat::command($name, "/quote -server ".$name. " away"); + } } } weechat::infolist_free($infolist); diff --git a/perl/multiline.pl b/perl/multiline.pl deleted file mode 100644 index 54474d41..00000000 --- a/perl/multiline.pl +++ /dev/null @@ -1,782 +0,0 @@ -use strict; use warnings; -$INC{'Encode/ConfigLocal.pm'}=1; -require Encode; -use utf8; - -# multiline.pl is written by Nei -# and licensed under the under GNU General Public License v3 -# or any later version - -# to read the following docs, you can use "perldoc multiline.pl" - -=head1 NAME - -multiline - Multi-line edit box for WeeChat (weechat edition) - -=head1 DESCRIPTION - -multiline will draw a multi-line edit box to your WeeChat window so -that when you hit the return key, you can first compose a complete -multi-line message before sending it all at once. - -Furthermore, if you have multi-line pastes then you can edit them -before sending out all the lines. - -=head1 USAGE - -make a key binding to send the finished message: - - /key bind meta-s /input return - -then you can send the multi-line message with Alt+S - -=head1 SETTINGS - -the settings are usually found in the - - plugins.var.perl.multiline - -namespace, that is, type - - /set plugins.var.perl.multiline.* - -to see them and - - /set plugins.var.perl.multiline.SETTINGNAME VALUE - -to change a setting C to a new value C. Finally, - - /unset plugins.var.perl.multiline.SETTINGNAME - -will reset a setting to its default value. - -the following settings are available: - -=head2 char - -character(s) which should be displayed to indicate end of line - -=head2 tab - -character(s) which should be displayed instead of Tab key character - -=head2 lead_linebreak - -if turned on, multi-line messages always start on a new line - -=head2 modify_keys - -if turned on, cursor keys are modified so that they respect line -boundaries instead of treating the whole multi-line message as a -single line - -=head2 magic - -indicator displayed when message will be sent soon - -=head2 magic_enter_time - -delay after pressing enter before sending automatically (in ms), or 0 -to disable - -=head2 magic_paste_only - -only use multi-line messages for multi-line pastes (multi-line on -enter is disabled by this) - -=head2 paste_lock - -time-out to detect pastes (disable the weechat built-in paste -detection if you want to use this) - -=head2 send_empty - -set to on to automatically disregard enter key on empty line - -=head2 hide_magic_nl - -whether the new line inserted by magic enter key will be hidden - -=head2 weechat_paste_fix - -disable ctrl-J binding when paste is detected to stop silly weechat -sending out pastes without allowing to edit them - -=head2 ipl - -this setting controls override of ctrl-M (enter key) by script. Turn -it off if you don't want multiline.pl to set and re-set the key binding. - -=head1 FUNCTION DESCRIPTION - -for full pod documentation, filter this script with - - perl -pE' - (s/^## (.*?) -- (.*)/=head2 $1\n\n$2\n\n=over\n/ and $o=1) or - s/^## (.*?) - (.*)/=item I<$1>\n\n$2\n/ or - (s/^## (.*)/=back\n\n$1\n\n=cut\n/ and $o=0,1) or - ($o and $o=0,1 and s/^sub /=back\n\n=cut\n\nsub /)' - -=cut - -use constant SCRIPT_NAME => 'multiline'; -our $VERSION = '0.6.3'; # af2e0a17b659a16 -weechat::register(SCRIPT_NAME, - 'Nei ', # Author - $VERSION, - 'GPL3', # License - 'Multi-line edit box', # Description - 'stop_multiline', '') || return; -sub SCRIPT_FILE() { - my $infolistptr = weechat::infolist_get('perl_script', '', SCRIPT_NAME); - my $filename = weechat::infolist_string($infolistptr, 'filename') if weechat::infolist_next($infolistptr); - weechat::infolist_free($infolistptr); - return $filename unless @_; -} - -{ -package Nlib; -# this is a weechat perl library -use strict; use warnings; no warnings 'redefine'; - -## i2h -- copy weechat infolist content into perl hash -## $infolist - name of the infolist in weechat -## $ptr - pointer argument (infolist dependend) -## @args - arguments to the infolist (list dependend) -## $fields - string of ref type "fields" if only certain keys are needed (optional) -## returns perl list with perl hashes for each infolist entry -sub i2h { - my %i2htm = (i => 'integer', s => 'string', p => 'pointer', b => 'buffer', t => 'time'); - local *weechat::infolist_buffer = sub { '(not implemented)' }; - my ($infolist, $ptr, @args) = @_; - $ptr ||= ""; - my $fields = ref $args[-1] eq 'fields' ? ${ pop @args } : undef; - my $infptr = weechat::infolist_get($infolist, $ptr, do { local $" = ','; "@args" }); - my @infolist; - while (weechat::infolist_next($infptr)) { - my @fields = map { - my ($t, $v) = split ':', $_, 2; - bless \$v, $i2htm{$t}; - } - split ',', - ($fields || weechat::infolist_fields($infptr)); - push @infolist, +{ do { - my (%list, %local, @local); - map { - my $fn = 'weechat::infolist_'.ref $_; - my $r = do { no strict 'refs'; &$fn($infptr, $$_) }; - if ($$_ =~ /^localvar_name_(\d+)$/) { - $local[$1] = $r; - () - } - elsif ($$_ =~ /^(localvar)_value_(\d+)$/) { - $local{$local[$2]} = $r; - $1 => \%local - } - elsif ($$_ =~ /(.*?)((?:_\d+)+)$/) { - my ($key, $idx) = ($1, $2); - my @idx = split '_', $idx; shift @idx; - my $target = \$list{$key}; - for my $x (@idx) { - my $o = 1; - if ($key eq 'key' or $key eq 'key_command') { - $o = 0; - } - if ($x-$o < 0) { - local $" = '|'; - weechat::print('',"list error: $target/$$_/$key/$x/$idx/@idx(@_)"); - $o = 0; - } - $target = \$$target->[$x-$o] - } - $$target = $r; - - $key => $list{$key} - } - else { - $$_ => $r - } - } @fields - } }; - } - weechat::infolist_free($infptr); - !wantarray && @infolist ? \@infolist : @infolist -} - -## hdh -- hdata helper -## $_[0] - arg pointer or hdata list name -## $_[1] - hdata name -## $_[2..$#_] - hdata variable name -## $_[-1] - hashref with key/value to update (optional) -## returns value of hdata, and hdata name in list ctx, or number of variables updated -sub hdh { - if (@_ > 1 && $_[0] !~ /^0x/ && $_[0] !~ /^\d+$/) { - my $arg = shift; - unshift @_, weechat::hdata_get_list(weechat::hdata_get($_[0]), $arg); - } - while (@_ > 2) { - my ($arg, $name, $var) = splice @_, 0, 3; - my $hdata = weechat::hdata_get($name); - unless (ref $var eq 'HASH') { - $var =~ s/!(.*)/weechat::hdata_get_string($hdata, $1)/e; - (my $plain_var = $var) =~ s/^\d+\|//; - my $type = weechat::hdata_get_var_type_string($hdata, $plain_var); - if ($type eq 'pointer') { - my $name = weechat::hdata_get_var_hdata($hdata, $var); - unshift @_, $name if $name; - } - - my $fn = "weechat::hdata_$type"; - unshift @_, do { no strict 'refs'; - &$fn($hdata, $arg, $var) }; - } - else { - return weechat::hdata_update($hdata, $arg, $var); - } - } - wantarray ? @_ : $_[0] -} - -use Pod::Select qw(); -use Pod::Simple::TextContent; - -## get_desc_from_pod -- return setting description from pod documentation -## $file - filename with pod -## $setting - name of setting -## returns description as text -sub get_desc_from_pod { - my $file = shift; - return unless -s $file; - my $setting = shift; - - open my $pod_sel, '>', \my $ss; - Pod::Select::podselect({ - -output => $pod_sel, - -sections => ["SETTINGS/$setting"]}, $file); - - my $pt = new Pod::Simple::TextContent; - $pt->output_string(\my $ss_f); - $pt->parse_string_document($ss); - - my ($res) = $ss_f =~ /^\s*\Q$setting\E\s+(.*)\s*/; - $res -} - -## get_settings_from_pod -- retrieve all settings in settings section of pod -## $file - file with pod -## returns list of all settings -sub get_settings_from_pod { - my $file = shift; - return unless -s $file; - - open my $pod_sel, '>', \my $ss; - Pod::Select::podselect({ - -output => $pod_sel, - -sections => ["SETTINGS//!.+"]}, $file); - - $ss =~ /^=head2\s+(.*)\s*$/mg -} - -## mangle_man_for_wee -- turn man output into weechat codes -## @_ - list of grotty lines that should be turned into weechat attributes -## returns modified lines and modifies lines in-place -sub mangle_man_for_wee { - for (@_) { - s/_\x08(.)/weechat::color('underline').$1.weechat::color('-underline')/ge; - s/(.)\x08\1/weechat::color('bold').$1.weechat::color('-bold')/ge; - } - wantarray ? @_ : $_[0] -} - -## read_manpage -- read a man page in weechat window -## $file - file with pod -## $name - buffer name -sub read_manpage { - my $caller_package = (caller)[0]; - my $file = shift; - my $name = shift; - - if (my $obuf = weechat::buffer_search('perl', "man $name")) { - eval qq{ - package $caller_package; - weechat::buffer_close(\$obuf); - }; - } - - my @wee_keys = Nlib::i2h('key'); - my @keys; - - my $winptr = weechat::current_window(); - my ($wininfo) = Nlib::i2h('window', $winptr); - my $buf = weechat::buffer_new("man $name", '', '', '', ''); - return weechat::WEECHAT_RC_OK unless $buf; - - my $width = $wininfo->{chat_width}; - --$width if $wininfo->{chat_width} < $wininfo->{width} || ($wininfo->{width_pct} < 100 && (grep { $_->{y} == $wininfo->{y} } Nlib::i2h('window'))[-1]{x} > $wininfo->{x}); - $width -= 2; # when prefix is shown - - weechat::buffer_set($buf, 'time_for_each_line', 0); - eval qq{ - package $caller_package; - weechat::buffer_set(\$buf, 'display', 'auto'); - }; - die $@ if $@; - - @keys = map { $_->{key} } - grep { $_->{command} eq '/input history_previous' || - $_->{command} eq '/input history_global_previous' } @wee_keys; - @keys = 'meta2-A' unless @keys; - weechat::buffer_set($buf, "key_bind_$_", '/window scroll -1') for @keys; - - @keys = map { $_->{key} } - grep { $_->{command} eq '/input history_next' || - $_->{command} eq '/input history_global_next' } @wee_keys; - @keys = 'meta2-B' unless @keys; - weechat::buffer_set($buf, "key_bind_$_", '/window scroll +1') for @keys; - - weechat::buffer_set($buf, 'key_bind_ ', '/window page_down'); - - @keys = map { $_->{key} } - grep { $_->{command} eq '/input delete_previous_char' } @wee_keys; - @keys = ('ctrl-?', 'ctrl-H') unless @keys; - weechat::buffer_set($buf, "key_bind_$_", '/window page_up') for @keys; - - weechat::buffer_set($buf, 'key_bind_g', '/window scroll_top'); - weechat::buffer_set($buf, 'key_bind_G', '/window scroll_bottom'); - - weechat::buffer_set($buf, 'key_bind_q', '/buffer close'); - - weechat::print($buf, " \t".mangle_man_for_wee($_)) # weird bug with \t\t showing nothing? - for `pod2man \Q$file\E 2>/dev/null | GROFF_NO_SGR=1 nroff -mandoc -rLL=${width}n -rLT=${width}n -Tutf8 2>/dev/null`; - weechat::command($buf, '/window scroll_top'); - - unless (hdh($buf, 'buffer', 'lines', 'lines_count') > 0) { - weechat::print($buf, weechat::prefix('error').$_) - for "Unfortunately, your @{[weechat::color('underline')]}nroff". - "@{[weechat::color('-underline')]} command did not produce". - " any output.", - "Working pod2man and nroff commands are required for the ". - "help viewer to work.", - "In the meantime, please use the command ", '', - "\tperldoc $file", '', - "on your shell instead in order to read the manual.", - "Thank you and sorry for the inconvenience." - } -} - -1 -} - -our $MAGIC_ENTER_TIMER; -our $MAGIC_LOCK; -our $MAGIC_LOCK_TIMER; -our $WEECHAT_PASTE_FIX_CTRLJ_CMD; -our $INPUT_CHANGED_EATER_FLAG; -our $IGNORE_INPUT_CHANGED; -our $IGNORE_INPUT_CHANGED2; - -use constant KEY_RET => 'ctrl-M'; -use constant INPUT_NL => '/input insert \x0a'; -use constant INPUT_MAGIC => '/input magic_enter'; -our $NL = "\x0a"; - -init_multiline(); - -my $magic_enter_cancel_dynamic = 1; -my $paste_undo_start_ignore_dynamic = 0; -my $input_changed_eater_dynamic = 0; -my $multiline_complete_fix_dynamic = 1; - -sub magic_enter_cancel_dynamic { $magic_enter_cancel_dynamic ? &magic_enter_cancel : weechat::WEECHAT_RC_OK } -sub paste_undo_start_ignore_dynamic { $paste_undo_start_ignore_dynamic ? &paste_undo_start_ignore : weechat::WEECHAT_RC_OK } -sub input_changed_eater_dynamic { $input_changed_eater_dynamic ? &input_changed_eater : weechat::WEECHAT_RC_OK } -sub multiline_complete_fix_dynamic { $multiline_complete_fix_dynamic ? &multiline_complete_fix : weechat::WEECHAT_RC_OK } - -weechat::hook_config('plugins.var.perl.'.SCRIPT_NAME.'.*', 'default_options', ''); -weechat::hook_modifier('input_text_display_with_cursor', 'multiline_display', ''); -weechat::hook_command_run('/help '.SCRIPT_NAME, 'help_cmd', ''); -weechat::hook_command_run(INPUT_MAGIC, 'magic_enter', ''); -weechat::hook_signal('input_text_*', 'magic_enter_cancel_dynamic', ''); -weechat::hook_command_run('/input *', 'paste_undo_start_ignore_dynamic', ''); -weechat::hook_signal('2000|input_text_changed', 'input_changed_eater_dynamic', ''); -weechat::hook_signal('key_pressed', 'magic_lock_hatch', ''); -# we need lower than default priority here or the first character is separated -weechat::hook_signal('500|input_text_changed', 'paste_undo_hack', '') - # can only do this on weechat 0.4.0 - if (weechat::info_get('version_number', '') || 0) >= 0x00040000; -weechat::hook_command_run("1500|/input complete*", 'multiline_complete_fix_dynamic', 'complete*'); -weechat::hook_command_run("1500|/input delete_*", 'multiline_complete_fix_dynamic', 'delete_*'); -weechat::hook_command_run("1500|/input move_*", 'multiline_complete_fix_dynamic', 'move_*'); - -sub _stack_depth { - my $depth = -1; - 1 while caller(++$depth); - $depth; -} - -## multiline_display -- show multi-lines on display of input string -## () - modifier handler -## $_[2] - buffer pointer -## $_[3] - input string -## returns modified input string -sub multiline_display { - Encode::_utf8_on($_[3]); - Encode::_utf8_on(my $nl = weechat::config_get_plugin('char') || ' '); - Encode::_utf8_on(my $tab = weechat::config_get_plugin('tab')); - my $cb = weechat::current_buffer() eq $_[2] && $MAGIC_ENTER_TIMER; - if ($cb) { - $_[3] =~ s/$NL\x19b#/\x19b#/ if weechat::config_string_to_boolean(weechat::config_get_plugin('hide_magic_nl')); - } - if ($_[3] =~ s/$NL/$nl\x0d/g) { - $_[3] =~ s/\A/ \x0d/ if weechat::config_string_to_boolean(weechat::config_get_plugin('lead_linebreak')); - } - $_[3] =~ s/\x09/$tab/g if $tab; - if ($cb) { - Encode::_utf8_on(my $magic = weechat::config_get_plugin('magic')); - $_[3] =~ s/\Z/$magic/ if $magic; - } - $_[3] -} - -## lock_timer_exp -- expire the magic lock timer -sub lock_timer_exp { - if ($MAGIC_LOCK_TIMER) { - weechat::unhook($MAGIC_LOCK_TIMER); - $MAGIC_LOCK_TIMER = undef; - } - weechat::WEECHAT_RC_OK -} - -## paste_undo_stop_ignore -- unset ignore2 flag -sub paste_undo_stop_ignore { - $IGNORE_INPUT_CHANGED2 = undef; - weechat::WEECHAT_RC_OK -} - -## paste_undo_start_ignore -- set ignore2 flag when /input is received so to allow /input undo/redo -## () - command_run handler -## $_[2] - command that was called -sub paste_undo_start_ignore { - return weechat::WEECHAT_RC_OK if $IGNORE_INPUT_CHANGED; - return weechat::WEECHAT_RC_OK if $_[2] =~ /insert/; - $IGNORE_INPUT_CHANGED2 = 1; - weechat::WEECHAT_RC_OK -} - -## paste_undo_hack -- fix up undo stack when paste is detected by calling /input undo -## () - signal handler -## $_[2] - buffer pointer -sub paste_undo_hack { - return weechat::WEECHAT_RC_OK if $IGNORE_INPUT_CHANGED; - return paste_undo_stop_ignore() if $IGNORE_INPUT_CHANGED2; - if ($MAGIC_LOCK > 0 && get_lock_enabled()) { - signall_ignore_input_changed(1); - $paste_undo_start_ignore_dynamic = 1; - - Encode::_utf8_on(my $input = weechat::buffer_get_string($_[2], 'input')); - my $pos = weechat::buffer_get_integer($_[2], 'input_pos'); - - weechat::command($_[2], '/input undo') for 1..2; - - weechat::buffer_set($_[2], 'input', $input); - weechat::buffer_set($_[2], 'input_pos', $pos); - - $paste_undo_start_ignore_dynamic = 0; - signall_ignore_input_changed(0); - } - weechat::WEECHAT_RC_OK -} - -## input_changed_eater -- suppress input_text_changed signal on new weechats -## () - signal handler -sub input_changed_eater { - $INPUT_CHANGED_EATER_FLAG = undef; - weechat::WEECHAT_RC_OK_EAT -} - -## signall_ignore_input_changed -- use various methods to "ignore" input_text_changed signal -## $_[0] - start ignore or stop ignore -sub signall_ignore_input_changed { - if ($_[0]) { - weechat::hook_signal_send('input_flow_free', weechat::WEECHAT_HOOK_SIGNAL_INT, 1); - $input_changed_eater_dynamic = 1; - $IGNORE_INPUT_CHANGED = 1; - weechat::buffer_set('', 'completion_freeze', '1'); - } - else { - weechat::buffer_set('', 'completion_freeze', '0'); - $IGNORE_INPUT_CHANGED = undef; - $input_changed_eater_dynamic = 0; - weechat::hook_signal_send('input_flow_free', weechat::WEECHAT_HOOK_SIGNAL_INT, 0); - } -} - -## multiline_complete_fix -- add per line /input handling for completion, movement and deletion -## () - command_run handler -## $_[0] - original bound data -## $_[1] - buffer pointer -## $_[2] - original command -sub multiline_complete_fix { - $magic_enter_cancel_dynamic = 0; - $multiline_complete_fix_dynamic = 0; - if ($_[2] =~ s/_message$/_line/ || !weechat::config_string_to_boolean(weechat::config_get_plugin('modify_keys'))) { - weechat::command($_[1], $_[2]); - } - else { - signall_ignore_input_changed(1); - Encode::_utf8_on(my $input = weechat::buffer_get_string($_[1], 'input')); - my $pos = weechat::buffer_get_integer($_[1], 'input_pos'); - if ($pos && $_[2] =~ /(?:previous|beginning_of)_/ && (substr $input, $pos-1, 1) eq $NL) { - substr $input, $pos-1, 1, "\0" - } - elsif ($pos < length $input && $_[2] =~ /(?:next|end_of)_/ && (substr $input, $pos, 1) eq $NL) { - substr $input, $pos, 1, "\0" - } - my @lines = $pos ? (split /$NL/, (substr $input, 0, $pos), -1) : ''; - my @after = $pos < length $input ? (split /$NL/, (substr $input, $pos), -1) : ''; - $lines[-1] =~ s/\0$/$NL/; - $after[0] =~ s/^\0/$NL/; - my ($p1, $p2) = (pop @lines, shift @after); - weechat::buffer_set($_[1], 'input', $p1.$p2); - weechat::buffer_set($_[1], 'input_pos', length $p1); - - $magic_enter_cancel_dynamic = 1; - $INPUT_CHANGED_EATER_FLAG = 1; - weechat::command($_[1], $_[2]); - my $changed_later = !$INPUT_CHANGED_EATER_FLAG; - magic_enter_cancel() if $changed_later; - $magic_enter_cancel_dynamic = 0; - - Encode::_utf8_on(my $p = weechat::buffer_get_string($_[1], 'input')); - $pos = weechat::buffer_get_integer($_[1], 'input_pos'); - weechat::command($_[1], '/input undo') if @lines || @after; - weechat::command($_[1], '/input undo'); - weechat::buffer_set($_[1], 'input', join $NL, @lines, $p, @after); - weechat::buffer_set($_[1], 'input_pos', $pos+length join $NL, @lines, ''); - - signall_ignore_input_changed(0); - weechat::hook_signal_send('input_text_changed', weechat::WEECHAT_HOOK_SIGNAL_POINTER, $_[1]) if $changed_later; - } - $multiline_complete_fix_dynamic = 1; - $magic_enter_cancel_dynamic = 1; - weechat::WEECHAT_RC_OK_EAT -} - -## help_cmd -- show multi-line script documentation -## () - command_run handler -sub help_cmd { - Nlib::read_manpage(SCRIPT_FILE, SCRIPT_NAME); - weechat::WEECHAT_RC_OK_EAT -} - -## get_lock_time -- gets timeout for paste detection according to setting -## returns timeout (at least 1) -sub get_lock_time { - my $lock_time = weechat::config_get_plugin('paste_lock'); - $lock_time = 1 unless $lock_time =~ /^\d+$/ && $lock_time; - $lock_time -} - -## get_lock_enabled -- checks whether the paste detection lock is enabled -## returns bool -sub get_lock_enabled { - my $lock = weechat::config_get_plugin('paste_lock'); - $lock = weechat::config_string_to_boolean($lock) - unless $lock =~ /^\d+$/; - $lock -} - -## magic_lock_hatch -- set a timer for paste detection -## () - signal handler -sub magic_lock_hatch { - lock_timer_exp(); - $MAGIC_LOCK_TIMER = weechat::hook_timer(get_lock_time(), 0, 1, 'lock_timer_exp', ''); - weechat::WEECHAT_RC_OK -} - -## magic_unlock -- reduce the lock added by paste detection -## () - timer handler -sub magic_unlock { - if ($MAGIC_LOCK_TIMER) { - weechat::hook_timer(get_lock_time(), 0, 1, 'magic_unlock', ''); - } - else { - --$MAGIC_LOCK; - if (!$MAGIC_LOCK && $WEECHAT_PASTE_FIX_CTRLJ_CMD) { - do_key_bind('ctrl-J', $WEECHAT_PASTE_FIX_CTRLJ_CMD); - $WEECHAT_PASTE_FIX_CTRLJ_CMD = undef; - } - } - weechat::WEECHAT_RC_OK -} - -## get_magic_enter_time -- get timeout for auto-sending messages according to config -## returns timeout -sub get_magic_enter_time { - my $magic_enter = weechat::config_get_plugin('magic_enter_time'); - $magic_enter = 1000 * weechat::config_string_to_boolean($magic_enter) - unless $magic_enter =~ /^\d+$/; - $magic_enter -} - -## magic_enter -- receive enter key and do magic things: set up a timer for sending the message, add newline -## () - command_run handler -## $_[1] - buffer pointer -sub magic_enter { - Encode::_utf8_on(my $input = weechat::buffer_get_string($_[1], 'input')); - if (!length $input && weechat::config_string_to_boolean(weechat::config_get_plugin('send_empty'))) { - weechat::command($_[1], '/input return'); - } - else { - magic_enter_cancel(); - weechat::command($_[1], INPUT_NL); - - unless (get_lock_enabled() && $MAGIC_LOCK) { - if (weechat::config_string_to_boolean(weechat::config_get_plugin('magic_paste_only')) && - $input !~ /$NL/) { - magic_enter_send($_[1]); - } - elsif (my $magic_enter = get_magic_enter_time()) { - $MAGIC_ENTER_TIMER = weechat::hook_timer($magic_enter, 0, 1, 'magic_enter_send', $_[1]); - } - } - } - weechat::WEECHAT_RC_OK_EAT -} - -## magic_enter_send -- actually send enter key when triggered by magic_enter, remove preceding newline -## $_[0] - buffer pointer -## sending is delayed by 1ms to circumvent crash bug in api -sub magic_enter_send { - magic_enter_cancel(); - weechat::command($_[0], '/input delete_previous_char'); - weechat::command($_[0], '/wait 1ms /input return'); - weechat::WEECHAT_RC_OK -} - -## magic_enter_cancel -- cancel the timer for automatic sending of message, for example when more text was added, increase the paste lock for paste detection when used as signal handler -## () - signal handler when @_ is set -sub magic_enter_cancel { - if ($MAGIC_ENTER_TIMER) { - weechat::unhook($MAGIC_ENTER_TIMER); - $MAGIC_ENTER_TIMER = undef; - } - if ($MAGIC_LOCK_TIMER && @_) { - if (!$MAGIC_LOCK && !$WEECHAT_PASTE_FIX_CTRLJ_CMD && - weechat::config_string_to_boolean(weechat::config_get_plugin('weechat_paste_fix'))) { - ($WEECHAT_PASTE_FIX_CTRLJ_CMD) = get_key_command('ctrl-J'); - $WEECHAT_PASTE_FIX_CTRLJ_CMD = '-' unless defined $WEECHAT_PASTE_FIX_CTRLJ_CMD; - do_key_bind('ctrl-J', '-'); - } - if ($MAGIC_LOCK < 1) { - my $lock_time = get_lock_time(); - ++$MAGIC_LOCK; - weechat::hook_timer(get_lock_time(), 0, 1, 'magic_unlock', ''); - } - } - weechat::WEECHAT_RC_OK -} - -## need_magic_enter -- check if magic enter keybinding is needed according to config settings -## returns bool -sub need_magic_enter { - weechat::config_string_to_boolean(weechat::config_get_plugin('send_empty')) || get_magic_enter_time() || - weechat::config_string_to_boolean(weechat::config_get_plugin('magic_paste_only')) -} - -## do_key_bind -- mute execute a key binding, or unbind if $_[-1] is '-' -## @_ - arguments to /key bind -sub do_key_bind { - if ($_[-1] eq '-') { - pop; - weechat::command('', "/mute /key unbind @_"); - } - elsif ($_[-1] eq '!') { - pop; - weechat::command('', "/mute /key reset @_"); - } - else { - weechat::command('', "/mute /key bind @_"); - } -} - -{ my %keys; -## get_key_command -- get the command bound to a key -## $_[0] - key in weechat syntax -## returns the command - sub get_key_command { - unless (exists $keys{$_[0]}) { - ($keys{$_[0]}) = - map { $_->{command} } grep { $_->{key} eq $_[0] } - Nlib::i2h('key') - } - $keys{$_[0]} - } -} - -## default_options -- set up default option values on start and when unset -## () - config handler if @_ is set -sub default_options { - my %defaults = ( - char => '↩', - tab => '──▶▏', - magic => '‼', - ipl => 'on', - lead_linebreak => 'on', - modify_keys => 'on', - send_empty => 'on', - magic_enter_time => '1000', - paste_lock => '1', - magic_paste_only => 'off', - hide_magic_nl => 'on', - weechat_paste_fix => 'on', - ); - unless (weechat::config_is_set_plugin('ipl')) { - if (my $bar = weechat::bar_search('input')) { - weechat::bar_set($bar, $_, '0') for 'size', 'size_max'; - } - } - for (keys %defaults) { - weechat::config_set_plugin($_, $defaults{$_}) - unless weechat::config_is_set_plugin($_); - } - do_key_bind(KEY_RET, INPUT_NL) - if weechat::config_string_to_boolean(weechat::config_get_plugin('ipl')); - my ($enter_key) = get_key_command(KEY_RET); - if (need_magic_enter()) { - do_key_bind(KEY_RET, INPUT_MAGIC) - if $enter_key eq INPUT_NL; - } - else { - do_key_bind(KEY_RET, INPUT_NL) - if $enter_key eq INPUT_MAGIC; - } - weechat::WEECHAT_RC_OK -} - -sub init_multiline { - $MAGIC_LOCK = -1; - default_options(); - my $sf = SCRIPT_FILE; - for (Nlib::get_settings_from_pod($sf)) { - weechat::config_set_desc_plugin($_, Nlib::get_desc_from_pod($sf, $_)); - } - weechat::WEECHAT_RC_OK -} - -sub stop_multiline { - magic_enter_cancel(); - if (need_magic_enter()) { - my ($enter_key) = get_key_command(KEY_RET); - do_key_bind(KEY_RET, INPUT_NL) - if $enter_key eq INPUT_MAGIC; - } - if ($WEECHAT_PASTE_FIX_CTRLJ_CMD) { - do_key_bind('ctrl-J', $WEECHAT_PASTE_FIX_CTRLJ_CMD); - $WEECHAT_PASTE_FIX_CTRLJ_CMD = undef; - } - if (weechat::config_string_to_boolean(weechat::config_get_plugin('ipl'))) { - do_key_bind(KEY_RET, '!'); - } - weechat::WEECHAT_RC_OK -} diff --git a/perl/newsbar.pl b/perl/newsbar.pl index 2a55a97b..66d060ef 100644 --- a/perl/newsbar.pl +++ b/perl/newsbar.pl @@ -50,6 +50,9 @@ # ----------------------------------------------------------------------------- # # Changelog: +# Version 0.19 2020-06-21, Sébastien Helleu +# * FIX: make call to bar_new compatible with WeeChat >= 2.9 +# # Version 0.18 2014-10-26, nils_2 # * IMPROVED: use hook_print() instead of hook_signal() for private messages # * ADD: option "blacklist_buffers" (idea by Pixelz) @@ -221,7 +224,7 @@ my $weechat_version; my $SCRIPT = "newsbar"; -my $SCRIPT_VERSION = "0.18"; +my $SCRIPT_VERSION = "0.19"; my $SCRIPT_AUTHOR = "rettub"; my $SCRIPT_LICENCE = "GPL3"; my $SCRIPT_DESCRIPTION = "Print highlights or text given by commands into bar 'NewsBar'. Auto popup on top of weechat if needed. 'beeps' can be executed local or remote"; @@ -987,18 +990,34 @@ sub init_bar { weechat::config_get_plugin('away_only') ); weechat::bar_item_new( $bar_name, "build_bar", "" ); - weechat::bar_new( - $bar_name, $Bar_hidden, - "1000", "root", - "", "top", - "vertical", "vertical", - "0", - weechat::config_get_plugin('bar_visible_lines'), - "default", "default", - weechat::config_string(weechat::config_get('weechat.bar.newsbar.color_bg')), - weechat::config_get_plugin('bar_seperator'), - $bar_name - ); + if ($weechat_version >= 0x02090000) { + weechat::bar_new( + $bar_name, $Bar_hidden, + "1000", "root", + "", "top", + "vertical", "vertical", + "0", + weechat::config_get_plugin('bar_visible_lines'), + "default", "default", + weechat::config_string(weechat::config_get('weechat.bar.newsbar.color_bg')), + weechat::config_string(weechat::config_get('weechat.bar.newsbar.color_bg')), + weechat::config_get_plugin('bar_seperator'), + $bar_name + ); + } else { + weechat::bar_new( + $bar_name, $Bar_hidden, + "1000", "root", + "", "top", + "vertical", "vertical", + "0", + weechat::config_get_plugin('bar_visible_lines'), + "default", "default", + weechat::config_string(weechat::config_get('weechat.bar.newsbar.color_bg')), + weechat::config_get_plugin('bar_seperator'), + $bar_name + ); + } } my $c; @@ -1009,17 +1028,31 @@ sub init_bar { unless (defined $Bar_title) { weechat::bar_item_new( $Bar_title_name, "build_bar_title", "" ); - weechat::bar_new( - $Bar_title_name, $Bar_hidden, - "1010", "root", - "", "top", - "vertical", "vertical", - "0", '1', - "default", "default", - $c, - 'off', - $Bar_title_name - ); + if ($weechat_version >= 0x02090000) { + weechat::bar_new( + $Bar_title_name, $Bar_hidden, + "1010", "root", + "", "top", + "vertical", "vertical", + "0", '1', + "default", "default", + $c, $c, + 'off', + $Bar_title_name + ); + } else { + weechat::bar_new( + $Bar_title_name, $Bar_hidden, + "1010", "root", + "", "top", + "vertical", "vertical", + "0", '1', + "default", "default", + $c, + 'off', + $Bar_title_name + ); + } } weechat::bar_item_update($Bar_title_name); diff --git a/perl/notify_send.pl b/perl/notify_send.pl index 8d063aa8..08626bed 100644 --- a/perl/notify_send.pl +++ b/perl/notify_send.pl @@ -1,40 +1,48 @@ -# WeeChat pm and highlight notifications via notify-send +# run system commands on WeeChat pm and highlight, with per-nick smart delays # # modelled after 'WeeChat ubus notifications' by Arvid Picciani # # license: GPL3 -# contact: shmibs@gmail.com +# contact: shmibs@shmibbles.me # history: -# 1.4 another small bug-fix -# 1.3 a small fix to formatting $message -# 1.2 use config options -# 1.1 restructured code for (greater) sanity -# 1.0 first working version +# 1.5 have default command discard output and errors +# 1.4 another small bug-fix +# 1.3 a small fix to formatting $message +# 1.2 use config options +# 1.1 restructured code for (greater) sanity +# 1.0 first working version use strict; use warnings; use constant SCRIPT_NAME => 'notify_send'; -weechat::register(SCRIPT_NAME, 'shmibs', '1.4', 'GPL3', 'execute a user-defined system command upon highlight or private message (with smart delays to avoid spam)', '', ''); +weechat::register(SCRIPT_NAME, 'shmibs', '1.5', 'GPL3', 'execute a user-defined' + . 'system command upon highlight or private message (with smart delays to' + . 'avoid spam)', '', ''); # global var declarations my %pv_times; my %highlight_times; -my %settings_default=( - 'wait_pm' => [ '180', 'necessary time delay between private messages (seconds) for command to be executed' ], - 'wait_highlight' => [ '60', 'necessary time delay between highlights (seconds) for command to be executed' ], - 'ignore_nicks' => [ '', 'comma-separated list of nicks to ignore' ], - 'command' => [ 'notify-send $type: $name', 'system command to be executed ($type, $name, and $message will be interpreted as values)' ] +my %settings_default = ( + 'wait_pm' => [ '180', 'necessary time delay between private messages' + . '(seconds) for command to be executed' ], + 'wait_highlight' => [ '60', 'necessary time delay between highlights' + . '(seconds) for command to be executed' ], + 'ignore_nicks' => [ '', 'comma-separated list of nicks to ignore' ], + 'command' => [ 'notify-send $type: $name &>/dev/null', 'system' + . 'command to be executed ($type, $name, and $message' + . 'will be interpreted as values)' ] ); -my %settings=(); +my %settings = (); -#------------------------------------[ START CONFIGURATION ]------------------------------------ +#----------------------------[ START CONFIGURATION ]---------------------------- sub config_changed { - my ($pointer, $name, $value) = @_; - $name = substr($name, length("plugins.var.perl.".SCRIPT_NAME."."), length($name)); - $settings{$name} = $value; - return weechat::WEECHAT_RC_OK; + my ($pointer, $name, $value) = @_; + $name = substr($name, length("plugins.var.perl." . SCRIPT_NAME . "."), + length($name)); + $settings{$name} = $value; + return weechat::WEECHAT_RC_OK; } sub config_init{ @@ -47,21 +55,24 @@ sub config_init{ $settings{$option} = weechat::config_get_plugin($option); } if ($version >= 0x00030500) { - weechat::config_set_desc_plugin($option, $settings_default{$option}[1]." (default: \"".$settings_default{$option}[0]."\")"); + weechat::config_set_desc_plugin($option, + $settings_default{$option}[1] . " (default: \"" + . $settings_default{$option}[0] . "\")"); } } } config_init(); -weechat::hook_config("plugins.var.perl.".SCRIPT_NAME.".*", "config_changed", ""); +weechat::hook_config("plugins.var.perl." . SCRIPT_NAME . ".*", + "config_changed", ""); -#-------------------------------------[ END CONFIGURATION ]------------------------------------- +#-----------------------------[ END CONFIGURATION ]----------------------------- -my @signals=qw(weechat_pv weechat_highlight); +my @signals = qw(weechat_pv weechat_highlight); # message received hook foreach(@signals) { - weechat::hook_signal($_,'new_notification',''); + weechat::hook_signal($_, 'new_notification', ''); } sub new_notification { @@ -69,12 +80,12 @@ sub new_notification { # $_[2] is the actual content # get the username and message contents - my $name=substr($_[2],0,index($_[2],' ')); - my $message=substr($_[2],index($_[2],' ')); + my $name = substr($_[2], 0, index($_[2], ' ') ); + my $message = substr($_[2], index($_[2], ' ') ); if($name eq ' *'){ - $name=substr($_[2],index($_[2],' *')+3); - $message=substr($name,index($name,' ')); - $name=substr($name,0,index($name,' ')); + $name = substr($_[2], index($_[2], ' *') + 3); + $message = substr($name, index($name, ' ') ); + $name = substr($name,0, index($name, ' ') ); } $message =~ s/ //; $name =~ s/@|\+//; @@ -82,33 +93,34 @@ sub new_notification { # get the type of the message my $type; if($_[1] eq 'weechat_pv') { - $type='PM'; + $type = 'PM'; } else { - $type='HL'; + $type = 'HL'; } # boolean to determine whether or not a notification should # be sent - my $send='true'; + my $send = 'true'; # ignore messages from nicks in ignore_nicks option - foreach(split(/,/,$settings{'ignore_nicks'})) { - $send='false' if($name eq $_); + foreach( split(/,/,$settings{'ignore_nicks'}) ) { + $send = 'false' if($name eq $_); } # determine whether a notification of the same type has been # made recently. if so, ignore it if($type eq 'PM'){ if(exists $pv_times{$name}) { - if(time-$pv_times{$name} < int($settings{'wait_pm'})) { - $send='false'; + if(time - $pv_times{$name} < int($settings{'wait_pm'}) ) { + $send = 'false'; } } $pv_times{$name} = time; } else { if(exists $highlight_times{$name}) { - if(time-$highlight_times{$name} < int($settings{'wait_highlight'})) { - $send='false'; + if(time - $highlight_times{$name} < + int($settings{'wait_highlight'}) ) { + $send = 'false'; } } $highlight_times{$name} = time; @@ -116,11 +128,12 @@ sub new_notification { # run system command if($send eq 'true') { - my ($command,$args) = split(/ /,$settings{'command'},2); - $args =~ s/\$type/$type/g; - $args =~ s/\$name/$name/g; - $args =~ s/\$message/$message/g; - system($command, $args); + my @args = split(/ /,$settings{'command'}); + my $command = shift(@args); + s/\$type/$type/g for @args; + s/\$name/$name/g for @args; + s/\$message/$message/g for @args; + system($command, @args); } return weechat::WEECHAT_RC_OK; diff --git a/perl/notifym.pl b/perl/notifym.pl new file mode 100644 index 00000000..2a086004 --- /dev/null +++ b/perl/notifym.pl @@ -0,0 +1,132 @@ +# +# Copyright (c) 2016-2019 Mitescu George Dan +# Copyright (c) 2019 Silvan Mosberger +# Copyright (c) 2016 Berechet Mihai +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +my $SCRIPT_NAME = "notifym"; +my $VERSION = "1.2"; + +# use Data::Dumper; + +weechat::register($SCRIPT_NAME, "dmitescu", $VERSION, "GPL3", + "Script which uses libnotify to alert the user about certain events.", + "", ""); + +my %options_def = ( 'notify_pv' => ['on', + 'Notify on private message.'], + 'notify_mentions' => ['on', + 'Notify on mention in all channel.'], + 'notify_channels' => ['off', + 'Notify all messages from whitelisted channels.'], + 'notify_servers' => ['off', + 'Notify all messages from whitelisted servers.'], + 'channel_whitelist' => ['.*', + 'Channel white-list. (perl regex required)'], + 'server_whitelist' => ['.*', + 'Server white-list. (perl regex required)'] + ); + +my %options = (); + +# Initiates options if non-existent and loads them +sub init { + foreach my $opt (keys %options_def) { + if (!weechat::config_is_set_plugin($opt)) { + weechat::config_set_plugin($opt, $options_def{$opt}[0]); + } + $options{$opt} = weechat::config_get_plugin($opt); + weechat::config_set_desc_plugin($opt, $options_def{$opt}[1] + . " (default: \"" . $options_def{$opt}[0] + . "\")"); + } +} + +# On update option, load it into the hash +sub update_config_handler { + my ($data, $option, $value) = @_; + $name = substr($option, + length("plugins.var.perl.".$SCRIPT_NAME."."), + length($option)); + $options{$name} = $value; + # weechat::print("", $name . " is now " . $value . "!"); + return weechat::WEECHAT_RC_OK; +} + +# Function to send notification +sub send_notification { + my ($urgency, $summary, $body) = @_; + my $retval = system("notify-send", "-u", $urgency, $summary, $body); +} + +# Verify matching options +sub opt_match { + my ($str, $option) = @_; + return $str =~ /$options{$option}/; +} + +# Handlers for signals : +# Private message + +sub message_handler { + my ($data, $signal, $signal_data) = @_; + # my @pta = split(":", $signal_data); + # weechat::print("", Dumper(\%options)); + my ($server, $command) = $signal =~ /(.*),irc_in_(.*)/; + if ($command eq 'PRIVMSG') { + my $hash_in = {"message" => $signal_data}; + my $hash_data = weechat::info_get_hashtable("irc_message_parse", $hash_in); + + my $nick = $hash_data->{"nick"}; + my $text = $hash_data->{"text"}; + my $chan = $hash_data->{"channel"}; + + if (($options{'notify_servers'} eq 'on') && + opt_match($server, 'server_whitelist')) { + # Server match + send_notification("normal", "$nick:", "$text"); + } elsif (($options{'notify_channels'} eq 'on') && + opt_match($chan, 'channel_whitelist')){ + # Channel match + send_notification("normal", "$nick:", "$text"); + } elsif ($options{'notify_pv'} eq 'on') { + # Private message match + my $mynick = weechat::info_get("irc_nick", $server); + if ($chan eq $mynick) { + send_notification("critical", "$nick says:", "$text"); + } + } else { + } + + # Mention match + my $mynick = weechat::info_get("irc_nick", $server); + if (index($text, $mynick) != -1) { + send_notification("critical", "$nick mentioned you!", ""); + } + # weechat::print("", Dumper($hash_data)); + } + return weechat::WEECHAT_RC_OK; +} + +# Main execution point + +init(); +send_notification("critical", + "Starting NotifyM plugin, version " . $VERSION . "!", + ""); +weechat::hook_config("plugins.var.perl." . $SCRIPT_NAME . ".*", + "update_config_handler", ""); +weechat::hook_signal("*,irc_in_*", "message_handler", ""); diff --git a/perl/parse_relayed_msg.pl b/perl/parse_relayed_msg.pl index 71b14d8e..3f890c05 100644 --- a/perl/parse_relayed_msg.pl +++ b/perl/parse_relayed_msg.pl @@ -1,5 +1,5 @@ # -# Copyright (c) 2011-2013 by w8rabbit (w8rabbit[at]mail[dot]i2p) +# Copyright (c) 2011-2019 by w8rabbit (w8rabbit[at]mail[dot]i2p) # or from outside i2p: w8rabbit[at]i2pmail[dot]org # # Script is under GPL3. @@ -8,6 +8,13 @@ # # thanks to darrob for hard beta-testing # +# 1.9.7: fix: a warning about declaration in same scope +# remove: unnecessary callback function +# 1.9.6: fix: nick parsing with messages containing @ and > +# 1.9.5: add compatibility with matrix-appservice-irc +# 1.9.4: add compatibility with other kind of messages than irc +# 1.9.3: add compatibility with new weechat_print modifier data (WeeChat >= 2.9) +# 1.9.2: add: i2pr-support # 1.9.1: fix: uninitialized value (by arza) # fix: indentation # 1.9: add: Gitter support @@ -64,13 +71,14 @@ use strict; my $SCRIPT_NAME = "parse_relayed_msg"; -my $SCRIPT_VERSION = "1.9.1"; +my $SCRIPT_VERSION = "1.9.7"; my $SCRIPT_DESCR = "proper integration of remote users' nicknames in channel and nicklist"; my $SCRIPT_AUTHOR = "w8rabbit"; my $SCRIPT_LICENCE = "GPL3"; # =============== options =============== -my %option = ( "supported_bot_names" => "cloudrelay*,MultiRelay*,FLIPRelayBot*,i2pRelay,u2,uuu,RelayBot,lll,iRelay,fox,wolf,hawk,muninn,gribble,vulpine,*GitterBot", +my %option = ( "supported_bot_names" => "i2pr,cloudrelay*,MultiRelay*,FLIPRelayBot*,i2pRelay,u2,uuu,RelayBot,lll,iRelay,fox,wolf,hawk,muninn,gribble,vulpine,*GitterBot", + "supported_message_kinds" => "irc_privmsg,matrix_message", "debug" => "off", "blacklist" => "", "servername" => "i2p,freenet", @@ -86,6 +94,7 @@ my %script_desc = ( "blacklist" => "Comma-separated list of relayed nicknames to be ignored (similar to /ignore). The format is case-sensitive: .", "supported_bot_names" => "Comma-separated list of relay bots.", + "supported_message_kinds" => "Comma-separated list of message kinds.", "debug" => "Enable output of raw IRC messages. This is a developer feature and should generally be turned off. The format is: : (default: off)", "servername" => "Comma-separated list of internal servers to enable $SCRIPT_NAME for. (default: i2p,freenet)", "nick_mode" => "Prefix character used to mark relayed nicknames. (default: ⇅). Since WeeChat 0.4.2 you can use format \${color:xxx} but this doesn't affect nicklist.", @@ -100,6 +109,7 @@ # =============== internal values =============== my $weechat_version = ""; my @bot_nicks = ""; +my @message_kinds = ""; my @list_of_server = ""; my @suppress_relaynet_channels = ""; my @blacklist = ""; @@ -114,17 +124,32 @@ sub parse_relayed_msg_cb my ( $data, $modifier, $modifier_data, $string ) = @_; # its neither a channel nor a query buffer - return $string if ( index( $modifier_data,"irc_privmsg" ) == -1 or $modifier_data eq "" ); + my $result = should_handle_modifier($modifier_data); + return $string unless ($result); - $modifier_data =~ (m/irc;(.+?)\.(.+?)\;/); # irc;servername.channelname; - my $servername = $1; - my $channelname = $2; + my $buffer = ""; + my $tags = ""; + if ($modifier_data =~ /0x/) + { + # WeeChat >= 2.9 + $modifier_data =~ m/([^;]*);(.*)/; + $buffer = $1; + $tags = $2; + } + else { + # WeeChat <= 2.8 + $modifier_data =~ m/([^;]*);([^;]*);(.*)/; + $buffer = weechat::buffer_search($1, $2); + $tags = $3; + } + my $servername = weechat::buffer_get_string($buffer, "localvar_server"); + my $channelname = weechat::buffer_get_string($buffer, "localvar_channel"); - return $string if (not defined $servername or not defined $channelname); + return $string if ($servername eq "" or $channelname eq ""); return $string if ( !grep /^$servername$/, @list_of_server ); # does server exists? - my $buf_ptr = weechat::buffer_search("irc",$servername . "." . $channelname); + my $buf_ptr = $buffer; $string =~ m/^(.*)\t(.*)/; # nick[tab]string my $nick = $1; # get the nick name (with prefix!) @@ -136,14 +161,14 @@ sub parse_relayed_msg_cb $nick = $2; # display_mode : 0 = /, 1 = @ - my $result = string_mask_to_regex($nick); - if ($result) + my $result_smtr = string_mask_to_regex($nick); + if ($result_smtr) # if ( grep /^$nick$/, @bot_nicks ) # does a bot exists? { my $blacklist_raw = weechat::config_get_plugin("blacklist"); @blacklist = split( /,/,$blacklist_raw); # message from muninn bot! - if ( $line =~ m/^<([^@]+)@([^>]+)\>\s(.+)$/ ) + if ( $line =~ m/^<([^@>]+)@([^>]+)\>\s(.+)$/ ) { my ($relaynick, $relaynet, $relaymsg) = ($1,$2,$3); if ( grep /^$servername.$relaynick$/, @blacklist ) # check for ignored relay nicks @@ -180,6 +205,44 @@ sub parse_relayed_msg_cb weechat::print_date_tags($buf_ptr,0,$modifier_data,$string); return ""; } + # PRIVMSG #i2p :[Freenode/nickname] here is the message. + elsif ( $line =~ m/^[\(\[`](.+?)\/(.+?)[\)\]`] (.+)$/ ) + { + my ($relayserver,$relaynick,$relaymsg) = ($1,$2,$3); + if ( grep /^$servername.$relaynick$/, @blacklist ) # check for ignored relay nicks + { + return ''; # delete message from ignored relaynick + } + my $nick_mode = ""; + ($relaynick,$nick_mode) = check_nick_mode($buf_ptr,$relaynick); + add_relay_nick_to_nicklist($buf_ptr,$relaynick,""); + (undef, $relaymsg) = colorize_lines($modifier_data,$relaynick, $relaymsg); + + $string = create_string_without_relaynet($servername,$channelname,$relaynick,$nick_mode,$relaymsg); + + $modifier_data = change_tags_for_message( $buf_ptr,$relaynick,"",$modifier_data,"" ); + weechat::print_date_tags($buf_ptr,0,$modifier_data,$string); + return ""; + } + # message from matrix-appservice-irc + elsif ( $line =~ m/^\[\w\] <@([^>]+)> (.+)$/ ) + { + my ($relaynick,$relaymsg) = ($1,$2); + if ( grep /^$servername.$relaynick$/, @blacklist ) # check for ignored relay nicks + { + return ''; # delete message from ignored relaynick + } + my $nick_mode = ""; + ($relaynick,$nick_mode) = check_nick_mode($buf_ptr,$relaynick); + add_relay_nick_to_nicklist($buf_ptr,$relaynick,""); + (undef, $relaymsg) = colorize_lines($modifier_data,$relaynick, $relaymsg); + + $string = create_string_without_relaynet($servername,$channelname,$relaynick,$nick_mode,$relaymsg); + + $modifier_data = change_tags_for_message( $buf_ptr,$relaynick,"",$modifier_data,"" ); + weechat::print_date_tags($buf_ptr,0,$modifier_data,$string); + return ""; + } # message from FLIP & Gitter elsif ( $line =~ m/^[\(\[`](.+?)[\)\]`] (.+)$/ ) { @@ -591,7 +654,6 @@ sub add_relay_nick_to_nicklist # check out every x minutes if nick is not too old sub check_own_nicklist { - my ($data, $signal, $signal_data) = @_; my $current_time = time(); my $timer = $option{"timer"}; while (my ($name, $time) = each %nick_timer) @@ -634,6 +696,7 @@ sub init_config } @bot_nicks = split( /,/, $option{supported_bot_names} ); # read bot names + @message_kinds = split( /,/, $option{supported_message_kinds} ); # read supported_message_kinds @list_of_server = split( /,/, $option{servername} ); # read server @suppress_relaynet_channels = split( /,/, $option{suppress_relaynet_channels} ); # read channels @blacklist = split( /,/, $option{blacklist} ); # read blacklist of relay nicks @@ -659,6 +722,11 @@ sub toggle_config_by_set @bot_nicks = ""; @bot_nicks = split( /,/, $option{supported_bot_names} ); } + if ( $name eq "supported_message_kinds" ) + { + @message_kinds = ""; + @message_kinds = split( /,/, $option{supported_message_kinds} ); + } if ( $name eq "servername" ) { @list_of_server = ""; @@ -745,6 +813,21 @@ sub shutdown return weechat::WEECHAT_RC_OK; } +sub should_handle_modifier +{ + my ($modifier_data) = @_; + + foreach ( @message_kinds ){ + my $message_kind = weechat::string_mask_to_regex($_); + if (index( $modifier_data,$message_kind ) != -1) + { + return 1; + } + } + + return 0; +} + # ========= string_mask_to_regex() ========= sub string_mask_to_regex { diff --git a/perl/pop3_mail.pl b/perl/pop3_mail.pl index 3e362b49..0b8417cc 100644 --- a/perl/pop3_mail.pl +++ b/perl/pop3_mail.pl @@ -20,6 +20,8 @@ # Add [mail] to your weechat.bar.status.items # # +# 2021-05-05: Sébastien Helleu +# 0.4 : add compatibility with XDG directories (WeeChat >= 3.2) # 2013-09-15: nils_2 (freenode.#weechat) # 0.3 : add: option prefix_item # @@ -57,7 +59,7 @@ use Encode; my $prgname = "pop3_mail"; -my $SCRIPT_version = "0.3"; +my $SCRIPT_version = "0.4"; my $description = "check POP3 server for mails and display mail header"; my $item_name = "mail"; @@ -547,13 +549,8 @@ sub read_file sub weechat_dir { - my $dir = weechat::config_get_plugin("pop3_list"); - if ( $dir =~ /%h/ ) - { - my $weechat_dir = weechat::info_get( 'weechat_dir', ''); - $dir =~ s/%h/$weechat_dir/; - } - return $dir; + my $options = { "directory" => "config" }; + return weechat::string_eval_path_home(weechat::config_get_plugin("pop3_list"), {}, {}, $options); } sub shutdown{ diff --git a/perl/pushover.pl b/perl/pushover.pl index 797d224a..aaa6904c 100644 --- a/perl/pushover.pl +++ b/perl/pushover.pl @@ -1,5 +1,5 @@ # -# Copyright (C) 2013-2015 stfn +# Copyright (C) 2013-2017 stfn # https://github.com/stfnm/weechat-scripts # # This program is free software: you can redistribute it and/or modify @@ -22,19 +22,15 @@ my %SCRIPT = ( name => 'pushover', author => 'stfn ', - version => '1.4', + version => '2.0', license => 'GPL3', - desc => 'Send push notifications to your mobile devices using Pushover, NMA, Pushbullet or Free Mobile', + desc => 'Send push notifications to your mobile devices using Pushover', opt => 'plugins.var.perl', ); my %OPTIONS_DEFAULT = ( 'enabled' => ['on', "Turn script on or off"], - 'service' => ['pushover', 'Notification service to use. Multiple services may be supplied as comma separated list. (supported services: pushover, nma, pushbullet)'], 'token' => ['ajEX9RWhxs6NgeXFJxSK2jmpY54C9S', 'pushover API token/key (You may feel free to use your own token, so you get your own monthly quota of messages without being affected by other users. See also: https://pushover.net/faq#overview-distribution )'], 'user' => ['', "pushover user key"], - 'nma_apikey' => ['', "nma API key"], - 'pb_apikey' => ['', "Pushbullet API key"], - 'pb_device_iden' => ['', "Device Iden of pushbullet device"], 'sound' => ['', "Sound (empty for default)"], 'priority' => ['', "priority (empty for default)"], 'show_highlights' => ['on', 'Notify on highlights'], @@ -46,8 +42,6 @@ 'verbose' => ['1', 'Verbosity level (0 = silently ignore any errors, 1 = display brief error, 2 = display full server response)'], 'rate_limit' => ['0', 'Rate limit in seconds (0 = unlimited), will send a maximum of 1 notification per time limit'], 'short_name' => ['off', 'Use short buffer name in notification'], - 'free_user' => ['', 'Free Mobile User ID (see your account)'], - 'free_pass' => ['', 'Automatic generated Free key'], ); my %OPTIONS = (); my $TIMEOUT = 30 * 1000; @@ -65,10 +59,20 @@ # Setup hooks weechat::hook_print("", "notify_message,notify_private,notify_highlight", "", 1, "print_cb", ""); -weechat::hook_command($SCRIPT{"name"}, "send custom push notification", +weechat::hook_command($SCRIPT{"name"}, "send notification", "", - "text: notification text to send", - "", "pushover_cb", ""); + "text: notification text to send\n" . + "\n" . + "Don't forget to configure your own Pushover user and token.\n" . + "\n" . + "You can also setup custom notifications (for any other services etc.) by using a /trigger on the hsignal \"pushover\".\n\nExamples:\n" . + "\n" . + "pushover.net using curl:\n" . + "/trigger add mynotify hsignal pushover \"\" \"\" \"/exec -bg curl -s --form-string 'token=abc123' --form-string 'user=user123' --form-string 'message=\${message_stripped}' https://api.pushover.net/1/messages.json\"\n" . + "\n" . + "free-mobile.fr using curl:\n" . + "/trigger add mynotify hsignal pushover \"\" \"\" \"/exec -bg curl -s 'https://smsapi.free-mobile.fr/sendmsg?user=USER&pass=TOKEN&msg=\${message_escaped}'\"\n", + "", "pushover_cmd_cb", ""); # # Handle config stuff @@ -200,7 +204,7 @@ sub print_cb # # /pushover # -sub pushover_cb +sub pushover_cmd_cb { my ($data, $buffer, $args) = @_; @@ -229,20 +233,19 @@ sub url_cb $msg .= "@_"; } - # Check server response and display error message if NOT successful - if ($command =~ /pushover/ && $return_code == 0 && !($out =~ /\"status\":1/)) { - weechat::print("", $msg); - } elsif ($command =~ /notifymyandroid/ && $return_code == 0 && !($out =~ /success code=\"200\"/)) { - weechat::print("", $msg); - } elsif ($command =~ /pushbullet/ && $return_code == 0 && !($out =~ /\"iden\"/)) { - weechat::print("", $msg); + # Check if server response reported success + if ($command =~ /pushover/ && $return_code == 0 && $out =~ /\"status\":1/) { + return weechat::WEECHAT_RC_OK; } + # Otherwise display error message + weechat::print("", $msg); + return weechat::WEECHAT_RC_OK; } # -# Notify wrapper (decides which service to use) +# Notify wrapper # sub notify($) { @@ -260,19 +263,13 @@ ($) weechat::hook_timer($timer, 0, 1, "rate_limit_cb", ""); } - # Notify services - if (grep_list("pushover", $OPTIONS{service})) { + # Notify service + if ($OPTIONS{token} ne "" && $OPTIONS{user} ne "") { notify_pushover(eval_expr($OPTIONS{token}), eval_expr($OPTIONS{user}), $message, "weechat", $OPTIONS{priority}, $OPTIONS{sound}); } - if (grep_list("nma", $OPTIONS{service})) { - notify_nma(eval_expr($OPTIONS{nma_apikey}), "weechat", "$SCRIPT{name}.pl", $message, $OPTIONS{priority}); - } - if (grep_list("pushbullet", $OPTIONS{service})) { - notify_pushbullet(eval_expr($OPTIONS{pb_apikey}), eval_expr($OPTIONS{pb_device_iden}), "weechat", $message); - } - if (grep_list("freemobile", $OPTIONS{service})) { - notify_freemobile(eval_expr($OPTIONS{free_pass}), eval_expr($OPTIONS{free_user}), $message); - } + + # Send hsignal (so triggers can pick up on it) + notify_hsignal($message); } # @@ -295,7 +292,7 @@ ($$$$$$) push(@post, "sound=" . url_escape($sound)) if ($sound && length($sound) > 0); # Send HTTP POST - my $hash = { "post" => 1, "postfields" => join(";", @post) }; + my $hash = { "post" => 1, "postfields" => join("&", @post) }; if ($DEBUG) { weechat::print("", "[$SCRIPT{name}] Debug: msg -> `$message' HTTP POST -> @post"); } else { @@ -306,72 +303,14 @@ ($$$$$$) } # -# https://www.notifymyandroid.com/api.jsp -# -sub notify_nma($$$$$) -{ - my ($apikey, $application, $event, $description, $priority) = @_; - - # Required API arguments - my @post = ( - "apikey=" . url_escape($apikey), - "application=" . url_escape($application), - "event=" . url_escape($event), - "description=" . url_escape($description), - ); - - # Optional API arguments - push(@post, "priority=" . url_escape($priority)) if ($priority && length($priority) > 0); - - # Send HTTP POST - my $hash = { "post" => 1, "postfields" => join("&", @post) }; - if ($DEBUG) { - weechat::print("", "[$SCRIPT{name}] Debug: msg -> `$description' HTTP POST -> @post"); - } else { - weechat::hook_process_hashtable("url:https://www.notifymyandroid.com/publicapi/notify", $hash, $TIMEOUT, "url_cb", ""); - } - - return weechat::WEECHAT_RC_OK; -} - -# -# https://docs.pushbullet.com/v2/pushes/ +# hsignal to use with /trigger # -sub notify_pushbullet($$$$) +sub notify_hsignal($) { - my ($apikey, $device_iden, $title, $body) = @_; - - # Required API arguments - my $apiurl = "https://$apikey\@api.pushbullet.com/v2/pushes"; - my @post = ( - "type=note", - ); - - # Optional API arguments - push(@post, "device_iden=" . url_escape($device_iden)) if ($device_iden && length($device_iden) > 0); - push(@post, "title=" . url_escape($title)) if ($title && length($title) > 0); - push(@post, "body=" . url_escape($body)) if ($body && length($body) > 0); - - # Send HTTP POST - my $hash = { "post" => 1, "postfields" => join("&", @post) }; - if ($DEBUG) { - weechat::print("", "$apiurl [$SCRIPT{name}] Debug: msg -> `$body' HTTP POST -> @post"); - } else { - weechat::hook_process_hashtable("url:$apiurl", $hash, $TIMEOUT, "url_cb", ""); - } - - return weechat::WEECHAT_RC_OK; -} - -# -# Free Mobile -# -sub notify_freemobile($$$) -{ - my ($token, $user, $message) = @_; - - # Not very clean but it works - system "curl \"https://smsapi.free-mobile.fr/sendmsg?user=" . url_escape($user) . "&pass=" . url_escape($token) . "&msg=" . url_escape($message) . "\""; + my $message = $_[0]; + my $message_escaped = url_escape($message); + my $message_stripped = $message; + $message_stripped =~ s/'//gi; # strip apostrophe - return weechat::WEECHAT_RC_OK; + weechat::hook_hsignal_send($SCRIPT{name}, { "message" => $message, "message_escaped" => $message_escaped, "message_stripped" => $message_stripped }); } diff --git a/perl/pv_info.pl b/perl/pv_info.pl new file mode 100644 index 00000000..ef6dc817 --- /dev/null +++ b/perl/pv_info.pl @@ -0,0 +1,328 @@ +use strict; +use warnings; + +# pv_info.pl – a WeeChat script that attach a new bar in query windows showing +# formatted `whois` information of chat partners and keep them updated using a timer. +# Copyright (C) 2018 Max Wölfing + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# TODO +# - Read/store user defined variables in configuration +# - Extend missing WHOIS informations and beautify output +# - Better sorting mechanism for WHOIS informations (see: FIXME_SORTING) +# - Maybe switch to WeeChat API for WHOIS (infolist/irc_nick) + +# KNOWN BUGS +# - 'init_bar_item' is called 4 times, but only one time directly by this script, probably a bug in WeeChat. + +# -- Config -- +my $bar_name = 'pv_info_bar'; +my $bar_item_name = 'whois'; +my $bar_item_refresh = 120; + +# -- Internal -- +use constant DEBUG => (0); +use if DEBUG, 'Data::Dumper'; +my $script_name = 'pv_info'; +my $script_version = '0.0.6'; +my $script_description = 'Attach a new bar in query windows, showing `whois` information of chat partners'; +my (%whois, %hooks); + +# -- Init -- +weechat::register($script_name, 'Max Woelfing ', + $script_version,'GPL3', $script_description,'unload_cb', ''); + +if ((weechat::info_get('version_number', '') // 0) < 0x00040000) { + weechat::print('', "WeeChat version >= 0.4.0 is required to run $script_name"); +} else { + if (weechat::config_string(weechat::config_get('weechat.bar.title.conditions')) !~ m/\$\{type\} != private/) { + weechat::print('','To disable the (unnecessary) '.weechat::color('yellow').'title bar'.weechat::color('default'). + ' in private buffers, set: '.weechat::color('bold').'weechat.bar.title.conditions'.weechat::color('default'). + ' to '.weechat::color('bold').'"${type} != private"'); + } + + weechat::bar_item_new($bar_item_name, 'init_bar_item', ''); + if ((weechat::info_get('version_number', '') // 0) >= 0x02090000) { + weechat::bar_new($bar_name, 'off', '500', 'window', '${type} == private', 'top', + 'vertical', 'horizontal', '0', '0', 'default', 'default', 'default', 'default', + 'on', $bar_item_name); + } else { + weechat::bar_new($bar_name, 'off', '500', 'window', '${type} == private', 'top', + 'vertical', 'horizontal', '0', '0', 'default', 'default', 'default', + 'on', $bar_item_name); + } + + # -- Hooks -- + $hooks{'sigwhois'} = weechat::hook_hsignal('irc_redirection_sigwhois_whois', 'sigwhois_cb', ''); + $hooks{'timer'} = weechat::hook_timer($bar_item_refresh * 1000, 60, 0, 'trigger_update', ''); + $hooks{'pv_opened'} = weechat::hook_signal('irc_pv_opened', 'sigwhois_send', ''); + $hooks{'buf_switch'} = weechat::hook_signal('buffer_switch', 'sigwhois_send', ''); + $hooks{'buf_closing'} = weechat::hook_signal('buffer_closing', 'buf_closing_cb', ''); + + weechat::print('', "$script_name loaded!"); +} + +sub init_bar_item { + my ($data, $bar_item, $window) = @_; + my $buffer = weechat::window_get_pointer($window, 'buffer'); + my $server = weechat::buffer_get_string($buffer, 'localvar_server'); + + if (weechat::buffer_get_string($buffer, 'localvar_type') eq 'private') { + my $nick = weechat::buffer_get_string($buffer, 'localvar_channel'); + my $mask = "$nick\@$server"; + + if (weechat::info_get('irc_is_nick', $nick) eq '') { + weechat::print('', weechat::prefix('network').'No nick for window found') if DEBUG; + + return ''; + } + + # TODO: Get /whois info from weechat API + + weechat::print('', weechat::prefix('network')."bar_item initialised for Nick = $nick, Mask = $mask") if DEBUG; + + my $user = weechat::color('default').weechat::color('bold').'['.weechat::color('darkgray').$nick.weechat::color('default').weechat::color('bold').']'; + my $str = ''; + + # FIXME_SORTING + # Sort output of 'whois' command by name of 'whois message' (number prefix) + for my $whois_msg (sort {lc $a cmp lc $b} keys %{$whois{$mask}}) { + $str .= "$user $whois{$mask}{$whois_msg}" . "\n"; + } + + # Remove spaces and/or linefeed at the end + $str =~ s/\s+$//; + chomp($str); + + # If empty, set to [$nick] + if (length($str) == 0) { + $str = "$user"; + } + + return $str; + } +} + +sub sigwhois_send { + my ($data, $signal, $signal_data) = @_; + + my $buffer; + if ($signal eq 'triggered_by_timer') { + $buffer = $signal_data; + } else { + $buffer = weechat::current_buffer(); + } + my $server = weechat::buffer_get_string($buffer, 'localvar_server'); + + if (weechat::buffer_get_string($buffer, 'localvar_type') eq 'private') { + my $nick = weechat::buffer_get_string($buffer, 'localvar_channel'); + + if (weechat::info_get('irc_is_nick', $nick) eq '') { + return weechat::WEECHAT_RC_ERROR; + } + + my $mask = "$nick\@$server"; + if ($whois{$mask}) { + weechat::print('', weechat::prefix('network')."Deleting old WHOIS info for user: $mask") if DEBUG; + delete $whois{$mask}; + } + + weechat::print('', weechat::prefix('network').'Sending whois signal..') if DEBUG; + weechat::hook_hsignal_send('irc_redirect_command', { 'server' => $server, 'pattern' => 'whois', 'signal' => 'sigwhois' }); + weechat::hook_signal_send('irc_input_send', weechat::WEECHAT_HOOK_SIGNAL_STRING, "$server;;1;;/whois $nick $nick"); + } + + return weechat::WEECHAT_RC_OK; +} + +sub sigwhois_cb { + my ($data, $signal, $signal_data) = @_; + my %hashtable = %{$signal_data}; + + weechat::print('', weechat::prefix('network').'We got an whois reply..') if DEBUG; + + # Sometimes IRC:311 is not the first WHOIS response and so we cannot set $mask on it, in that case we fill a generic user table, + # and merge it later, but first we have to clean the room.. + my $mask = '__undefined__'; + if ($whois{$mask}) { + delete $whois{$mask}; + } + my $server = $hashtable{'server'}; + my $bee_user = 0; + if ($server eq "bitlbee") { + $bee_user = 1; + } + foreach my $line ($hashtable{'output'}) { + weechat::print('', weechat::prefix('network')."+--------------------------------------------------------------------------------------------------+\n".$line."\n". + weechat::prefix('network').'+--------------------------------------------------------------------------------------------------+') if DEBUG; + + # 275 - whois (secure connection) + if ($line =~ /275 (\S+) (\S+) :(.*)/) { + $whois{$mask}{'HHH_secure_connection'} = weechat::color('default').$3; + } + + # 276 - whois (certificate fingerprint) + if ($line =~ /276 (\S+) (\S+) :(.*)/) { + $whois{$mask}{'III_cert_fingerprint'} = weechat::color('default').$3; + } + + # 301 - whois (away) + if ($line =~ /301 (\S+) (\S+) :(.*)/) { + $whois{$mask}{'BBB_away'} = weechat::color('bold')."Away status: ".weechat::color('darkgray').$3; + } + + # 307 - whois (registered nick) + if ($line =~ /307 (.*) :user (.*)/) { + $whois{$mask}{'MMM_registered'} = weechat::color('default').$2; + } + + # 310 - whois (help mode) + + # 311 - whois (user) + if ($line =~ /311 (\S+) (\S+) (\S+) (\S+) (.*) :(.*)/) { + $mask = "$2\@$server"; + weechat::print('', weechat::prefix('network')."Using '$mask' as '\$whois{\$mask}'") if DEBUG; + $whois{$mask}{'AAA_user'} = weechat::color('white').$6. " ".weechat::color('darkgray')."(".weechat::color('88')."$2\@$4".weechat::color('darkgray').")"; + } + + # 312 - whois (server) + if ($line =~ /312 (\S+) (\S+) (\S+) :(.*)/) { + $whois{$mask}{'EEE_server'} = weechat::color('default').$3." ".weechat::color('default')."(".weechat::color('bold').$4.weechat::color('default').")"; + } + + # 313 - whois (operator) + + # 317 - whois (idle) + if ($line =~ /317 (\S+) (\S+) (\d+) (\d+) (.*)/) { + my $idle_time; + my @idle_time_parts; + if ($3 != 0) { + @idle_time_parts = gmtime($3); + $idle_time = sprintf("%d days, %d hours, %d minutes, %d seconds",@idle_time_parts[7,2,1,0]); + } else { + $idle_time = "No"; + } + my $signon = scalar localtime $4; + $whois{$mask}{'LLL_idle'} = weechat::color('bold')."idle: ".weechat::color('darkgray').$idle_time.weechat::color('default').", ".weechat::color('bold')."signon at: ".weechat::color('darkgray').$signon; + } + + # 318 - whois (end) + + # 319 - whois (channels) + if ($line =~ /319 (\S+) (\S+) :(.*)/) { + $whois{$mask}{'DDD_channels'} = weechat::color('darkgray').$3; + } + + # 320 - whois (identified user) + # FIXME + if ($line =~ /320 (\S+) (\S+) :(.*)/) { + my $away_msg = $3; + if (length($away_msg) >= 20) { + if ($away_msg =~ /^(\S+) .* as a result .* (\d* min)/) { + $away_msg = "$1 (As a result of being idle more than $2)"; + } + } + $whois{$mask}{'CCC_identified_user'} = weechat::color('bold')."Away / Status message: ".weechat::color('darkgray').$away_msg; + } + + # 326 - whois (has oper privs) + # 327 - whois (host) + + # 330 - whois (logged in as) + if ($line =~ /330 (\S+) (\S+) (\S+) :(.*)/) { + $whois{$mask}{'JJJ_logged_in_as'} = weechat::color('default').$4." ".weechat::color('bold').$3; + } + + # 335 - whois (is a bot on) + + # 338 - whois (host) + if ($line =~ /338 (\S+) (\S+) (\S+) (\S+) :.*/) { + $whois{$mask}{'KKK_host'} = weechat::color('bold')."Actual user\@host: ".weechat::color('darkgray').$3.weechat::color('default').", ".weechat::color('bold')."Actual IP: ".weechat::color('darkgray').$4; + } + + # 343 - whois (is opered as) + + # 378 - whois (connecting from) + if ($line =~ /378 (\S+) (\S+) (\S+) (.*)/) { + $whois{$mask}{'FFF_connecting_from'} = weechat::color('default').$4; + } + + # 379 - whois (using modes) + # 401 - no such nick/channel + # 402 - no such server + + # 671 - whois (secure connection) + if ($line =~ /671 (\S+) (\S+) :(.*)/) { + $whois{$mask}{'GGG_secure_connection'} = weechat::color('default').$3; + } + } + + # Add a custom informations for BitlBee user + if ($bee_user == 1) { + $whois{$mask}{'ZZZ_bitlbee'} = "is connected by ".weechat::color('yellow').weechat::color('bold')."BitlBee"; + } + + # If we have some data in $whois{'__undefined__'} merge it + if ($mask ne '__undefined__' && $whois{'__undefined__'}) { + weechat::print('', weechat::prefix('network')."We have data in __undefined__, need to merge it with \$whois{'$mask'}..") if DEBUG; + foreach my $key (keys %{$whois{'__undefined__'}}) { + $whois{$mask}{$key} = $whois{'__undefined__'}{$key}; + } + delete $whois{'__undefined__'}; + } + + weechat::print('', weechat::prefix('network').'Got new informations, updating bar_item..') if DEBUG; + weechat::bar_item_update($bar_item_name); + + return weechat::WEECHAT_RC_OK; +} + +sub trigger_update { + my $buffer = weechat::current_buffer(); + + if (weechat::buffer_get_string($buffer, 'localvar_type') ne "private") { + return weechat::WEECHAT_RC_OK; + } + + weechat::print('', weechat::prefix('network').'Updating the current private buffer using the trigger..') if DEBUG; + sigwhois_send('', 'triggered_by_timer', $buffer); + + return weechat::WEECHAT_RC_OK; +} + +sub buf_closing_cb { + my ($data, $signal, $buffer) = @_; + + if (weechat::buffer_get_string($buffer, 'localvar_type') ne "private") { + return weechat::WEECHAT_RC_OK; + } + + my $nick = weechat::buffer_get_string($buffer, 'localvar_channel'); + return weechat::WEECHAT_RC_OK if (weechat::info_get('irc_is_nick', $nick) eq ''); + + if ($whois{$nick}) { + delete $whois{$nick}; + } + + return weechat::WEECHAT_RC_OK; +} + +sub unload_cb { + for my $hook (keys %hooks) { + weechat::unhook($hooks{$hook}); + } + weechat::bar_remove(weechat::bar_search($bar_name)); + weechat::bar_item_remove(weechat::bar_item_search($bar_item_name)); +} diff --git a/perl/query_blocker.pl b/perl/query_blocker.pl index 09dca245..53085196 100644 --- a/perl/query_blocker.pl +++ b/perl/query_blocker.pl @@ -4,7 +4,7 @@ # # ----------------------------------------------------------------------------- # Copyright (c) 2009-2014 by rettub -# Copyright (c) 2011-2016 by nils_2 +# Copyright (c) 2011-2025 by nils_2 # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,7 +22,7 @@ # ----------------------------------------------------------------------------- # # Simple IRC query blocker. -# - requires WeeChat 0.3.2 or newer +# - requires WeeChat 0.4.2 or newer # - suggests perl script newsbar # # Got inspiration from (xchat script): @@ -38,9 +38,45 @@ # # ----------------------------------------------------------------------------- # History: -# 2016-12-11, mumixam : +# 2025-05-26: nils_2@libera.#weechat +# version 1.8: +# ADD: new argument '-all' for del (idea PeGaSuS) +# IMP: hook_completion_list_add() to completion_list_add() +# +# 2025-03-02: nils_2@libera.#weechat +# version 1.7: +# ADD: use of print_date_tags to set an tag for query_blocker messages, new option msg_tag (idea PeGaSuS) +# +# 2023-06-29: nils_2@libera.#weechat +# version 1.6: +# FIX: nick was not correctly parsed when message has tags. +# +# 2022-02-21: CrazyCat +# version 1.5: +# FIX: regression from https://github.com/weechat/scripts/issues/493 +# +# 2022-02-18: nils_2@libera.#weechat: +# version 1.4: +# FIX: https://github.com/weechat/scripts/issues/493 +# +# 2021-05-05: Sébastien Helleu : +# version 1.3: +# FIX: add compatibility with XDG directories (WeeChat >= 3.2) +# +# 2018-07-30, usefulz & nils_2: +# version 1.2: +# FIX: undefine subroutine +# ADD: eval_expression() for options +# FIX: Warnung: Use of uninitialized value using highmon as a bar +# FIX: Warnung: Use of uninitialized value using newsbar +# +# 2017-04-14, nils_2: +# version 1.1: +# ADD: function to ignore server (https://github.com/weechat/scripts/issues/79) +# +# 2016-12-11, mumixam: # version 1.0: -# FIX: message starting with color not caught +# FIX: message starting with color not caught # # 2014-05-22, nils_2: # version 0.9: @@ -102,11 +138,11 @@ my $SCRIPT = 'query_blocker'; my $AUTHOR = 'rettub '; -my $VERSION = '1.0'; +my $VERSION = '1.8'; my $LICENSE = 'GPL3'; my $DESCRIPTION = 'Simple blocker for private message (i.e. spam)'; my $COMMAND = "query_blocker"; # new command name -my $ARGS_HELP = " | | | | | | | "; +my $ARGS_HELP = " | | | | | | | "; my %help_desc = ( "block_queries" => "to enable or disable $COMMAND (default: 'off')", "quiet" => "will send auto reply about blocking, but don't send any notice to you. (default: 'off')", "show_deny_message" => "show you the deny message, sent to user. (default: 'off')", @@ -114,14 +150,15 @@ "show_nick_only" => "only show nick and server. (default: 'off')", "show_first_message_only"=> "Show only first message sent by blocked queries (default: 'on')", "whitelist" => "path/file-name to store/read nicks not to be blocked (default: qb-whitelist.txt)", - "auto_message" => "messages to inform user that you don't like to get private messages without asking first. '%N' will be replaced with users nick.", - "auto_message_prefix" => "Prefix for auto message, may not be empty!", - "msgbuffer" => "buffer used to display $SCRIPT messages (current = current buffer, private = private buffer, weechat = weechat core buffer, server = server buffer, buffer = $SCRIPT buffer, highmon = highmon buffer)", + "auto_message" => "messages to inform user that you don't like to get private messages without asking first. '%N' will be replaced with users nick (note: content is evaluated, see /help eval).", + "auto_message_prefix" => "Prefix for auto message, may not be empty! (note: content is evaluated, see /help eval)", + "msgbuffer" => "buffer used to display $SCRIPT messages (current = current buffer, private = private buffer, weechat = weechat core buffer, server = server buffer, buffer = $SCRIPT buffer, highmon = highmon buffer, newsbar = newsbar-bar)", "logger" => "logger status for $SCRIPT buffer (default: 'off')", - "hotlist_show" => "$SCRIPT buffer appear in hotlists (status bar/buffer.pl) (default: 'off')", + "hotlist_show" => "$SCRIPT buffer appear in hotlists (status bar/buflist) (default: 'off')", "open_on_startup" => "open $SCRIPT buffer on startup. option msgbuffer has to be set to 'buffer' (default: 'off')", "temporary_mode" => "if 'on' you have to manually add a nick to whitelist. otherwise a conversation will be temporary only and after closing query buffer the nick will be discard (default: 'off')", "ignore_auto_message" => "path/file-name to store/read nicks to not send an auto message (default: qb-ignore_auto_message.txt)", + "msg_tag" => "set a tag for query_blocker messages to filter or trigger messages (default: '')", ); my $CMD_HELP = < "off", "temporary_mode" => "off", "ignore_auto_message" => "qb-ignore_auto_message.txt", + "msg_tag" => "", ); my $Last_query_nick = undef; @@ -294,6 +333,7 @@ sub print_info { my ( $buffer, $server, $my_nick, $nick, $message ) = @_; my $prefix_network = weechat::config_string( weechat::config_get("weechat.look.prefix_network")); my $buf_pointer = ""; + my $bar_pointer = ""; my $orig_message = $message; if ( $buffer eq "current" ){ @@ -315,11 +355,11 @@ sub print_info { } # no buffer found, use weechat fallback buffer - if ( $buf_pointer eq "" ){ + if ( $buf_pointer eq "" or not defined $buf_pointer ){ if ( $server eq "" ){ $buf_pointer = weechat::buffer_search_main(); }else{ - $buf_pointer = fallback($server); + $buf_pointer = fallback_buffer($server); } } return $buf_pointer if (lc(weechat::config_get_plugin('quiet') eq "on")); @@ -329,15 +369,23 @@ sub print_info { }else{ $message = ""; } - unless ( exists $Blocked{$server.".".$nick} and lc(weechat::config_get_plugin('show_first_message_only') eq 'off') ) { - weechat::print($buf_pointer,"$prefix_network\t" + my $print_line = "$prefix_network\t" .irc_nick_find_color($nick).$nick .weechat::color('reset') ." tries to start a query on " .irc_nick_find_color($server).$server .weechat::color('reset') - .$message ); + .$message; + weechat::print_date_tags($buf_pointer,0,weechat::config_get_plugin('msg_tag'),$print_line); + +# weechat::print($buf_pointer,"$prefix_network\t" +# .irc_nick_find_color($nick).$nick +# .weechat::color('reset') +# ." tries to start a query on " +# .irc_nick_find_color($server).$server +# .weechat::color('reset') +# .$message ); weechat::print($buf_pointer,"$prefix_network\t" ."to allow query: /$COMMAND add " .irc_nick_find_color($server).$server @@ -352,25 +400,31 @@ sub print_info { .irc_nick_find_color($nick).$nick .weechat::color('reset')) unless (weechat::config_get_plugin('show_hint') eq 'off'); }else{ - weechat::print($buf_pointer,irc_nick_find_color($server).$server."." + my $print_line = irc_nick_find_color($server).$server."." .irc_nick_find_color($nick).$nick."\t" .weechat::color('reset') - .$orig_message ); + .$orig_message; + weechat::print_date_tags($buf_pointer,0,weechat::config_get_plugin('msg_tag'),$print_line); +# weechat::print($buf_pointer,irc_nick_find_color($server).$server."." +# .irc_nick_find_color($nick).$nick."\t" +# .weechat::color('reset') +# .$orig_message ); } return $buf_pointer; } +sub eval_expression{ + my ($string) = @_; + return weechat::string_eval_expression($string, {}, {},{}); +} + +# get value from msgbuffer_fallback option sub fallback_buffer{ - my $server = @_; + my ($server) = @_; my $fallback = weechat::config_string( weechat::config_get("irc.look.msgbuffer_fallback") ); my $buf_pointer; - if ( $fallback eq "current" ) - { - my $buf_pointer = weechat::current_buffer(); - }elsif ( $fallback eq "server" ) - { - $buf_pointer = weechat::buffer_search("irc","server".".".$server); - } + $buf_pointer = weechat::current_buffer() if ( $fallback eq "current" ); + $buf_pointer = weechat::buffer_search("irc","server".".".$server) if ( $fallback eq "server" ); return $buf_pointer; } @@ -378,10 +432,18 @@ sub modifier_irc_in_privmsg { my ( $data, $signal, $server, $arg ) = @_; my $my_nick = weechat::info_get( 'irc_nick', $server ); + # by default, blocking is enabled for all server. except the one with a localvar + return $arg if (weechat::buffer_get_string(weechat::buffer_search("irc", "server.".$server), 'localvar_query_blocker')); + + # check for query message - if ( $arg =~ m/:(.+?)!.+? PRIVMSG $my_nick :(.*)/i ) { - my $query_nick = $1; - my $query_msg = $2; + if ( $arg =~ m/:(.+?)!.+? PRIVMSG (.+?) :(.*)/i ) { + my $hashtable = weechat::info_get_hashtable("irc_message_parse" => + { "message" => $arg }); + my $query_nick = $hashtable->{nick}; + my $my_nick_msg = $2; + my $query_msg = $3; + + return $arg if ($my_nick ne $my_nick_msg); # always allow own queries return $arg if ($query_nick eq $my_nick); @@ -423,19 +485,35 @@ sub modifier_irc_in_privmsg { # auto responce msg to query_nick (deny_message) my $msg = weechat::config_get_plugin('auto_message_prefix') . weechat::config_get_plugin('auto_message'); + $msg =~ s/%N/$query_nick/g; # keep this for historical reasons + $msg = eval_expression($msg); - $msg =~ s/%N/$query_nick/g; if (lc(weechat::config_get_plugin('show_deny_message')) eq 'off' or lc(weechat::config_get_plugin('quiet') eq 'on')) { # According to the RFC 1459, automatic messages must not be sent as response to NOTICEs and currently it might be possible to get in loop of automatic away messages or something similar. # weechat::command( '', "/mute -all /msg -server $server $query_nick $msg " ); weechat::command( '', "/mute -all /notice -server $server $query_nick $msg " ); } - else + else # show deny message! { # weechat::command( '', "/mute -all /msg -server $server $query_nick $msg " ); weechat::command( '', "/mute -all /notice -server $server $query_nick $msg " ); - weechat::print($buf_pointer,"$SCRIPT\t"."$query_nick"."@"."$server: $msg"); + if ( newsbar() eq "1" and lc(weechat::config_get_plugin('msgbuffer')) eq 'newsbar' ) { + weechat::command( '', + "/newsbar add --color lightred [QUERY-WARN]\t" # $color $category + . irc_nick_find_color($query_nick) + . $query_nick + . weechat::color('reset') . '@' + . irc_nick_find_color($server) + . $server + . weechat::color('reset') + . ": " + . weechat::color('bold') + . "$msg"); + } + else { + weechat::print($buf_pointer,"$SCRIPT\t"."$query_nick"."@"."$server: $msg"); + } } # counter for how many blocked messages $Blocked{$server.".".$query_nick} = 0; @@ -542,14 +620,14 @@ sub qb_unhook { sub query_blocker_completion_del_cb{ my ($data, $completion_item, $buffer, $completion) = @_; foreach ( sort { "\L$a" cmp "\L$b" } keys %Allowed ) { - weechat::hook_completion_list_add($completion, $_,1, weechat::WEECHAT_LIST_POS_SORT); + weechat::completion_list_add($completion, $_,1, weechat::WEECHAT_LIST_POS_SORT); } return weechat::WEECHAT_RC_OK; } sub query_blocker_completion_add_cb{ my ($data, $completion_item, $buffer, $completion) = @_; foreach (reverse keys %Blocked) { - weechat::hook_completion_list_add($completion, $_,1, weechat::WEECHAT_LIST_POS_SORT); + weechat::completion_list_add($completion, $_,1, weechat::WEECHAT_LIST_POS_SORT); } return weechat::WEECHAT_RC_OK; } @@ -654,17 +732,23 @@ sub query_blocker { } elsif ( $cmd eq 'add' ) { _add($arg,1); }elsif ( $cmd eq 'del' and defined $arg ) { - foreach ( split( / +/, $arg ) ) { - if (exists $Allowed{$_} ) { - delete $Allowed{$_}; - my ($server,$nick) = split(/\./,$_); - weechat::print( '', "Nick removed from whitelist: '". - weechat::color(weechat::config_color(weechat::config_get("weechat.color.chat_server"))).$server . - weechat::color('reset') . "." . - irc_nick_find_color($nick) . $nick . - weechat::color('reset') . "'"); - } else { - weechat::print( '', "Can't remove nick, not in whitelist: '" . irc_nick_find_color($_) . $_ . weechat::color('reset') . "'"); + if ( $arg eq '-all' ) { + %Allowed = (); # empty hash with all whitelist nick + weechat::print( '', "Nicks removed from whitelist"); + } + else { + foreach ( split( / +/, $arg ) ) { + if (exists $Allowed{$_} ) { + delete $Allowed{$_}; + my ($server,$nick) = split(/\./,$_); + weechat::print( '', "Nick removed from whitelist: '". + weechat::color(weechat::config_color(weechat::config_get("weechat.color.chat_server"))).$server . + weechat::color('reset') . "." . + irc_nick_find_color($nick) . $nick . + weechat::color('reset') . "'"); + } else { + weechat::print( '', "Can't remove nick, not in whitelist: '" . irc_nick_find_color($_) . $_ . weechat::color('reset') . "'"); + } } } whitelist_save(); @@ -708,6 +792,7 @@ sub qb_msg { return weechat::WEECHAT_RC_OK if ($server eq ""); my ($msg) = $command =~ /^\/msg -server .*?\s.*?\s(.*)/; + return weechat::WEECHAT_RC_OK if (not defined $msg); my $n = _get_nick($_[2]); return weechat::WEECHAT_RC_OK if (lc($n) eq "nickserv" or lc($n) eq "chanserv"); my $prefix = weechat::config_get_plugin('auto_message_prefix'); @@ -729,6 +814,7 @@ sub query_blocker_buffer_open weechat::buffer_set($query_blocker_buffer, "notify", "0"); }elsif (weechat::config_get_plugin("hotlist_show") eq "on"){ weechat::buffer_set($query_blocker_buffer, "notify", "3"); +# weechat::buffer_set($query_blocker_buffer, "hotlist", "WEECHAT_HOTLIST_HIGHLIGHT"); } weechat::buffer_set($query_blocker_buffer, "title", $SCRIPT); # logger @@ -763,13 +849,15 @@ sub buffer_closing_cb if ( weechat::register( $SCRIPT, $AUTHOR, $VERSION, $LICENSE, $DESCRIPTION, "", "" ) ) { weechat::hook_command( $COMMAND, $DESCRIPTION, $ARGS_HELP, $CMD_HELP, $COMPLETITION, $CALLBACK, "" ); $weechat_version = weechat::info_get("version_number", ""); - if ( ($weechat_version ne "") && (weechat::info_get("version_number", "") < 0x00030200) ) { - weechat::print("",weechat::prefix("error")."$SCRIPT: needs WeeChat >= 0.3.2. Please upgrade: http://www.weechat.org/"); + if ( ($weechat_version ne "") && (weechat::info_get("version_number", "") < 0x00040200) ) { + weechat::print("",weechat::prefix("error")."$SCRIPT: needs WeeChat >= 0.4.2. Please upgrade: http://www.weechat.org/"); weechat::command("","/wait 1ms /perl unload $SCRIPT"); } + my $weechat_dir = weechat::info_get("weechat_config_dir", ""); + $weechat_dir = weechat::info_get("weechat_dir", "") if (!$weechat_dir); if ( weechat::config_get_plugin("whitelist") eq '' ) { - weechat::config_set_plugin( "whitelist", weechat::info_get( "weechat_dir", "" ) . "/" . $SETTINGS{"whitelist"} ); + weechat::config_set_plugin( "whitelist", $weechat_dir . "/" . $SETTINGS{"whitelist"} ); } while ( my ( $option, $default_value ) = each(%SETTINGS) ) { weechat::config_set_plugin( $option, $default_value ) diff --git a/perl/recoverop.pl b/perl/recoverop.pl index e7b87911..095e3fb3 100644 --- a/perl/recoverop.pl +++ b/perl/recoverop.pl @@ -48,7 +48,7 @@ weechat::register( "recoverop", "AYANOKOUZI, Ryuunosuke", - "0.1.1", "GPL3", "recover channel operator in empty channel", + "0.1.3", "GPL3", "recover channel operator in empty channel", "", "" ); my $script_name = "recoverop"; @@ -91,7 +91,7 @@ sub part_join { weechat::buffer_search( "irc", "$server.$channel" ); if ($buffer) { - my $sec = int rand 10; + my $sec = 1 + int rand 10; weechat::command( $buffer, "/wait ${sec}s /cycle" ); if ($conf->{mode}) { @@ -109,10 +109,10 @@ sub my_signal_irc_in_PART_cb { my $data = shift; my $signal = shift; my $type_data = shift; - my $signal_data = shift; + my $hashtable = weechat::info_get_hashtable("irc_message_parse" => + { "message" => $type_data }); my $server = ( split ',', $signal )[0]; - my ( $user, $channel ) = ( split ' ', $type_data, 4 )[ 0, 2 ]; - my ( $nick, $username, $address ) = ( $user =~ m/:(.*)!(.*)@(.*)/ ); + my $channel = $hashtable->{channel}; + my $nick = $hashtable->{nick}; part_join( $server, $channel, $nick ); return weechat::WEECHAT_RC_OK; } @@ -121,10 +121,9 @@ sub my_signal_irc_in_QUIT_cb { my $data = shift; my $signal = shift; my $type_data = shift; - my $signal_data = shift; + my $hashtable = weechat::info_get_hashtable("irc_message_parse" => + { "message" => $type_data }); my $server = ( split ',', $signal )[0]; - my $user = ( split ' ', $type_data, 3 )[0]; - my ( $nick, $username, $address ) = ( $user =~ m/:(.*)!(.*)@(.*)/ ); + my $nick = $hashtable->{nick}; my $infolist = weechat::infolist_get( "irc_channel", "", "$server" ); while ( weechat::infolist_next($infolist) ) { diff --git a/perl/rslap.pl b/perl/rslap.pl index 0229c111..260635e0 100644 --- a/perl/rslap.pl +++ b/perl/rslap.pl @@ -1,6 +1,5 @@ # # rslap.pl - Random slap strings for weechat 0.3.0 -# Version 1.3.1 # # Let's you /slap a nick but with a random string # Customisable via the 'rslap' file in your config dir @@ -25,6 +24,8 @@ # is a valid entry number # History: +# 2021-05-05, Sébastien Helleu : +# v1.4: add compatibility with XDG directories (WeeChat >= 3.2) # 2010-12-30, KenjiE20 : # v1.3.1 -fix: uninitialised variable error # 2010-04-25, KenjiE20 : @@ -55,9 +56,11 @@ # along with this program. If not, see . # -weechat::register("rslap", "KenjiE20", "1.3.1", "GPL3", "Slap Randomiser", "", ""); +weechat::register("rslap", "KenjiE20", "1.4", "GPL3", "Slap Randomiser", "", ""); -$file = weechat::info_get("weechat_dir", "")."/rslap"; +my $weechat_dir = weechat::info_get("weechat_data_dir", ""); +$weechat_dir = weechat::info_get("weechat_dir", "") if (!$weechat_dir); +$file = $weechat_dir."/rslap"; my @lines; $lastrun = 0; $rslap_slapback_hook = 0; diff --git a/perl/rssagg.pl b/perl/rssagg.pl index 99c79b34..6bfc6ece 100644 --- a/perl/rssagg.pl +++ b/perl/rssagg.pl @@ -40,6 +40,10 @@ # # # History: +# 2021-05-06, Sébastien Helleu : +# v1.3: Add compatibility with XDG directories (WeeChat >= 3.2). +# 2020-06-21, Sébastien Helleu : +# v1.2: Make call to bar_new compatible with WeeChat >= 2.9. # 2013-04-06, R1cochet : # v1.1: Added option "rssagg.engine.autostop". Added "last" option to /rssagg command. # Muted filter in rssagg buffer. Fixed partial feed callback. Other bug fixes. @@ -53,7 +57,7 @@ use XML::FeedPP; my $SCRIPT_NAME = "rssagg"; -my $VERSION = "1.1"; +my $VERSION = "1.3"; my $SCRIPT_DESC = "RSS/RDF/Atom feed aggregator for WeeChat"; ######################### Global Vars ######################### @@ -261,7 +265,8 @@ sub init_config { } my %init_hash = (); - my $home_dir = weechat::info_get("weechat_dir", ""); + my $home_dir = weechat::info_get("weechat_config_dir", ""); + $home_dir = weechat::info_get("weechat_dir", "") if (!$home_dir); if (-e "$home_dir/$SCRIPT_NAME.conf") { open(my $fh, "<:encoding(UTF-8)", "$home_dir/$SCRIPT_NAME.conf") || weechat::print("", "can't open UTF-8 encoded filename: $!"); my $section; @@ -322,7 +327,8 @@ sub config_write { # write to my config file sub tmp_dir { my $dir = weechat::config_string($config{'options'}{'tmp_dir'}); if ($dir =~ /%h/) { - my $homedir = weechat::info_get("weechat_dir", ""); + my $homedir = weechat::info_get("weechat_cache_dir", ""); + $homedir = weechat::info_get("weechat_dir", "") if (!$homedir); $dir =~ s/%h/$homedir/; } if ($dir !~ /\/$/) { $dir .= "/"; } @@ -657,7 +663,11 @@ sub set_current_line { sub bar_create { $rssagg_bar = weechat::bar_search("rssagg"); if ($rssagg_bar eq "") { - $rssagg_bar = weechat::bar_new("rssagg", "off", 0, "root", "", "top", "vertical", "vertical", "4", "20", "default", "cyan", "default", 'off', "rssagg"); + if ($wee_version_number >= 0x02090000) { + $rssagg_bar = weechat::bar_new("rssagg", "off", 0, "root", "", "top", "vertical", "vertical", "4", "20", "default", "cyan", "default", "default", 'off', "rssagg"); + } else { + $rssagg_bar = weechat::bar_new("rssagg", "off", 0, "root", "", "top", "vertical", "vertical", "4", "20", "default", "cyan", "default", 'off', "rssagg"); + } } weechat::bar_item_new("rssagg", "rssagg_bar_build", ""); return weechat::WEECHAT_RC_OK; diff --git a/perl/spell_menu.pl b/perl/spell_menu.pl index 482c97ee..48964caf 100644 --- a/perl/spell_menu.pl +++ b/perl/spell_menu.pl @@ -24,6 +24,11 @@ =head1 USAGE aspell.check.default_dict +since WeeChat 2.5 its: + spell.check.enabled on + spell.check.suggestions >-1 + spell.check.default_dict + you also need to have a menu script, if you don't have it yet: /script install menu.pl @@ -86,8 +91,14 @@ =head2 complete_noend =cut +my $plugin_name = "spell"; # WeeChat >= 2.5 +my $old_plugin_name = "aspell"; # WeeChat < 2.5 + use constant SCRIPT_NAME => 'spell_menu'; -weechat::register(SCRIPT_NAME, 'Nei ', '0.4', 'GPL3', 'spell checker menu', '', '') || return; +weechat::register(SCRIPT_NAME, 'Nei ', '0.5', 'GPL3', 'spell checker menu', '', '') || return; +my $weechat_version = weechat::info_get('version_number', '') || 0; +$plugin_name = $old_plugin_name if ($weechat_version < 0x02050000); # v2.5 + sub SCRIPT_FILE() { my $infolistptr = weechat::infolist_get('perl_script', '', SCRIPT_NAME); my $filename = weechat::infolist_string($infolistptr, 'filename') if weechat::infolist_next($infolistptr); @@ -352,7 +363,7 @@ sub spell_menu { return weechat::WEECHAT_RC_OK } return weechat::WEECHAT_RC_OK if $_[2] eq '/input complete_next' && weechat::config_string_to_boolean(weechat::config_get_plugin('no_complete')); - Encode::_utf8_on(my $sugs = weechat::buffer_get_string($_[1], 'localvar_aspell_suggest')); + Encode::_utf8_on(my $sugs = weechat::buffer_get_string($_[1], 'localvar_'.$plugin_name.'_suggest')); return weechat::WEECHAT_RC_OK unless $sugs; my $fix = $1 if $_[2] =~ /^fix (\d+)/; my $badword; @@ -368,7 +379,7 @@ sub spell_menu { weechat::buffer_set($_[1], 'input_pos', $rpos + length $goodword); } else { - return weechat::WEECHAT_RC_OK unless weechat::config_boolean(weechat::config_get('aspell.check.enabled')); + return weechat::WEECHAT_RC_OK unless weechat::config_boolean(weechat::config_get($plugin_name.'.check.enabled')); return weechat::WEECHAT_RC_OK unless $pos >= $rpos; if ($_[2] eq '/input complete_next') { my $offset = $rpos + length $badword; @@ -385,7 +396,7 @@ sub spell_menu { my ($i, $j, $dc) = (0, 0, 0); my %seen; my @shortcut = (undef, 1..9, 0, 'a'..'z'); - my @dict = split ',', (weechat::config_string(weechat::config_get('aspell.dict.'.weechat::buffer_get_string($_[1], 'full_name'))) || weechat::config_string(weechat::config_get('aspell.check.default_dict'))); + my @dict = split ',', (weechat::config_string(weechat::config_get($plugin_name.'.dict.'.weechat::buffer_get_string($_[1], 'full_name'))) || weechat::config_string(weechat::config_get($plugin_name.'.check.default_dict'))); for my $sug (split '([,/])', $sugs) { next if $sug eq ','; if ($sug eq '/') { # next dict @@ -409,12 +420,12 @@ sub spell_menu { } for (@dict) { $dc = sprintf '%02d', $dc + 1; - $r{"9$dc.command"} = "/aspell addword $dict[$dc-1] \$0"; + $r{"9$dc.command"} = "/".$plugin_name." addword $dict[$dc-1] \$0"; $r{"9$dc.name"} = "ADD($dict[$dc-1]) $badword"; } } else { - $r{'9.command'} = '/aspell addword $0'; + $r{'9.command'} = '/'.$plugin_name.' addword $0'; $r{'9.name'} = "ADD $badword"; } %spell_menu = %r; diff --git a/perl/stalker.pl b/perl/stalker.pl index ce2bb65e..fd07b670 100644 --- a/perl/stalker.pl +++ b/perl/stalker.pl @@ -1,5 +1,5 @@ # -# Copyright (c) 2013-2014 by Nils Görs +# Copyright (c) 2013-2018 by Nils Görs # Copyright (c) 2013-2014 by Stefan Wold # based on irssi script stalker.pl from Kaitlyn Parkhurst (SymKat) # https://github.com/symkat/Stalker @@ -20,22 +20,36 @@ # along with this program. If not, see . # # History: -# version 1.5:nils_2@freenode.#weechat +# +# version 1.6.3: Sébastien Helleu +# 2021-11-06: add: compatibility with WeeChat >= 3.4 (new parameters in function hdata_search) +# +# version 1.6.2: Sébastien Helleu +# 2021-05-06: add: compatibility with WeeChat >= 3.2 (XDG directories) +# +# version 1.6.1: nils_2@freenode.#weechat +# 2018-01-11: fix: wrong variable name +# +# version 1.6: nils_2@freenode.#weechat +# 2018-01-09: add: use hook_process_hashtable() for /WHOIS +# : imp: use hook_process_hashtable() instead hook_process() for security reasons +# +# version 1.5: nils_2@freenode.#weechat # 2015-06-15: add: new option del_date # -# version 1.4:nils_2@freenode.#weechat +# version 1.4: nils_2@freenode.#weechat # 2014-05-14: fix: perl error under some circumstances (thanks Piotrek) # -# version 1.3:nils_2@freenode.#weechat +# version 1.3: nils_2@freenode.#weechat # 2014-04-28: fix: output when loading script twice # -# version 1.2:nils_2@freenode.#weechat +# version 1.2: nils_2@freenode.#weechat # 2014-04-13: add: support of customise irc_join messages (weechat >= 0.4.4) # -# version 1.1:nils_2@freenode.#weechat +# version 1.1: nils_2@freenode.#weechat # 2013-10-31: add: flood-protection on JOINs # -# version 1.0:nils_2@freenode.#weechat +# version 1.0: nils_2@freenode.#weechat # 2013-10-28: add: option 'additional_join_info' (idea by: arch_bcn) # add: option 'timeout' time to wait for result of hook_process() # add: localvar 'drop_additional_join_info' @@ -101,7 +115,7 @@ use DBI; my $SCRIPT_NAME = "stalker"; -my $SCRIPT_VERSION = "1.5"; +my $SCRIPT_VERSION = "1.6.3"; my $SCRIPT_AUTHOR = "Nils Görs "; my $SCRIPT_LICENCE = "GPL3"; my $SCRIPT_DESC = "Records and correlates nick!user\@host information"; @@ -133,7 +147,7 @@ 'flood_max_nicks' => '20', # '' => '', ); -my %desc_options = ('db_name' => 'file containing the SQLite database where information is recorded. This database is created on loading of ' . $SCRIPT_NAME . ' if it does not exist. ("%h" will be replaced by WeeChat home, "~/.weechat" by default) (default: %h/nicks.db)', +my %desc_options = ('db_name' => 'file containing the SQLite database where information is recorded. This database is created on loading of ' . $SCRIPT_NAME . ' if it does not exist. ("%h" will be replaced by WeeChat data directory) (default: %h/nicks.db)', 'debug' => 'Prints debug output to core buffer so you know exactly what is going on. This is far too verbose to be enabled when not actively debugging something. (default: off)', 'max_recursion' => 'For each correlation between nick <-> host that happens, one point of recursion happens. A corrupt database, general evilness, or misfortune can cause the recursion to skyrocket. This is a ceiling number that says if after this many correlation attempts we have not found all nickname and hostname correlations, stop the process and return the list to this point. Use this option with care on slower machines like raspberry pi.', 'recursive_search' => 'When enabled, recursive search causes stalker to function better than a simple hostname to nickname map. Disabling the recursive search in effect turns stalker into a more standard hostname -> nickname map.', @@ -148,7 +162,7 @@ 'ignore_whois' => 'When enabled, /WHOIS won\'t be monitored. (default: off)', 'tags' => 'comma separated list of tags used for messages printed by stalker. See documentation for possible tags (e.g. \'no_log\', \'no_highlight\'). This option does not effect DEBUG messages.', 'additional_join_info' => 'add a line below the JOIN message that will display alternative nicks (tags: "irc_join", "irc_smart_filter" will be add to additional_join_info). You can use a localvar to drop additional join info for specific buffer(s) "stalker_drop_additional_join_info" (default: off)', - 'timeout' => 'timeout in seconds for hook_process(), used with option "additional_join_info". On slower machines, like raspberry pi, increase time. (default: 1)', + 'timeout' => 'timeout in seconds for hook_process_hashtable(), used with option "additional_join_info". On slower machines, like raspberry pi, increase time. (default: 1)', 'flood_timer' => 'Time in seconds for which flood protection is active. Once max_nicks is reached, joins will be ignored for the remaining duration of the timer. (default:10)', 'flood_max_nicks' => 'Maximum number of joins to allow in flood_timer length of time. Once maximum number of joins is reached, joins will be ignored until the timer ends (default:20)', ); @@ -181,7 +195,7 @@ ], ); -# ---------------[ external routines for hook_process() ]--------------------- +# ---------------[ external routines for hook_process_hashtable() ]--------------------- if ($#ARGV == 8 ) # (0-8) nine arguments given? { my $db_filename = $ARGV[0]; @@ -515,7 +529,7 @@ sub add_record my ( $nick, $user, $host, $serv ) = @_; return unless ($nick and $user and $host and $serv); - # Check if we already have this record, before using a hook_process() + # Check if we already have this record, before using a hook_process_hashtable() my $sth = $DBH_child->prepare( "SELECT nick FROM records WHERE nick = ? AND user = ? AND host = ? AND serv = ?" ); $sth->execute( $nick, $user, $host, $serv ); my $result = $sth->fetchrow_hashref; @@ -533,7 +547,22 @@ sub add_record my $db_filename = weechat_dir(); DEBUG("info", "Start hook_process(), to add $nick $user\@$host on $serv to database"); - weechat::hook_process("perl $filename $db_filename 'db_add_record' '$nick' '$user' '$host' '$serv' 'dummy' 'dummy' 'dummy'", 1000 * $options{'timeout'},"db_add_record_cb",""); + + weechat::hook_process_hashtable("perl", + { + "arg1" => $filename, + "arg2" => $db_filename, + "arg3" => 'db_add_record', + "arg4" => $nick, + "arg5" => $user, + "arg6" => $host, + "arg7" => $serv, + "arg8" => 'dummy', + "arg9" => 'dummy', + "arg10" => 'dummy', + }, 1000 * $options{'timeout'},"db_add_record_cb",""); + + } # function called when data from child is available, or when child has ended, arguments and return value @@ -839,13 +868,8 @@ sub _color_str # -------------------------------[ subroutines ]------------------------------------- sub weechat_dir { - my $dir = $options{'db_name'}; - if ( $dir =~ /%h/ ) - { - my $weechat_dir = weechat::info_get( 'weechat_dir', ''); - $dir =~ s/%h/$weechat_dir/; - } - return $dir; + my $eval_options = { "directory" => "data" }; + return weechat::string_eval_path_home($options{'db_name'}, {}, {}, $eval_options); } # -------------------------------[ main ]------------------------------------- sub stalker_command_cb @@ -964,6 +988,9 @@ sub command_must_be_executed_on_irc_buffer # hdata: hdata pointer # pointer: pointer to a WeeChat/plugin object # search: expression to evaluate, default pointer in expression is the name of hdata (and this pointer changes for each element in list); for help on expression, see command /eval in WeeChat User’s guide +# pointers: pointers for evaluated expression +# extra_vars: extra variables for evaluated expression +# options: options for evaluated expression # move: number of jump(s) to execute after unsuccessful search (negative or positive integer, different from 0) sub channel_scan_41 @@ -979,10 +1006,38 @@ sub channel_scan_41 my $hdata_channel = weechat::hdata_get("irc_channel"); my $hdata_nick = weechat::hdata_get("irc_nick"); - my $ptr_server = weechat::hdata_search($hdata_server, weechat::hdata_get_list($hdata_server, 'irc_servers'), '${irc_server.name} == ' . $server_name, 1); + my $ptr_server; + if ($weechat_version >= 0x03040000) + { + $ptr_server = weechat::hdata_search($hdata_server, + weechat::hdata_get_list($hdata_server, 'irc_servers'), + '${irc_server.name} == ${server_name}', + {}, + {'server_name' => $server_name}, + {}, + 1); + } + else + { + $ptr_server = weechat::hdata_search($hdata_server, weechat::hdata_get_list($hdata_server, 'irc_servers'), '${irc_server.name} == ' . $server_name, 1); + } if ($ptr_server) { - my $ptr_channel = weechat::hdata_search($hdata_channel, weechat::hdata_pointer($hdata_server, $ptr_server, 'channels'), '${irc_channel.name} == ' . $channel_name, 1); + my $ptr_channel; + if ($weechat_version >= 0x03040000) + { + $ptr_channel = weechat::hdata_search($hdata_channel, + weechat::hdata_pointer($hdata_server, $ptr_server, 'channels'), + '${irc_channel.name} == ${channel_name}', + {}, + {'channel_name' => $channel_name}, + {}, + 1); + } + else + { + $ptr_channel = weechat::hdata_search($hdata_channel, weechat::hdata_pointer($hdata_server, $ptr_server, 'channels'), '${irc_channel.name} == ' . $channel_name, 1); + } if ($ptr_channel) { @@ -1001,14 +1056,8 @@ sub channel_scan_41 } } -# weechat::print("",$ptr_buffer); -# weechat::print("",$server_name); - -# if ($ptr_servers) -# { -# my $channel = weechat::hdata_search(hdata['channel'], weechat.hdata_pointer(hdata['server'], server, 'channels'), '${irc_channel.name} == #test', 1) -# } } + sub channel_scan { my $ptr_buffer = $_[0]; @@ -1102,7 +1151,6 @@ sub irc_in2_whois_cb my (undef, undef, undef, $nick, $user, $host, undef) = split(' ', $callback_data); my $msgbuffer_whois = weechat::config_string(weechat::config_get('irc.msgbuffer.whois')); - DEBUG('info', 'weechat_hook_signal(): WHOIS'); # check for nick_regex @@ -1144,11 +1192,32 @@ sub irc_in2_whois_cb } my $use_regex = 0; - my $nicks_found = join( ", ", (get_nick_records('yes', 'nick', $nick, $server, $use_regex))); -# my $nicks_found = join( ", ", (get_nick_records('no', 'nick', $nick, $server, $use_regex))); + my $filename = get_script_filename(); + return weechat::WEECHAT_RC_OK if ($filename eq ""); + my $db_filename = weechat_dir(); + my $name = weechat::buffer_get_string($ptr_buffer,'localvar_name'); + DEBUG("info", "Start hook_process(), get additional for WHOIS() info from $nick with $user\@$host on $name"); + + weechat::hook_process_hashtable("perl", + { + "arg1" => $filename, + "arg2" => $db_filename, + "arg3" => 'additional_join_info', + "arg4" => $nick, + "arg5" => $user, + "arg6" => $host, + "arg7" => $server, + "arg8" => $options{'max_recursion'}, + "arg9" => $options{'ignore_guest_nicks'}, + "arg10" => $options{'guest_nick_regex'}, + }, 1000 * $options{'timeout'},"hook_process_get_nicks_records_cb","$nick $ptr_buffer 'dummy'"); + +# my $nicks_found = join( ", ", (get_nick_records('yes', 'nick', $nick, $server, $use_regex))); + + return weechat::WEECHAT_RC_OK; # only the given nick is returned? - return weechat::WEECHAT_RC_OK if ($nicks_found eq $nick or $nicks_found eq ""); +# return weechat::WEECHAT_RC_OK if ($nicks_found eq $nick or $nicks_found eq ""); # more than one nick was returned from sqlite my $prefix_network = weechat::prefix('network'); @@ -1259,7 +1328,21 @@ sub irc_in2_join_cb my $db_filename = weechat_dir(); DEBUG("info", "Start hook_process(), get additional info from $nick with $user\@$host on $name"); - weechat::hook_process("perl $filename $db_filename 'additional_join_info' '$nick' '$user' '$host' '$server' $options{'max_recursion'} $options{'ignore_guest_nicks'} '$options{'guest_nick_regex'}'", 1000 * $options{'timeout'},"hook_process_get_nicks_records_cb","$nick $buffer $my_tags"); + + weechat::hook_process_hashtable("perl", + { + "arg1" => $filename, + "arg2" => $db_filename, + "arg3" => 'additional_join_info', + "arg4" => $nick, + "arg5" => $user, + "arg6" => $host, + "arg7" => $server, + "arg8" => $options{'max_recursion'}, + "arg9" => $options{'ignore_guest_nicks'}, + "arg10" => $options{'guest_nick_regex'}, + }, 1000 * $options{'timeout'},"hook_process_get_nicks_records_cb","$nick $buffer $my_tags"); + } return weechat::WEECHAT_RC_OK; } diff --git a/perl/stats_bar.pl b/perl/stats_bar.pl index 70050764..db433957 100644 --- a/perl/stats_bar.pl +++ b/perl/stats_bar.pl @@ -4,6 +4,8 @@ # # History: # +# 2018-05-14, CrazyCat +# version 1.2 : added check for no swap # 2009-10-13, wishbone # version 1: initial release # @@ -64,7 +66,7 @@ use strict; -my $VERSION = "1.0"; +my $VERSION = "1.2"; weechat::register("stats_bar","wishbone",$VERSION,"GPL3","statistics bar", "", ""); my ($refresh,$old_refresh) = (10,0); @@ -75,6 +77,7 @@ inf_stats => "0", load_stats => "0", mem_stats => "0", + temp_stats => "0", ); my $stats_interface = ""; @@ -101,9 +104,9 @@ weechat::bar_item_new('inf_stats','stats_cb',"inf_stats"); weechat::bar_item_new('load_stats','stats_cb',"load_stats"); weechat::bar_item_new('mem_stats','stats_cb',"mem_stats"); +weechat::bar_item_new('temp_stats','stats_cb',"temp_stats"); refresh_stats_cb(); $refresh_hook = weechat::hook_timer($refresh*1000, 0,0,'refresh_stats_cb', ""); -$config_hook1 = weechat::hook_config("plugins.var.perl.stats_bar.*","read_settings",""); @@ -123,11 +126,10 @@ sub stats_cb { } sub refresh_stats_cb { -# get_inf_stats(); -# get_load_stats(); weechat::bar_item_update('inf_stats'); weechat::bar_item_update('load_stats'); weechat::bar_item_update('mem_stats'); + weechat::bar_item_update('temp_stats'); return weechat::WEECHAT_RC_OK; } @@ -190,7 +192,7 @@ sub get_inf_stats { chomp @lines; @lines = grep(/^\s*($stats_interface)/,@lines); if (@lines) { - if ($lines[0] !~ /^(\s*)(.*):(\d+)\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+(\d+)/) { + if ($lines[0] !~ /^(\s*)(.*):\s*(\d+)\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+(\d+)/) { } else { $last_in = $3; $last_out = $4; @@ -227,7 +229,11 @@ sub get_mem_stats { ($swaptotal) = ($lines[2] =~ /^SwapTotal:\s+(\d+)/); ($swapfree) = ($lines[3] =~ /^SwapFree:\s+(\d+)/); $mem_percentage = ($memfree/$memtotal)*100; - $swap_percentage = ($swapfree/$swaptotal)*100; + if ($swaptotal == 0) { + $swap_percentage = 0; + } else { + $swap_percentage = ($swapfree/$swaptotal)*100; + } $stats_data{mem_stats} = sprintf("m:%.0f%% s:%.0f%%",$mem_percentage, $swap_percentage); } else { @@ -238,4 +244,17 @@ sub get_mem_stats { } } - +sub get_temp_stats { + my @lines = (); + my ($tempmil,$temp); + if (!open(DEV, "< /sys/class/thermal/thermal_zone0/temp")) { + weechat::print("", "Failed to open /sys/class/thermal/thermal_zone0/temp"); + } else { + @lines = ; + close DEV; + chomp @lines; + ($tempmil) = ($lines[0] =~ /(\d+)/); + $temp = ($tempmil)/1000; + $stats_data{temp_stats} = sprintf("CPU:%2.1lf°C", $temp); + } +} diff --git a/perl/strmon.pl b/perl/strmon.pl index 7fc343f5..e395d16b 100644 --- a/perl/strmon.pl +++ b/perl/strmon.pl @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -# +# use IO::Socket::INET; use WWW::Curl::Easy; use URI::Escape; @@ -22,7 +22,7 @@ use strict; use vars qw( %cmode $strmon_buffer $command_buffer $strmon_help $version $daemon_file $strmon_tag ); -$version = "0.5.2"; +$version = "0.5.4"; weechat::register( "strmon", "Stravy", $version, "GPL", "Messages monitoring and notifications", "", "" ); @@ -34,7 +34,7 @@ $strmon_tag='strmon_message'; $daemon_file=<<'AFP'; -#!/usr/bin/perl +#!/usr/bin/perl # # Copyright (c) 2009 by Stravy # @@ -61,7 +61,7 @@ # # Default directory to look for images is $HOME/.config/strmon_daemon/pics # Default directory to look for sounds is $HOME/.config/strmon_daemon/sounds -# these directories can be manually changed in this script by modifying +# these directories can be manually changed in this script by modifying # variables $picdir and $sounddir # use strict; @@ -72,7 +72,7 @@ use vars qw($VERSION @ISA $picdir $sounddir); $VERSION = '0.2'; @ISA = qw(Net::Daemon); # to inherit from Net::Daemon - + $picdir=$ENV{'HOME'}."/.config/strmon_daemon/pics"; $sounddir=$ENV{'HOME'}."/.config/strmon_daemon/sounds"; @@ -103,7 +103,7 @@ $sock->close(); return; } - $line =~ s/\s+$//; # Remove CRLF + $line =~ s/\s+$//; # Remove CRLF my($rc); my $message=do_the_work($line); $rc = printf $sock ("$message\n"); @@ -138,7 +138,7 @@ } if ($unformated) { - $ret=do_unformated($ligne); + $ret=do_unformated($ligne); } else { $ret=do_message($mod,$pic,$sound,$bgcolor,$fgcolor,$chancolor,$nickcolor,$nchan,$chan,$nick,$ligne); @@ -173,16 +173,16 @@ $ret="Let's be silent"; return $ret; } else - { + { $text=format_text($text); - + my $message=""; $message="$chan "; $message.="$nick "; $pic=$picdir."/".$pic unless ($pic=~/^\//); - $message.="
"; + $message.="
"; $message.="$text"; - + unless ($mod==2) { my $command="notify-send \"$message\" "; @@ -196,7 +196,7 @@ } $ret="Notification done"; return $ret; - } + } } sub do_unformated @@ -207,7 +207,7 @@ { my @list=`ls $picdir`; chop @list; - $ret=join(",",@list); + $ret=join(",",@list); } elsif ($ligne=~/^daemon soundlist/) { my @list=`ls $sounddir`; @@ -257,20 +257,19 @@ Although it can be run by itself, full advantage of strmon is achieved using companion script strmon_daemon.pl which allow sound and osd notifications, it is embedded in this script and can be generated with command : - /strmon daemon write + /strmon daemon write that will write script strmon_daemon.pl into \$HOME. -By default use of this daemon is deactivated, you can print current state, +By default use of this daemon is deactivated, you can print current state, enable or disable it with command: /strmon daemon [on|off] The script strmon_daemon.pl uses programs mplayer (http://www.mplayerhq.hu/) -and qnotify (http://www.homac.de/cgi-bin/qnotify/index.pl), which must be -installed on local machine. +and notify-send, which must be installed on local machine. strmon_daemon.pl also needs the following files to exist on local machine : \$HOME/.config/strmon_daemon/pics/default.png \$HOME/.config/strmon_daemon/sounds/default.ogg -Principle of operation : +Principle of operation : 1) start notification daemon strmon_daemon.pl on local machine. 2) If you run weechat on the local machine, just load strmon.pl script into weechat and that's it. @@ -281,10 +280,12 @@ localhost:9867 on the local machine thus allowing strmon.pl weechat script to access the notification daemon on local machine. -In this version, notifo support has been added (smartphone notification), -see http://notifo.com/ to get an account. +In this version, notifo support (smartphone notification) has been replaced +by 'Notify My Android' as notifo no longer exist. +See https://www.notifymyandroid.com to get an account, unfortunately you will +only have 5 notifications per day for the free version. -strmon is configured by entering commands either with +strmon is configured by entering commands either with /strmon command in any buffer, either with command @@ -319,7 +320,7 @@ color {bgcolor|fgcolor|chanelcolor|nickcolor} color without argument : print default colors used for osd notifications with argument : set the specified color to be used as : - + bgcolor : color used as background fgcolor : text color used for the content of the message chanelcolor : text color used for chanel name @@ -331,15 +332,13 @@ ex : color fgcolor #ffffff color fgcolor black - notifo [on|off] - notifo test - notifo user [username] - notifo secret [API_secret] - without arguments : print current status of notifo use. + nma [on|off] + nma test + nma apikey [APIKEY] + without arguments : print current status of nma (Notify My Android) use. test : try to send a test notification - on|off : set use of notifo notifications - user [username] : print or set username for notifo account - secret [API_secret] : print or set user's api_secret for notifo account + on|off : set use of nma notifications + apikey [APIKEY] : print or set the apikey for nma account daemon [on|off] daemon write @@ -351,7 +350,7 @@ without arguments : print current status of daemon use. Note that if it is 'off' the only other possible commands are on|off, port and write. - on|off : set use of notification daemon + on|off : set use of notification daemon write : will write file \$HOME/strmon_daemon.pl test : try to contact the server with a test notification (bypassing default operation mode) @@ -371,15 +370,15 @@ sound sound soundfile without arguments : print current default sound - with argument : set the specified sound as default sound - + with argument : set the specified sound as default sound + nick nickname nick nickname test - nick nickname mode {normal|silent|nosound|novisual} + nick nickname mode {normal|silent|nosound|novisual} nick nickname pic picfile nick nickname sound soundfile nick nickname {bgcolor|fgcolor|chanelcolor|nickcolor} color - without arguments : show options specific to one nickname + without arguments : show options specific to one nickname with argument : set option specific to one nickname, or make a test notification from nickname. @@ -412,7 +411,7 @@ renumbered after that. number {normal|silent|nosound|novisual} : set the notification mode for the monitor given by its number - hl {on|off} [normal|silent|nosound|novisual] : activate/deactivate + hl {on|off} [normal|silent|nosound|novisual] : activate/deactivate monitoring of highlights and optionally set notification mode. tag add tagname [normal|silent|nosound|novisual] : monitor messages @@ -422,7 +421,7 @@ set notification mode. nick add nickname [normal|silent|nosound|novisual] : monitor messages from the specified nickname - + AFP $strmon_buffer = ""; @@ -447,7 +446,7 @@ sub strmon_buffer_input { my $cb_buffer=$_[1]; my $cb_data=$_[2]; - + #weechat::print($cb_buffer, $cb_data); $cb_data=~s/\s*$//; $cb_data=~s/^\s*//; @@ -470,10 +469,10 @@ sub strmon_buffer_input { # color strmon_color_command($args); - } elsif ($main eq 'notifo') + } elsif ($main eq 'nma') { - # notifo - strmon_notifo_command($args); + # nma + strmon_nma_command($args); } elsif ($main eq 'daemon') { # daemon @@ -495,7 +494,7 @@ sub strmon_buffer_input # filtertags strmon_filtertags_command($args); } elsif ($main eq 'filternicks') - { + { strmon_filternicks_command($args); } elsif ($main eq 'monitor') { @@ -568,52 +567,41 @@ sub strmon_color_command } -sub strmon_notifo_command +sub strmon_nma_command { my $args=shift @_; - my $usenotifo=weechat::config_get_plugin("usenotifo"); - my $user=weechat::config_get_plugin("notifo_user"); - my $secret=weechat::config_get_plugin("notifo_secret"); + my $usenma=weechat::config_get_plugin("usenma"); + my $apikey=weechat::config_get_plugin("nma_apikey"); $args=~/^(\S*)\s*(.*)$/; my $first=$1; my $second=$2; if ($first eq '') { # print usage - weechat::print_date_tags($command_buffer,time,$strmon_tag,"Use of notifo is currently : $usenotifo"); + weechat::print_date_tags($command_buffer,time,$strmon_tag,"Use of nma is currently : $usenma"); } elsif (($first eq 'on') || ($first eq 'off')) { - weechat::config_set_plugin('usenotifo',$first); - weechat::print_date_tags($command_buffer,time,$strmon_tag,"Use of notifo set to : $first"); - } elsif ($first eq 'user') - { - if ($second eq '') - { - weechat::print_date_tags($command_buffer,time,$strmon_tag,"Notifo user is : $user"); - } else - { - weechat::config_set_plugin('notifo_user',$second); - weechat::print_date_tags($command_buffer,time,$strmon_tag,"Notifo user set to : $second"); - } - } elsif ($first eq 'secret') + weechat::config_set_plugin('usenma',$first); + weechat::print_date_tags($command_buffer,time,$strmon_tag,"Use of nma set to : $first"); + } elsif ($first eq 'apikey') { if ($second eq '') { - weechat::print_date_tags($command_buffer,time,$strmon_tag,"Notifo secret is : $secret"); + weechat::print_date_tags($command_buffer,time,$strmon_tag,"nma apikey is : $apikey"); } else { - weechat::config_set_plugin('notifo_secret',$second); - weechat::print_date_tags($command_buffer,time,$strmon_tag,"Notifo secret set to : $second"); + weechat::config_set_plugin('nma_apikey',$second); + weechat::print_date_tags($command_buffer,time,$strmon_tag,"nma apikey set to : $second"); } } elsif ($first eq 'test') { - # test notifo + # test nma my $testdata='1 irc.#test Nickname : This is a test message'; - strmon_notifo_execute($testdata); - weechat::print_date_tags($command_buffer,time,$strmon_tag,"Notifo test done"); + strmon_nma_execute($testdata); + weechat::print_date_tags($command_buffer,time,$strmon_tag,"nma test done"); } else { - weechat::print_date_tags($command_buffer,time,$strmon_tag,"Wrong argument for notifo command."); + weechat::print_date_tags($command_buffer,time,$strmon_tag,"Wrong argument for nma command."); } return weechat::WEECHAT_RC_OK; @@ -642,17 +630,17 @@ sub strmon_daemon_command print $sock "daemon $first\n"; my $answer=$sock->getline(); $sock->shutdown(2); - my @liste=split(',',$answer); + my @liste=split(',',$answer); foreach (@liste) { weechat::print_date_tags($command_buffer,time,$strmon_tag,$_); - } + } } else { weechat::print_date_tags($command_buffer,time,$strmon_tag,"Problem contacting daemon"); } - - } elsif (($first eq 'play') || ($first eq 'show')) + + } elsif (($first eq 'play') || ($first eq 'show')) { if ($usedaemon ne 'on') { @@ -683,7 +671,7 @@ sub strmon_daemon_command my $nc=weechat::config_get_plugin('default_nick_color'); my $pi=weechat::config_get_plugin('default_picture'); my $so=weechat::config_get_plugin('default_sound'); - + if (my $sock = IO::Socket::INET->new(PeerAddr => 'localhost', PeerPort => $port+0, Proto => 'tcp')) @@ -708,9 +696,9 @@ sub strmon_daemon_command { weechat::print_date_tags($command_buffer,time,$strmon_tag,"Argument should be an integer"); } - + } elsif ( ($first eq '') || ($first eq 'on') || ($first eq 'off') ) - { + { if ($first eq '') { weechat::print_date_tags($command_buffer,time,$strmon_tag,"Use of daemon is currently : $usedaemon"); @@ -797,10 +785,10 @@ sub strmon_nick_command weechat::print_date_tags($command_buffer,time,$strmon_tag,"$nickname bgcolor : $bc\n$nickname fgcolor : $fc\n$nickname chanelcolor : $cc\n$nickname nickcolor : $nc"); } } - + } else { - # + # $args=~/^(\S+)\s*(.*)$/; my $first=$1; my $args=$2; @@ -834,7 +822,7 @@ sub strmon_nick_command if ($first eq 'test') { strmon_notify($cmode{$mo},$pi,$so,$bc,$fc,$cc,$nc,"1 irc.#test $nickname : This is a test message from $nickname"); - weechat::print_date_tags($command_buffer,time,$strmon_tag,"Test notification from $nickname done"); + weechat::print_date_tags($command_buffer,time,$strmon_tag,"Test notification from $nickname done"); } elsif ( ($first eq 'mode') && ($args ne '') ) { @@ -885,7 +873,7 @@ sub strmon_filtertags_command if ($args eq '') { my $tags=weechat::config_get_plugin('filtertags'); - weechat::print_date_tags($command_buffer,time,$strmon_tag,"Currently filtered tags : $tags"); + weechat::print_date_tags($command_buffer,time,$strmon_tag,"Currently filtered tags : $tags"); } elsif ($args=~/^list\s*(.*)$/) { $args=$1; @@ -916,7 +904,7 @@ sub strmon_filternicks_command if ($args eq '') { my $nicks=weechat::config_get_plugin('filternicks'); - weechat::print_date_tags($command_buffer,time,$strmon_tag,"Currently filtered nicks : $nicks"); + weechat::print_date_tags($command_buffer,time,$strmon_tag,"Currently filtered nicks : $nicks"); } elsif ($args=~/^list\s*(.*)$/) { $args=$1; @@ -951,7 +939,7 @@ sub strmon_monitor_command if ($args eq '') { # no arguments, just print the list - weechat::print_date_tags($command_buffer,time,$strmon_tag,weechat::color('chat')."Monitor highlights is ".weechat::color('magenta').$hl.weechat::color('chat').", with notification mode ".weechat::color('green').$hlm.weechat::color("reset")); + weechat::print_date_tags($command_buffer,time,$strmon_tag,weechat::color('chat')."Monitor highlights is ".weechat::color('magenta').$hl.weechat::color('chat').", with notification mode ".weechat::color('green').$hlm.weechat::color("reset")); weechat::print_date_tags($command_buffer,time,$strmon_tag,weechat::color('chat')."There are ".weechat::color('yellow').scalar(@taglist).weechat::color('chat')." tags monitored :".weechat::color("reset")); my $ntags=0; foreach (@taglist) @@ -1023,7 +1011,7 @@ sub strmon_monitor_command } } elsif ($second=~/\d+/) { - # + # if ( ($second > scalar(@taglist)) || ($second <= 0) ) { weechat::print_date_tags($command_buffer,time,$strmon_tag,"Given number does not match an existing tag"); @@ -1041,7 +1029,7 @@ sub strmon_monitor_command { $taglist[$second-1]="$t:$args"; weechat::config_set_plugin('monitortags',join(',',@taglist)); - weechat::print_date_tags($command_buffer,time,$strmon_tag,weechat::color('chat')."Notification mode set to ".weechat::color('green').$m.weechat::color('chat')." for tag ".weechat::color('magenta').$t.weechat::color('reset')); + weechat::print_date_tags($command_buffer,time,$strmon_tag,weechat::color('chat')."Notification mode set to ".weechat::color('green').$m.weechat::color('chat')." for tag ".weechat::color('magenta').$t.weechat::color('reset')); } } else { @@ -1079,7 +1067,7 @@ sub strmon_monitor_command } } elsif ($second=~/\d+/) { - # + # if ( ($second > scalar(@nicklist)) || ($second <= 0) ) { weechat::print_date_tags($command_buffer,time,$strmon_tag,"Given number does not match an existing nick"); @@ -1097,7 +1085,7 @@ sub strmon_monitor_command { $nicklist[$second-1]="$n:$args"; weechat::config_set_plugin('monitornicks',join(',',@nicklist)); - weechat::print_date_tags($command_buffer,time,$strmon_tag,weechat::color('chat')."Notification mode set to ".weechat::color('green').$m.weechat::color('chat')." for nick ".weechat::color('magenta').$n.weechat::color('reset')); + weechat::print_date_tags($command_buffer,time,$strmon_tag,weechat::color('chat')."Notification mode set to ".weechat::color('green').$m.weechat::color('chat')." for nick ".weechat::color('magenta').$n.weechat::color('reset')); } } else { @@ -1135,7 +1123,7 @@ sub strmon_monitor_command } } elsif ($second=~/\d+/) { - # + # if ( ($second > scalar(@buflist)) || ($second <= 0) ) { weechat::print_date_tags($command_buffer,time,$strmon_tag,"Given number does not match an existing monitored buffer"); @@ -1153,7 +1141,7 @@ sub strmon_monitor_command { $buflist[$second-1]="$b:$args"; weechat::config_set_plugin('monitorbuf',join(',',@buflist)); - weechat::print_date_tags($command_buffer,time,$strmon_tag,weechat::color('chat')."Notification mode changed to ".weechat::color('green').$args.weechat::color('chat')." for ".weechat::color('magenta').$b.weechat::color('reset')); + weechat::print_date_tags($command_buffer,time,$strmon_tag,weechat::color('chat')."Notification mode changed to ".weechat::color('green').$args.weechat::color('chat')." for ".weechat::color('magenta').$b.weechat::color('reset')); } } else { @@ -1168,7 +1156,7 @@ sub strmon_monitor_command } else { weechat::print_date_tags($command_buffer,time,$strmon_tag,"Bad argument for monitor command"); - } + } } return weechat::WEECHAT_RC_OK; } @@ -1191,22 +1179,16 @@ sub strmon_buffer_open sub strmon_default_settings { # set default values -# use notifo -if (! weechat::config_is_set_plugin("usenotifo")) +# use nma +if (! weechat::config_is_set_plugin("usenma")) { - weechat::config_set_plugin("usenotifo","off"); + weechat::config_set_plugin("usenma","off"); } -# notifo user -if (! weechat::config_is_set_plugin("notifo_user")) +# nma apikey +if (! weechat::config_is_set_plugin("nma_apikey")) { - weechat::config_set_plugin("notifo_user","nouser"); - } - -# notifo secret -if (! weechat::config_is_set_plugin("notifo_secret")) - { - weechat::config_set_plugin("notifo_secret","nosecret"); + weechat::config_set_plugin("nma_apikey","nokey"); } # use daemon @@ -1308,11 +1290,11 @@ sub strmon_buffer_close return weechat::WEECHAT_RC_OK; } -sub strmon_notifo_execute +sub strmon_nma_execute { (my $data) = @_; my $nout=weechat::string_remove_color($data,""); - # do not notify unformatted messages (such as channel messages when monitoring + # do not notify unformatted messages (such as channel messages when monitoring # a buffer) return unless($nout=~/^(\d+)\s(\S+)\s(\S+)\s:\s(.*)$/); my $nchan=$1; @@ -1324,23 +1306,26 @@ sub strmon_notifo_execute my @fields; # Try to find an url in the message to send with the notification - my $url="http://www.google.com/"; + my $url=""; if ($msg=~/(https?:\/\/\S+)/) { $url=$1; - } + } - push @fields,"label=".uri_escape($chan); - push @fields,"msg=".uri_escape($msg); - push @fields,"title=".uri_escape($nick); - push @fields,"uri=".uri_escape($url); + my $apikey=weechat::config_get_plugin("nma_apikey"); + push @fields,"apikey=$apikey"; + push @fields,"application=Weechat"; + push @fields,"event=".uri_escape("Chan:$chan Nick:$nick"); + push @fields,"description=".uri_escape($msg); + if ($url ne "") + { + push @fields,"url=".uri_escape($url); + } my $pdata=join("&",@fields); $curl->setopt(CURLOPT_POSTFIELDS, $pdata); - $curl->setopt(CURLOPT_URL, 'https://api.notifo.com/v1/send_notification'); - my $credential=weechat::config_get_plugin("notifo_user").":".weechat::config_get_plugin("notifo_secret"); - $curl->setopt(CURLOPT_USERPWD,$credential); + $curl->setopt(CURLOPT_URL, 'https://www.notifymyandroid.com/publicapi/notify'); $curl->setopt(CURLOPT_SSL_VERIFYPEER,0); # redirect response into variable $response_body @@ -1354,14 +1339,24 @@ sub strmon_notifo_execute # Write an output in case of problems if ($retcode == 0) { # parse result - $response_body=~/^\{"status":"(.+)","response_code":(\d+),"response_message":"(.+)"\}/; - my $rstatus=$1; - my $rcode=$2; - my $rmsg=$3; + $response_body=~/^.+code=\"(\d+)\"/; + my $rcode=$1; + + if ($rcode==200) { + # Message sent do not write anything + + } elsif ($rcode==400) { + weechat::print_date_tags($command_buffer,time,$strmon_tag,"nma error 400 : The data supplied is in the wrong format, invalid length or null"); + } elsif ($rcode==401) { + weechat::print_date_tags($command_buffer,time,$strmon_tag,"nma error 401 : None of the API keys provided were valid."); + } elsif ($rcode==402) { + weechat::print_date_tags($command_buffer,time,$strmon_tag,"nma error 402 : Maximum number of API calls per hour exceeded."); + } elsif ($rcode==500) { + weechat::print_date_tags($command_buffer,time,$strmon_tag,"nma error 500 : Internal server error. Please contact our support if the problem persists."); + } else { + weechat::print_date_tags($command_buffer,time,$strmon_tag,"Unknown nma error code : $rcode"); + } - if ($rstatus ne 'success') { - weechat::print_date_tags($command_buffer,time,$strmon_tag,"A notifo error happened : Status = $rstatus Code = $rcode Msg = $rmsg"); - } } else { weechat::print_date_tags($command_buffer,time,$strmon_tag,"A curl error happened : ".$curl->strerror($retcode)." ($retcode)"); @@ -1376,8 +1371,8 @@ sub strmon_notify (my $mode, my $pic, my $sound, my $bg_color, my $fg_color, my $chan_color, my $nick_color, my $data) = @_; my $ret=0; my $usedaemon=weechat::config_get_plugin('usedaemon'); - my $usenotifo=weechat::config_get_plugin('usenotifo'); - + my $usenma=weechat::config_get_plugin('usenma'); + # Daemon notification if ($usedaemon eq 'on') { @@ -1393,10 +1388,10 @@ sub strmon_notify } } - # notifo notification - if ($usenotifo eq 'on') + # nma notification + if ($usenma eq 'on') { - strmon_notifo_execute($data); + strmon_nma_execute($data); $ret=1; } return $ret; @@ -1433,7 +1428,7 @@ sub strmon_event } } - # get a "clean" buffer name + # get a "clean" buffer name my $bufname = weechat::string_remove_color(weechat::buffer_get_string($cb_bufferp, 'name') ,""); # get a "clean" nick name @@ -1453,7 +1448,7 @@ sub strmon_event } # initialize pic - my $picture=weechat::config_get_plugin("default_picture"); + my $picture=weechat::config_get_plugin("default_picture"); # initialize sound my $sound=weechat::config_get_plugin("default_sound"); @@ -1507,7 +1502,7 @@ sub strmon_event weechat::print_date_tags($strmon_buffer,time,$strmon_tag, $outstr); $mode = $mode | $cmode{weechat::config_get_plugin("globalmode")} | $cmode{$hlm}; strmon_notify($mode,$picture,$sound,$bg_color,$fg_color,$chan_color,$nick_color,$outstr); - return weechat::WEECHAT_RC_OK; + return weechat::WEECHAT_RC_OK; } @@ -1555,9 +1550,3 @@ sub strmon_event return weechat::WEECHAT_RC_OK; } - - - - - - diff --git a/perl/sysinfo.pl b/perl/sysinfo.pl index f3c23b19..9a7535e2 100644 --- a/perl/sysinfo.pl +++ b/perl/sysinfo.pl @@ -1,6 +1,9 @@ # -# Copyright (c) 2002-2005 David Rudie -# All rights reserved. +# SPDX-FileCopyrightText: 2011-2019 Nils Görs +# SPDX-FileCopyrightText: 2002-2005 David Rudie +# SPDX-FileCopyrightText: 2003-2006 Travis Morgan +# +# SPDX-License-Identifier: BSD-2-Clause # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -23,9 +26,6 @@ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # -# Several additions and fixes to this script were contributed by Travis -# Morgan and therefore are Copyright (c) 2003-2006 Travis Morgan -# # If you notice any bugs including spacing issues, wrong detection of hardware, # obvious features missing, etc, we both want to hear about them. If you make # this script work on other operating systems and/or architectures please send @@ -37,11 +37,16 @@ # # You can also reach Travis in #crd on efnet. # -# ported to WeeChat (http://www.weechat.org/) by Nils Görs. Copyright -# (c) 2011-2016 Nils Görs +# ported to WeeChat (http://www.weechat.org/) by Nils Görs. # +# 2025-04-22: 1.2.1 Sébastien Helleu +# : fix script license +# 2019-05-13: 1.2 nils_2 (freenode@nils_2) +# : make script compatible with kernel 5.x +# 2019-03-17: 1.1 nils_2 (freenode@nils_2) +# : fix: warning isn't numeric in subtraction # 2016-03-23: 1.0 nils_2 (freenode@nils_2) -# : fix: problem with armv7l Processor (eg cubietruck) +# : fix: problem with armv7l Processor (eg cubietruck) # 2015-05-16: 0.9 Keridos # add compatibility with linux kernel 4.x # 2015-02-15: 0.8 nils_2 (freenode@nils_2) @@ -54,8 +59,8 @@ # : based on sysinfo 2.81.21 # : bug with "armv5tel" in sysinfo 2.81.21 fixed (Version bumped to 2.81.22 and sent to maintainer) # 2012-01-07: 0.4 welwood08 -# : Fix distro+version for Ubuntu LTS -# : version bumped to 0.4 and copyright status changed +# : Fix distro+version for Ubuntu LTS +# : version bumped to 0.4 and copyright status changed # 2011-10-01: 0.3 nils_2 # : bar_item added (idea by Banton) # 2011-09-27: 0.2 nils_2 @@ -72,9 +77,9 @@ use strict; my $SCRIPT_NAME = "sysinfo"; -my $SCRIPT_VERSION = "1.0"; +my $SCRIPT_VERSION = "1.2.1"; my $SCRIPT_DESCR = "provides a system info command"; -my $SCRIPT_LICENSE = "GPL3"; +my $SCRIPT_LICENSE = "BSD-2-Clause"; my $SCRIPT_AUTHOR = "Nils Görs "; # Set up the arrays and variables first. @@ -234,7 +239,7 @@ my $d8 = 1 if $darwin && $osv =~ /^8\.\d+\.\d+/; my $d9 = 1 if $darwin && $osv =~ /^9\.\d+\.\d+/; my $l26 = 1 if $linux && $osv =~ /^2\.6/; -my $l3 = 1 if $linux && $osv =~ /^2\.7/ || $osv =~ /^3\./ || $osv =~ /^4\./; +my $l3 = 1 if $linux && $osv =~ /^2\.7/ || $osv =~ /^[3-5]\./; my $f_old = 1 if $freebsd && $osv =~ /^4\.1-/ || $osv =~ /^4\.0-/ || $osv =~ /^3/ || $osv =~ /^2/; my $isJail = `sysctl -n security.jail.jailed` if $freebsd; @@ -866,6 +871,8 @@ sub memoryusage { $varp = sprintf("%.2f", 100-($vard / ($vara-$vard) * 100)); $vara = sprintf("%.2f", $vara / 1024 / 1024); $vard = sprintf("%.2f", $vard / 1024 / 1024); + $vara =~y/,/./; + $vard =~y/,/./; return ($vara-$vard)."MB/".$vara."MB ($varp%)"; } diff --git a/perl/unset_unused.pl b/perl/unset_unused.pl index 9d0a8b67..ecb8f471 100644 --- a/perl/unset_unused.pl +++ b/perl/unset_unused.pl @@ -1,5 +1,5 @@ # -# Copyright (c) 2011-2013 by Nils Görs +# Copyright (c) 2011-2019 by Nils Görs # # unset script option(s) from not installed scripts # @@ -17,10 +17,10 @@ # along with this program. If not, see . # # +# 22-07-19: 0.5: fixed: scripts currently unloaded wasn't recognised (reported by squigz) +# 18-04-18: 0.4: add support for php and javascript # 14-10-04: 0.3: fixed: problem with unset options (reported by GermainZ) -# -# 13-07-27: 0.2 : added: support for guile_script -# +# 13-07-27: 0.2: added: support for guile_script # 11-08-28: 0.1 # # Development is currently hosted at @@ -29,23 +29,22 @@ use strict; my $PRGNAME = "unset_unused"; -my $VERSION = "0.3"; +my $VERSION = "0.5"; my $AUTHOR = "Nils Görs "; my $LICENCE = "GPL3"; my $DESCR = "unset script option(s) from not installed scripts (YOU ARE USING THIS SCRIPT AT YOUR OWN RISK!)"; my $weechat_version = ""; -my @option_list; my %script_plugins = ( - "python" => "python_script", - "perl" => "perl_script", - "ruby" => "ruby_script", - "tcl" => "tcl_script", - "lua" => "lua_script", - "guile" => "guile_script", + "python" => "python_script", + "perl" => "perl_script", + "ruby" => "ruby_script", + "tcl" => "tcl_script", + "lua" => "lua_script", + "guile" => "guile_script", + "php" => "php_script", + "javascript" => "javascript_script", ); -my $option_struct; -my %option_struct; my $str; # get installed scripts @@ -70,6 +69,8 @@ sub get_options my $key; my $number = 0; chop($str); + my $option_struct; + my %option_struct; foreach my $plugin (keys %script_plugins) { @@ -118,6 +119,7 @@ sub my_command_cb{ my ($getargs) = ($_[2]); return weechat::WEECHAT_RC_OK if ($getargs eq ""); + $str = ""; # reset get_scripts(); if ( $getargs eq "list") @@ -142,10 +144,11 @@ sub my_command_cb{ weechat::hook_command($PRGNAME, $DESCR, "list || unset\n", - " list : list all unused script options\n". - " unset : reset config options (without warning!)\n\n". - "If \"plugins.desc.\" exists, it will be removed, too.\n". - "save your settings with \"/save plugins\" or restore settings with \"/reload plugins\"". + " list: list all script options from not installed scripts (run this command first!)\n". + " unset: remove script options from not installed scripts (without warning!)\n\n". + "If \"plugins.desc.\" exists, it will be removed, too.\n\n". + "save your settings with \"/save plugins\" or restore settings with \"/reload plugins\"\n". + "You can also use \"/unset -mask\", see /help unset". "\n", "list %-||". "unset %-", diff --git a/perl/url_arza.pl b/perl/url_arza.pl index 30ad3f59..18ad9ea9 100644 --- a/perl/url_arza.pl +++ b/perl/url_arza.pl @@ -9,8 +9,12 @@ # Requires WeeChat >=0.3.6 -weechat::register('url_arza', 'arza ', '0.1', 'GPL3', 'Shorten long urls in buffers and input line', '', ''); -weechat::hook_print('', 'notify_message', '://', 0, 'url_in', ''); +# Changelog: +# 2013-07-27 0.1: initial release +# 2019-02-10 0.2: support multiple urls per line, fix default values, fix truncated prefix, increase timeout, simplify + +weechat::register('url_arza', 'arza ', '0.2', 'GPL3', 'Shorten long urls in buffers and input line', '', ''); +weechat::hook_print('', 'notify_message', '://', 1, 'url_in', ''); weechat::hook_command('url_arza', 'Shorten url in input line', '', '', '', 'command', ''); weechat::hook_config('plugins.var.perl.url_arza.*', 'set', ''); @@ -20,17 +24,18 @@ [ 'url', 'http://arza.us/s/?password=&url=', 'url for shortener, url to shorten is appended, the shortener should return the short url' ], [ 'url_append_command', '&id_min_length=1', 'string to append to the url when shortening in input line' ], [ 'url_append_incoming', '&id_min_length=2', 'string to append to the url when shortening incoming urls' ], - [ 'min_length', 100, 'minimum length for incoming urls to shorten, after http://' ], + [ 'min_length', 100, 'minimum length for incoming urls to shorten' ], ){ my ($name, $value, $description) = @$_; if(weechat::config_is_set_plugin($name)){ $settings{$name}=weechat::config_get_plugin($name); }else{ weechat::config_set_plugin($name, $value); weechat::config_set_desc_plugin($name, "$description (default: $value)"); + $settings{$name}=$value; } } -my $timeout=2000; +my $timeout=5000; my $delimiter="^\t"; @@ -42,14 +47,13 @@ sub command { my $buffer=$_[1]; my $input=weechat::buffer_get_string($buffer, 'input'); - ( my ($long) = $input=~/(https?:\/\/\S+)$/ ) || return weechat::WEECHAT_RC_OK; + my ($long) = $input=~/(https?:\/\/\S+)$/ or return weechat::WEECHAT_RC_OK; weechat::hook_process("url:$settings{url}".escape($long).$settings{'url_append_command'}, $timeout, 'command_fetch', "$buffer $long $input"); return weechat::WEECHAT_RC_OK; } sub command_fetch { $_[2] && return weechat::WEECHAT_RC_ERROR; - my ($buffer, $long, $input) = split(/ /, $_[0], 3); - my $short=$_[3]; + my ($buffer, $long, $input, $short) = (split(/ /, $_[0], 3), $_[3]); my $input_pos=weechat::buffer_get_integer($buffer, 'input_pos') + length($short) - length($long); $input=~s/\Q$long\E$/$short/; weechat::buffer_set($buffer, 'input', $input); @@ -59,15 +63,20 @@ sub url_in { my ($buffer, $displayed, $prefix, $line) = ($_[1], $_[4], $_[6], $_[7]); - $displayed && ( my ($long) = $line=~/(https?:\/\/\S{$settings{'min_length'},})/ ) || return weechat::WEECHAT_RC_OK; - weechat::hook_process("url:$settings{url}".escape($long).$settings{'url_append_incoming'}, $timeout, 'url_in_fetch', "$buffer $prefix"); + $displayed or return weechat::WEECHAT_RC_OK; + for my $long ($line=~/(https?:\/\/\S+)/g){ + if(length($long) >= $settings{'min_length'}){ + weechat::hook_process("url:$settings{'url'}".escape($long).$settings{'url_append_incoming'}, $timeout, 'url_in_fetch', "$buffer $prefix"); + } + } return weechat::WEECHAT_RC_OK; } sub url_in_fetch { $_[2] && return weechat::WEECHAT_RC_ERROR; - my ($buffer, $prefix) = split(/ /, $_[0], 2); - my $short=$_[3]; - weechat::print_date_tags($buffer, 0, 'no_log', ' ' x length(weechat::string_remove_color($prefix, '')) . $delimiter . $short); + my ($buffer, $prefix, $short) = (split(/ /, $_[0], 2), $_[3]); + weechat::print_date_tags( $buffer, 0, 'no_log', + ' ' x ( weechat::config_string(weechat::config_get('weechat.look.prefix_align')) eq "none" ? length(weechat::string_remove_color($prefix, '')) : 0 ) + . $delimiter . $short ); return weechat::WEECHAT_RC_OK; } @@ -76,4 +85,3 @@ sub set { $settings{substr($_[1], length('plugins.var.perl.url_arza.'))}=$_[2]; return weechat::WEECHAT_RC_OK; } - diff --git a/perl/weetris.pl b/perl/weetris.pl deleted file mode 100644 index 3669a055..00000000 --- a/perl/weetris.pl +++ /dev/null @@ -1,492 +0,0 @@ -# -# Copyright (C) 2008-2011 Sebastien Helleu -# Copyright (C) 2009 drubin -# Copyright (C) 2010-2011 Trashlord -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -# Tetris game for WeeChat. -# -# History: -# 2011-02-28, Trashlord : -# version 0.9: add playing time display -# 2010-10-08, Trashlord : -# version 0.8: add best score and best level statistics -# 2009-12-17, Sebastien Helleu : -# version 0.7: add levels, fix bugs with pause -# 2009-12-16, drubin : -# version 0.6: add key for pause, basic doc and auto jump to buffer -# 2009-06-21, Sebastien Helleu : -# version 0.5: fix bug with weetris buffer after /upgrade -# 2009-05-02, Sebastien Helleu : -# version 0.4: sync with last API changes, fix problem with key alt-n -# 2008-11-14, Sebastien Helleu : -# version 0.3: minor code cleanup -# 2008-11-12, Sebastien Helleu : -# version 0.2: hook timer only when weetris buffer is open -# 2008-11-05, Sebastien Helleu : -# version 0.1: first official version -# 2008-04-30, Sebastien Helleu : -# script creation - -use strict; - -my $version = "0.9"; - -my $weetris_buffer = ""; -my $timer = ""; -my $level = 1; -my $max_level = 10; - -my ($nbx, $nby) = (10, 20); -my $start_y = 0; -my @matrix = (); - -# ------------------------- ------------------------- -# | | | | | | | | | | -# |32768|16384| 8192| 4096| | 8 | 128 | 2048|32768| -# | | | | | | | | | | -# ------------------------- ------------------------- -# | | | | | | | | | | -# | 2048| 1024| 512 | 256 | | 4 | 64 | 1024|16384| -# | | | | | after | | | | | -# ------------------------- rotate ------------------------- -# | | | | | ===> | | | | | -# | 128 | 64 | 32 | 16 | | 2 | 32 | 512 | 8192| -# | | | | | | | | | | -# ------------------------- ------------------------- -# | | | | | | | | | | -# | 8 | 4 | 2 | 1 | | 1 | 16 | 256 | 4096| -# | | | | | | | | | | -# ------------------------- ------------------------- - -my @items = (1024+512+64+32, # O - 2048+1024+512+256, # I - 2048+1024+512+64, # T - 2048+1024+512+128, # L - 2048+1024+512+32, # J - 1024+512+128+64, # S - 2048+1024+64+32, # Z - ); -my @item_color = ("red", "blue", "brown", "green", "brown", "cyan", "magenta"); -my @item_x_inc = (3, 2, 1, 0, 3, 2, 1, 0, 3, 2, 1, 0, 3, 2, 1, 0); -my @item_y_inc = (3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0); -my @item_rotation = (4096, 256, 16, 1, 8192, 512, 32, 2, 16384, 1024, 64, 4, 32768, 2048, 128, 8); - -my $playing = 0; -my $paused = 0; -my $lines = 0; -my ($item_x, $item_y) = (0, 0); -my $item_number = 0; -my $item_form = 0; -my $title = "WeeTris.pl $version - enjoy! | Keys: arrows: move/rotate, alt-N: new game, alt-P: pause"; -my $mlevel = 1; -my $mlines = 0; -my ($play_minutes, $play_seconds, $play_starting_time) = (0, 0, time()); # Indicates playing time. -my $total_seconds = time(); -my $time_display_timer = ""; - -sub buffer_close -{ - $weetris_buffer = ""; - if ($timer ne "") - { - weechat::unhook($timer); - $timer = ""; - } - if (defined $time_display_timer) - { - weechat::unhook($time_display_timer); - ($play_minutes, $play_seconds, $play_starting_time) = (0, 0, 0); - $time_display_timer = undef; - } - weechat::print("", "Thank you for playing WeeTris!"); - return weechat::WEECHAT_RC_OK; -} - -sub display_line -{ - my $y = $_[0]; - my $str = " │"; - if ($paused eq 1) - { - if ($y == $nby / 2) - { - my $paused = " " x $nbx; - my $index = (($nbx * 2) - 6) / 2; - substr($paused, $index, 6) = "PAUSED"; - $str .= $paused; - } - else - { - $str .= " " x $nbx; - } - } - else - { - for (my $x = 0; $x < $nbx; $x++) - { - my $char = substr($matrix[$y], $x, 1); - if ($char eq " ") - { - $str .= weechat::color(",default"); - } - else - { - $str .= weechat::color(",".$item_color[$char]); - } - $str .= " "; - } - } - $str .= weechat::color(",default")."│"; - weechat::print_y($weetris_buffer, $start_y + $y + 1, $str); -} - -sub weetris_display_playing_time -{ - $total_seconds = time() - $play_starting_time; - $play_minutes = int($total_seconds / 60); - $play_seconds = int($total_seconds % 60); - $total_seconds++; - $play_seconds = "0".$play_seconds if ($play_seconds < 10); - $play_minutes = "0".$play_minutes if ($play_minutes < 10); - weechat::print_y($weetris_buffer, $start_y + $nby + 6, " Playing time : $play_minutes:$play_seconds"); -} - -sub display_level_lines -{ - my $plural = ""; - my $dash = "-" x 22; - $plural = "s" if ($lines > 1); - my $str = sprintf(" Level %-3d %6d line%s", $level, $lines, $plural); - weechat::print_y($weetris_buffer, $start_y + $nby + 2, $str); - weechat::print_y($weetris_buffer, $start_y + $nby + 3, " $dash"); - weechat::print_y($weetris_buffer, $start_y + $nby + 4, " Highest level: $mlevel"); - weechat::print_y($weetris_buffer, $start_y + $nby + 5, " Max lines : $mlines"); -} - -sub apply_item -{ - my $char = " "; - $char = $item_number if ($_[0] eq 1); - for (my $i = 0; $i < 16; $i++) - { - if (($item_form & (1 << $i)) > 0) - { - substr($matrix[$item_y + $item_y_inc[$i]], $item_x + $item_x_inc[$i], 1) = $char; - } - } -} - -sub display_all -{ - apply_item(1); - - # bar on top - weechat::print_y($weetris_buffer, $start_y, " ┌".("──" x $nbx)."┐"); - - # middle - for (my $y = 0; $y < $nby; $y++) - { - display_line($y); - } - - # bottom bar - weechat::print_y($weetris_buffer, $start_y + $nby + 1, " └".("──" x $nbx)."┘"); - - apply_item(0); -} - -sub new_form -{ - $item_number = int(rand($#items + 1)); - $item_form = $items[$item_number]; - $item_x = ($nbx / 2) - 2; - $item_y = 0; -} - -sub init_timer -{ - weechat::unhook($timer) if ($timer ne ""); - my $delay = 700 - (($level - 1) * 60); - $delay = 100 if ($delay < 100); - $timer = weechat::hook_timer($delay, 0, 0, "weetris_timer", ""); -} - -sub new_game -{ - weechat::print_y($weetris_buffer, $start_y + $nby + 2, ""); - for (my $y = 0; $y < $nby; $y++) - { - $matrix[$y] = " " x $nbx; - } - new_form(); - $playing = 1; - $paused = 0; - $lines = 0; - $level = 1; - $play_starting_time = time(); - weechat::print_y($weetris_buffer, $start_y + $nby + 6, " Playing time : 00:00"); - init_timer(); - $time_display_timer = weechat::hook_timer(1000, 0, 0, "weetris_display_playing_time", ""); - display_all(); - display_level_lines(); -} - -sub rotation -{ - my $form = $_[0]; - my $new_form = 0; - for (my $i = 0; $i < 16; $i++) - { - if (($form & (1 << $i)) > 0) - { - $new_form = $new_form | $item_rotation[$i]; - } - } - return $new_form; -} - -sub is_possible -{ - my ($new_x, $new_y, $new_form) = ($_[0], $_[1], $_[2]); - for (my $i = 0; $i < 16; $i++) - { - if (($new_form & (1 << $i)) > 0) - { - return 0 if (($new_x + $item_x_inc[$i] < 0) - || ($new_x + $item_x_inc[$i] >= $nbx) - || ($new_y + $item_y_inc[$i] < 0) - || ($new_y + $item_y_inc[$i] >= $nby) - || (substr($matrix[$new_y + $item_y_inc[$i]], $new_x + $item_x_inc[$i], 1) ne " ")); - } - } - return 1; -} - -sub remove_completed_lines -{ - my $y = $nby - 1; - my $lines_removed = 0; - while ($y >= 0) - { - if (index($matrix[$y], " ") == -1) - { - for (my $i = $y; $i >= 0; $i--) - { - if ($i == 0) - { - $matrix[$i] = " " x $nbx; - } - else - { - $matrix[$i] = $matrix[$i - 1]; - } - } - # Removes the line and increases the number of lines made in the game in $lines - $lines++; - $lines_removed = 1; - if ($lines > $mlines) - { - set_best("max_lines", $lines); - $mlines = $lines; - } - } - else - { - $y--; - } - } - if ($lines_removed) - { - my $new_level = int(($lines / 10) + 1); - $new_level = $max_level if ($new_level > $max_level); - if ($new_level != $level) - { - # Next level - $level = $new_level; - if ($level > $mlevel) - { - set_best("max_level", $level); - $mlevel = $level; - } - init_timer(); - } - display_level_lines(); - } -} - -sub end_of_item -{ - apply_item(1); - new_form(); - if (is_possible($item_x, $item_y, $item_form)) - { - remove_completed_lines(); - } - else - { - $item_form = 0; - $playing = 0; - $paused = 0; - if (defined $time_display_timer) - { - weechat::unhook($time_display_timer); - $time_display_timer = undef; - } - weechat::print_y($weetris_buffer, $start_y + $nby + 2, ">> End of game, score: $lines lines, level $level (alt-N to restart) <<"); - } -} - -sub weetris_init -{ - $weetris_buffer = weechat::buffer_search("perl", "weetris"); - if ($weetris_buffer eq "") - { - $weetris_buffer = weechat::buffer_new("weetris", "", "", "buffer_close", ""); - } - if ($weetris_buffer ne "") - { - weechat::buffer_set($weetris_buffer, "type", "free"); - weechat::buffer_set($weetris_buffer, "title", $title); - weechat::buffer_set($weetris_buffer, "key_bind_meta2-A", "/weetris up"); - weechat::buffer_set($weetris_buffer, "key_bind_meta2-B", "/weetris down"); - weechat::buffer_set($weetris_buffer, "key_bind_meta2-D", "/weetris left"); - weechat::buffer_set($weetris_buffer, "key_bind_meta2-C", "/weetris right"); - weechat::buffer_set($weetris_buffer, "key_bind_meta-n", "/weetris new_game"); - weechat::buffer_set($weetris_buffer, "key_bind_meta-p", "/weetris pause"); - new_game(); - weechat::buffer_set($weetris_buffer, "display", "1"); - } -} - -sub weetris -{ - my ($data, $buffer, $args) = ($_[0], $_[1], $_[2]); - if ($weetris_buffer ne "") - { - weechat::buffer_set($weetris_buffer, "display", "1"); - } - if ($weetris_buffer eq "") - { - weetris_init(); - } - - if ($args eq "new_game") - { - new_game(); - } - - if ($args eq "pause") - { - if ($playing eq 1) - { - $paused ^= 1; - display_all(); - } - } - - if (($playing eq 1) && ($paused eq 0)) - { - if ($args eq "up") - { - my $new_form = rotation($item_form); - if (is_possible($item_x, $item_y, $new_form)) - { - $item_form = $new_form; - display_all(); - } - } - if ($args eq "down") - { - if (is_possible($item_x, $item_y + 1, $item_form)) - { - $item_y++; - display_all(); - } - else - { - end_of_item(); - } - } - if ($args eq "left") - { - if (is_possible($item_x - 1, $item_y, $item_form)) - { - $item_x--; - display_all(); - } - } - if ($args eq "right") - { - if (is_possible($item_x + 1, $item_y, $item_form)) - { - $item_x++; - display_all(); - } - } - } - - return weechat::WEECHAT_RC_OK; -} - -sub weetris_timer -{ - if (($weetris_buffer ne "") && ($playing eq 1) && ($paused eq 0)) - { - if (is_possible($item_x, $item_y + 1, $item_form)) - { - $item_y++; - } - else - { - end_of_item(); - } - display_all(); - } - return weechat::WEECHAT_RC_OK; -} -weechat::register("weetris", "Sebastien Helleu ", - $version, "GPL3", "Tetris game for WeeChat, yeah!", "", ""); -weechat::hook_command("weetris", "Run WeeTris", "", - "Keys:\n". - " arrow up: rotate current item\n". - " arrow left: move item to the left\n". - "arrow right: move item to the right\n". - " alt+n: restart the game\n". - " alt+p: pause current game", - "", "weetris", ""); -$weetris_buffer = weechat::buffer_search("perl", "weetris"); -if ($weetris_buffer ne "") -{ - weetris_init(); -} - -# Best level statistics and max lines achieved - -sub get_best -{ - my $arg = shift; - return weechat::config_get_plugin($arg); -} - -sub set_best -{ - my ($arg, $value) = @_; - weechat::config_set_plugin($arg, $value); -} - -$mlines = get_best("max_lines") || 0; -$mlevel = get_best("max_level") || 1; diff --git a/perl/yaaa.pl b/perl/yaaa.pl index 71962b26..8e6e13e9 100644 --- a/perl/yaaa.pl +++ b/perl/yaaa.pl @@ -1,9 +1,12 @@ +# SPDX-FileCopyrightText: 2009 jnbek +# +# SPDX-License-Identifier: GPL-3.0-or-later +# # YaAA: Yet Another Auto Away Script written in Perl -# Copyright (c) 2009 by jnbek # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, @@ -12,8 +15,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# along with this program. If not, see . # #Configuration Options: #/set plugins.var.perl.yaaa.autoaway 10 @@ -28,7 +30,7 @@ # 0.3: Fixed more config bugs. Fully Tested. # TODO: Add a nick changer. -my $version = '0.3'; +my $version = '0.3.1'; my $interval = 5; # Seconds between checks my $autoaway = 10; # Minutes til away my $message = "AFK"; diff --git a/python/aesthetic.py b/python/aesthetic.py new file mode 100644 index 00000000..988071d6 --- /dev/null +++ b/python/aesthetic.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# +# Script Name: aesthetic.py +# Script Author: Wojciech Siewierski +# Script License: GPL3 +# Contact: vifon @ irc.freenode.net + +SCRIPT_NAME = 'aesthetic' +SCRIPT_AUTHOR = 'Wojciech Siewierski' +SCRIPT_VERSION = '1.0.6' +SCRIPT_LICENSE = 'GPL3' +SCRIPT_DESC = 'Make messages more A E S T H E T I C A L L Y pleasing.' + +import_ok = True + +try: + import weechat +except ImportError: + print('This script must be run under WeeChat') + print('You can obtain a copy of WeeChat, for free, at https://weechat.org') + import_ok = False + +weechat_version = 0 + +import shlex +import sys + +def aesthetic_(args): + for arg in args: + try: + arg = arg.decode('utf8') + except AttributeError: + pass + yield " ".join(arg.upper()) + for n, char in enumerate(arg[1:]): + yield " ".join(" "*(n+1)).join(char.upper()*2) + +def aesthetic(args): + if sys.version_info < (3,): + return (x.encode('utf8') for x in aesthetic_(args)) + else: + return aesthetic_(args) + +def aesthetic_cb(data, buffer, args): + for x in aesthetic(shlex.split(args)): + weechat.command(buffer, x) + return weechat.WEECHAT_RC_OK + +if __name__ == "__main__" and import_ok: + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + weechat_version = weechat.info_get("version_number", "") or 0 + weechat.hook_command( + "aesthetic", + """Format a message like this: + +E X A M P L E +X X +A A +M M +P P +L L +E E + +Each argument is formatted separately, use sh-like quotes for grouping. For example '/aesthetic foo bar' will send two such blocks while '/aesthetic "foo bar"' would send one larger one. + +Use with care to not cause undesirable message spam.""", + "message", "", + "", + "aesthetic_cb", "" + ) diff --git a/python/aformat.py b/python/aformat.py new file mode 100644 index 00000000..bd8d096a --- /dev/null +++ b/python/aformat.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Hairo R. Carela +# +# Everyone is permitted to copy and distribute verbatim or modified +# copies of this license document, and changing it is allowed as long +# as the name is changed. +# +# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION +# +# 0. You just DO WHAT THE FUCK YOU WANT TO. +# +# Alternate way of text formatting, useful for relays without text formatting +# features (Glowingbear, WeechatAndroid, etc) +# +# Usage: +# /aformat *text* for bold text +# /aformat /text/ for italic text +# /aformat _text_ for underlined text +# /aformat |text| for reversed (black on white) text +# +# History: +# 2016-09-24: +# v0.1: Initial release +# 2018-06-19: +# v0.2: py3k-ok +# +# TODO: +# - Colors support + +import sys + +try: + import weechat + from weechat import WEECHAT_RC_OK + import_ok = True +except ImportError: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") + import_ok = False + +SCRIPT_NAME = "aformat" +SCRIPT_AUTHOR = "Hairo R. Carela " +SCRIPT_VERSION = "0.2" +SCRIPT_LICENSE = "WTFPL" +SCRIPT_DESC = ("Alternate way of text formatting, see /help for instructions") + +PY3 = sys.version > '3' + +class format: + # Special byte sequences, using weechat.color("stuff") had some unwanted + # results, i'll look into it if needed. Colors are unused for now + BOLD = '\x02' + ITALIC = '\x1D' + UNDERLINE = '\x1F' + REVERSE = '\x16' + END = '\x0F' + +if PY3: + unichr = chr + def send(buf, text): + weechat.command(buf, "/input send {}".format(text)) +else: + def send(buf, text): + weechat.command(buf, "/input send {}".format(text.encode("utf-8"))) + +def cb_aformat_cmd(data, buf, args): + if not PY3: + args = args.decode("utf-8") + + # Get the indexes of the separators (*/_|) in the string + bolds = [i for i, ltr in enumerate(args) if ltr == "*"] + italics = [i for i, ltr in enumerate(args) if ltr == "/"] + underlines = [i for i, ltr in enumerate(args) if ltr == "_"] + reverses = [i for i, ltr in enumerate(args) if ltr == "|"] + + if len(bolds) != 0: + for i, v in enumerate(bolds): + if i%2 == 0: + args = args[:v] + format.BOLD + args[v+1:] + else: + args = args[:v] + format.END + args[v+1:] + + if len(italics) != 0: + for i, v in enumerate(italics): + if i%2 == 0: + args = args[:v] + format.ITALIC + args[v+1:] + else: + args = args[:v] + format.END + args[v+1:] + + if len(underlines) != 0: + for i, v in enumerate(underlines): + if i%2 == 0: + args = args[:v] + format.UNDERLINE + args[v+1:] + else: + args = args[:v] + format.END + args[v+1:] + + if len(reverses) != 0: + for i, v in enumerate(reverses): + if i%2 == 0: + args = args[:v] + format.REVERSE + args[v+1:] + else: + args = args[:v] + format.END + args[v+1:] + + send(buf, args) + return weechat.WEECHAT_RC_OK + + +if import_ok and __name__ == "__main__": + weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, + SCRIPT_LICENSE, SCRIPT_DESC, '', '') + weechat.hook_command("aformat", "Alternate way of text formatting, useful for relays without text formatting features (Glowingbear, WeechatAndroid, etc)", + "text <*/_|> text <*/_|> more text", + " *: bold text\n" + " /: italic text\n" + " _: underlined text\n" + " |: reversed (black on white) text\n\n" + " eg.: typing: /aformat This /must/ be the *work* of an _enemy_ |stand|\n" + " will output: This {0}must{4} be the {1}work{4} of an {2}enemy{4} {3}stand{4}".format(weechat.color("italic"), weechat.color("bold"), weechat.color("underline"), weechat.color("reverse"), weechat.color("reset")), + "", "cb_aformat_cmd", "") diff --git a/python/allquery.py b/python/allquery.py deleted file mode 100644 index e789bd6a..00000000 --- a/python/allquery.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2011-2013 by F. Besser -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# -# History: -# 2013-09-01, nils_2@freenode.#weechat: -# version 0.2: add support of servername for "-exclude" -# : make script behave like /allchan and /allserver command -# : add function "-current" -# : case-insensitive search for query/server -# -# 2011-09-05, F. Besser : -# version 0.1: script created -# -# Development is on: -# https://github.com/fbesser/weechat_scripts -# -# (this script requires WeeChat 0.3.0 or newer) -# - - -SCRIPT_NAME = "allquery" -SCRIPT_AUTHOR = "fbesser" -SCRIPT_VERSION = "0.2" -SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Executes command on all irc query buffer" - -SCRIPT_COMMAND = "allquery" - -import_ok = True - -try: - import weechat -except ImportError: - print('This script must be run under WeeChat.') - print('Get WeeChat now at: http://www.weechat.org/') - import_ok = False - -try: - import re -except ImportError, message: - print('Missing package(s) for %s: %s' % (SCRIPT_NAME, message)) - import_ok = False - - -def make_list(argument): - """ Make a list out of argument string of format -argument=value0,value1""" - arglist = argument.lower().split("=", 1) - arguments = arglist[1].split(",") - return arguments - -def allquery_command_cb(data, buffer, args): - """ Callback for /allquery command """ - args = args.strip() - if args == "": - weechat.command("", "/help %s" % SCRIPT_COMMAND) - return weechat.WEECHAT_RC_OK - argv = args.split(" ") - - exclude_nick = None - current_server = None - - if '-current' in argv: - current_server = weechat.buffer_get_string(weechat.current_buffer(), "localvar_server") - # remove "-current" + whitespace from argumentlist - args = args.replace("-current", "") - args = args.lstrip() - argv.remove("-current") - - # search for "-exclude" in arguments - i = 0 - for entry in argv[0:]: - if entry.startswith("-exclude="): - exclude_nick = make_list(argv[i]) - command = " ".join(argv[i+1::]) - break - i +=1 - else: - command = args - - # no command found. - if not command: - return weechat.WEECHAT_RC_OK - - if not command.startswith("/"): - command = "/%s" % command - - infolist = weechat.infolist_get("buffer", "", "") - while weechat.infolist_next(infolist): - if weechat.infolist_string(infolist, "plugin_name") == "irc": - ptr = weechat.infolist_pointer(infolist, "pointer") - server = weechat.buffer_get_string(ptr, "localvar_server") - query = weechat.buffer_get_string(ptr, "localvar_channel") - execute_command = re.sub(r'\$nick', query, command) - if weechat.buffer_get_string(ptr, "localvar_type") == "private": - if current_server is not None: - if server == current_server: - exclude_nick_and_server(ptr,query,server,exclude_nick,execute_command) - else: - exclude_nick_and_server(ptr,query,server,exclude_nick,execute_command) - weechat.infolist_free(infolist) - return weechat.WEECHAT_RC_OK - - -def exclude_nick_and_server(ptr, query, server, exclude_nick, execute_command): - server = "%s.*" % server # servername + ".*" - if exclude_nick is not None: - if not query.lower() in exclude_nick and not server.lower() in exclude_nick: - weechat.command(ptr, execute_command) - else: - weechat.command(ptr, execute_command) - - -if __name__ == '__main__' and import_ok: - if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, - SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - - weechat.hook_command(SCRIPT_COMMAND, SCRIPT_DESC, - '[-current] [-exclude=[,...]] []', - ' -current: execute command for query of current server only\n' - ' -exclude: exclude some querys and/or server from executed command\n' - ' command: command executed in query buffers\n' - ' arguments: arguments for command (special variables $nick will be replaced by its value)\n\n' - 'Examples:\n' - ' close all query buffers:\n' - ' /' + SCRIPT_COMMAND + ' buffer close\n' - ' close all query buffers, but don\'t close FlashCode:\n' - ' /' + SCRIPT_COMMAND + ' -exclude=FlashCode buffer close\n' - ' close all query buffers, except for server freenode:\n' - ' /' + SCRIPT_COMMAND + ' -exclude=freenode.* buffer close\n' - ' msg to all query buffers:\n' - ' /' + SCRIPT_COMMAND + ' say Hello\n' - ' notice to all query buffers:\n' - ' /' + SCRIPT_COMMAND + ' notice $nick Hello', - '%(commands)', - 'allquery_command_cb', '') diff --git a/python/alternatetz.py b/python/alternatetz.py index 468efbfc..5d3f9570 100644 --- a/python/alternatetz.py +++ b/python/alternatetz.py @@ -7,6 +7,10 @@ # plugin to get alternate timezones in a weechat bar # # Changelog: +# 0.4 Sébastien Helleu +# Remove trailing tabs +# 0.3 Pol Van Aubel +# Fix tab/space usage for Python3 compatibility # 0.2 Added help, and multiple timezeones # 0.1 first version # @@ -17,15 +21,15 @@ SCRIPT_NAME = "alternatetz" SCRIPT_AUTHOR = "Chmouel Boudjnah " -SCRIPT_VERSION = "0.2" +SCRIPT_VERSION = "0.4" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Display Alternate Time from different Timezones" SCRIPT_COMMAND = 'alternatetz' -OPTIONS = { -'timezone' : ('GMT', 'list of timezones to display. The list is comprised of space separated list timezones using the Olson tz database'), -'timeformat' : ('%H:%M', 'strftime compatible format') +OPTIONS = { +'timezone': ('GMT', 'list of timezones to display. The list is comprised of space separated list timezones using the Olson tz database'), +'timeformat': ('%H:%M', 'strftime compatible format') } def alternatetz_item_cb(*kwargs): @@ -33,7 +37,7 @@ def alternatetz_item_cb(*kwargs): tznames = OPTIONS['timezone'].split() for tzname in tznames: tz = pytz.timezone(tzname) - ret += tz.zone + ': ' + datetime.datetime.now(tz).strftime(OPTIONS['timeformat']) + ' ' + ret += tz.zone + ': ' + datetime.datetime.now(tz).strftime(OPTIONS['timeformat']) + ' ' return ret[:-1] def alternatetz_timer_cb(*kwargs): diff --git a/python/announce_url_title.py b/python/announce_url_title.py index 1b802d3f..dfb01a13 100644 --- a/python/announce_url_title.py +++ b/python/announce_url_title.py @@ -25,7 +25,8 @@ # Explanation about ignores: # * plugins.var.python.announce_url_title.ignore_buffers: # Comma separated list of patterns for define ignores. -# URLs from channels where its name matches any of these patterns will be ignored. +# URLs from channels where its name matches any of these patterns will be +# ignored. # Wildcards '*', '?' and char groups [..] can be used. # An ignore exception can be added by prefixing '!' in the pattern. # @@ -35,25 +36,30 @@ # except from #ubuntu-offtopic # # * plugins.var.python.announce_url_title.url_ignore -# simply does partial match, so specifying 'google' will ignore every url with the word google in it +# simply does partial match, so specifying 'google' will ignore every url +# with the word google in it # # # History: # +# 2021-06-05, Sébastien Helleu +# version 19: make script compatible with Python 3, fix PEP8 errors # 2014-05-10, Sébastien Helleu # version 18: change hook_print callback argument type of displayed/highlight # (WeeChat >= 1.0) # 2013-11-07, excalibr -# version 17: add more characters to exclude in escaping (this fix problem with youtube urls) +# version 17: add more characters to exclude in escaping (this fix problem +# with youtube urls) # 2012-11-15, xt # version 16: improve escaping # 2011-09-04, Deltafire -# version 15: fix remote execution exploit due to unescaped ' character in urls; -# small bug fix for version 14 changes +# version 15: fix remote execution exploit due to unescaped ' character in +# urls; small bug fix for version 14 changes # 2011-08-23, Deltafire # version 14: ignore filtered lines # 2011-03-11, Sébastien Helleu -# version 13: get python 2.x binary for hook_process (fix problem when python 3.x is default python version) +# version 13: get python 2.x binary for hook_process (fix problem when +# python 3.x is default python version) # 2010-12-10, xt # version 12: add better ignores (code based on m4v inotify.py) # 2010-11-02, xt @@ -80,190 +86,226 @@ # 2009-12-02, xt # version 0.1: initial -import weechat -w = weechat -import re -import htmllib from time import time as now from fnmatch import fnmatch -from urllib import quote +from html import unescape +from urllib.error import URLError +from urllib.parse import quote +from urllib.request import Request, urlopen + +import weechat +import re -SCRIPT_NAME = "announce_url_title" -SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "18" +SCRIPT_NAME = "announce_url_title" +SCRIPT_AUTHOR = "xt " +SCRIPT_VERSION = "19" SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Announce URL titles to channel or locally" +SCRIPT_DESC = "Announce URL titles to channel or locally" settings = { - "buffers" : 'freenode.#testing,', # comma separated list of buffers - "buffers_notice" : 'freenode.#testing,', # comma separated list of buffers - 'ignore_buffers' : 'grep,', # comma separated list of buffers to be ignored by this module - 'title_max_length': '80', - 'url_ignore' : '', # comma separated list of strings in url to ignore - 'reannounce_wait': '5', # 5 minutes delay - 'prefix': '', - 'suffix': '', - 'announce_public': 'off', # print it or msg the buffer - 'global': 'off', # whether to enable for all buffers - 'user_agent': 'WeeChat/%(version)s (http://www.weechat.org)', # user-agent format string - 'global_prefix':'url', # Prefix for when not public announcement + # comma separated list of buffers + "buffers": "libera.#testing,", + # comma separated list of buffers + "buffers_notice": "libera.#testing,", + # comma separated list of buffers to be ignored by this module + "ignore_buffers": "grep,", + "title_max_length": "80", + # comma separated list of strings in url to ignore + "url_ignore": "", + # 5 minutes delay + "reannounce_wait": "5", + "prefix": "", + "suffix": "", + # print it or msg the buffer + "announce_public": "off", + # whether to enable for all buffers + "global": "off", + # user-agent format string + "user_agent": "WeeChat/%(version)s (https://weechat.org)", + # Prefix for when not public announcement + "global_prefix": "url", } -octet = r'(?:2(?:[0-4]\d|5[0-5])|1\d\d|\d{1,2})' -ipAddr = r'%s(?:\,.%s){3}' % (octet, octet) +octet = r"(?:2(?:[0-4]\d|5[0-5])|1\d\d|\d{1,2})" +ipAddr = r"%s(?:\,.%s){3}" % (octet, octet) # Base domain regex off RFC 1034 and 1738 -label = r'[0-9a-z][-0-9a-z]*[0-9a-z]?' -domain = r'%s(?:\.%s)*\.[a-z][-0-9a-z]*[a-z]?' % (label, label) -urlRe = re.compile(r'(\w+://(?:%s|%s)(?::\d+)?(?:/[^\])>\s]*)?)' % (domain, ipAddr), re.I) +label = r"[0-9a-z][-0-9a-z]*[0-9a-z]?" +domain = r"%s(?:\.%s)*\.[a-z][-0-9a-z]*[a-z]?" % (label, label) +urlRe = re.compile( + r"(\w+://(?:%s|%s)(?::\d+)?(?:/[^\])>\s]*)?)" % (domain, ipAddr), re.I +) -buffer_name = '' +buffer_name = "" urls = {} -script_nick = 'url' -def say(s, buffer=''): - """normal msg""" - weechat.prnt(buffer, '%s\t%s' %(script_nick, s)) - -def unescape(s): - """Unescape HTML entities""" - p = htmllib.HTMLParser(None) - p.save_bgn() - p.feed(s) - return p.save_end() - -def url_print_cb(data, buffer, time, tags, displayed, highlight, prefix, message): +script_nick = "url" + + +def say(s, buffer=""): + """Display message.""" + weechat.prnt(buffer, "%s\t%s" % (script_nick, s)) + + +def url_print_cb( + data, buffer, time, tags, displayed, highlight, prefix, message +): global buffer_name, urls, ignore_buffers # Do not trigger on filtered lines and notices - if not int(displayed) or prefix == '--': - return w.WEECHAT_RC_OK + if not int(displayed) or prefix == "--": + return weechat.WEECHAT_RC_OK - msg_buffer_name = w.buffer_get_string(buffer, "name") - # Skip ignored buffers + msg_buffer_name = weechat.buffer_get_string(buffer, "name") - #if msg_buffer_name in w.config_get_plugin('ignore_buffers').split(','): - # return w.WEECHAT_RC_OK + # Skip ignored buffers if msg_buffer_name in ignore_buffers: - return w.WEECHAT_RC_OK + return weechat.WEECHAT_RC_OK found = False - notice = False - if w.config_get_plugin('global') == 'on': + if weechat.config_get_plugin("global") == "on": found = True buffer_name = msg_buffer_name else: - for active_buffer in w.config_get_plugin('buffers').split(','): + buffers = weechat.config_get_plugin("buffers").split(",") + for active_buffer in buffers: if active_buffer.lower() == msg_buffer_name.lower(): found = True buffer_name = msg_buffer_name break - for active_buffer in w.config_get_plugin('buffers_notice').split(','): + buffers_notice = weechat.config_get_plugin("buffers_notice").split(",") + for active_buffer in buffers_notice: if active_buffer.lower() == msg_buffer_name.lower(): found = True buffer_name = msg_buffer_name break if not found: - return w.WEECHAT_RC_OK + return weechat.WEECHAT_RC_OK - ignorelist = w.config_get_plugin('url_ignore').split(',') + ignorelist = weechat.config_get_plugin("url_ignore").split(",") for url in urlRe.findall(message): - url = quote(url, "%/:=&?~#+!$,;@()*[]") # Escape URL + url_esc = quote(url, "%/:=&?~#+!$,;@()*[]") # Escape URL ignore = False for ignore_part in ignorelist: if ignore_part.strip(): - if ignore_part in url: + if ignore_part in url_esc: ignore = True - w.prnt('', '%s: Found %s in URL: %s, ignoring.' %(SCRIPT_NAME, ignore_part, url)) + weechat.prnt( + "", + "%s: Found %s in URL: %s, ignoring." + % (SCRIPT_NAME, ignore_part, url_esc), + ) break if ignore: continue - if url in urls: + if url_esc in urls: continue else: - urls[url] = {} - url_process_launcher() + urls[url_esc] = {} - return w.WEECHAT_RC_OK - -def url_process_launcher(): - ''' Iterate found urls, fetch title if hasn't been launched ''' - global urls + url_process_launcher() - user_agent = w.config_get_plugin('user_agent') % {'version': w.info_get("version", "")} - for url, url_d in urls.items(): - if not url_d: # empty dict means not launched - url_d['launched'] = now() + return weechat.WEECHAT_RC_OK - # Read 8192 - python2_bin = w.info_get("python2_bin", "") or "python" - cmd = python2_bin + " -c \"import urllib2; opener = urllib2.build_opener();" - cmd += "opener.addheaders = [('User-agent','%s')];" % user_agent - cmd += "print opener.open('%s').read(8192)\"" % url - url_d['stdout'] = '' - url_d['url_hook_process'] = w.hook_process(cmd, 30 * 1000, "url_process_cb", "") +def url_read(url): + """Read URL.""" + user_agent = weechat.config_get_plugin("user_agent") % { + "version": weechat.info_get("version", "") + } + req = Request( + url, + headers={ + "User-agent": user_agent, + }, + ) + try: + head = urlopen(req).read(8192).decode("utf-8", errors="ignore") + except URLError: + return "" + match = re.search("(?i)(.*?)", head) + return unescape(match.group(1)) if match else "" - return w.WEECHAT_RC_OK def url_process_cb(data, command, rc, stdout, stderr): - """ Callback parsing html for title """ + """Process callback.""" global buffer_name, urls - url = command.split("'")[-2] - if stdout != "": - urls[url]['stdout'] += stdout - if int(rc) >= 0: - - head = re.sub("[\r\n\t ]"," ", urls[url]['stdout']) - title = re.search('(?i)\(.*?)\', head) - if title: - title = unescape(title.group(1)) - - max_len = int(w.config_get_plugin('title_max_length')) - if len(title) > max_len: - title = "%s [...]" % title[0:max_len] - - splits = buffer_name.split('.') #FIXME bad code - server = splits[0] - buffer = '.'.join(splits[1:]) - output = w.config_get_plugin('prefix') + title + w.config_get_plugin('suffix') - announce_public = w.config_get_plugin('announce_public') - if announce_public == 'on': - found = False - for active_buffer in w.config_get_plugin('buffers').split(','): - if active_buffer.lower() == buffer_name.lower(): - w.command('', '/msg -server %s %s %s' %(server, buffer, output)) - found = True - for active_buffer in w.config_get_plugin('buffers_notice').split(','): - if active_buffer.lower() == buffer_name.lower(): - w.command('', '/notice -server %s %s %s' %(server, buffer, output)) - found = True - if found == False: - say(output,w.buffer_search('', buffer_name)) - else: - say(output,w.buffer_search('', buffer_name)) - urls[url]['stdout'] = '' - - return w.WEECHAT_RC_OK + title = stdout + max_len = int(weechat.config_get_plugin("title_max_length")) + if len(title) > max_len: + title = "%s [...]" % title[0:max_len] + + splits = buffer_name.split(".") # FIXME bad code + server = splits[0] + buffer = ".".join(splits[1:]) + output = ( + weechat.config_get_plugin("prefix") + + title + + weechat.config_get_plugin("suffix") + ) + announce_public = weechat.config_get_plugin("announce_public") + if announce_public == "on": + found = False + buffers = weechat.config_get_plugin("buffers").split(",") + for active_buffer in buffers: + if active_buffer.lower() == buffer_name.lower(): + weechat.command( + "", + "/msg -server %s %s %s" % (server, buffer, output), + ) + found = True + buffers_notice = weechat.config_get_plugin("buffers_notice").split(",") + for active_buffer in buffers_notice: + if active_buffer.lower() == buffer_name.lower(): + weechat.command( + "", + "/notice -server %s %s %s" % (server, buffer, output), + ) + found = True + if not found: + say(output, weechat.buffer_search("", buffer_name)) + else: + say(output, weechat.buffer_search("", buffer_name)) + + return weechat.WEECHAT_RC_OK + + +def url_process_launcher(): + """Iterate found urls, fetch title if hasn't been launched.""" + global urls + + for url, url_d in urls.items(): + if not url_d: # empty dict means not launched + url_d["launched"] = now() + url_d["url_hook_process"] = weechat.hook_process( + "func:url_read", + 30 * 1000, + "url_process_cb", + url, + ) + + return weechat.WEECHAT_RC_OK + def purge_cb(*args): - ''' Purge the url list on configured intervals ''' + """Purge the url list on configured intervals.""" global urls t_now = now() - for url, url_d in urls.items(): - if (t_now - url_d['launched']) > \ - int(w.config_get_plugin('reannounce_wait'))*60: - del urls[url] + reannounce_wait = int(weechat.config_get_plugin("reannounce_wait")) * 60 + for url in list(urls): + if t_now - urls[url]["launched"] > reannounce_wait: + del urls[url] + + return weechat.WEECHAT_RC_OK - return w.WEECHAT_RC_OK class Ignores(object): def __init__(self, ignore_type): @@ -274,10 +316,10 @@ def __init__(self, ignore_type): def _get_ignores(self): assert self.ignore_type is not None - ignores = weechat.config_get_plugin(self.ignore_type).split(',') - ignores = [ s.lower() for s in ignores if s ] - self.ignores = [ s for s in ignores if s[0] != '!' ] - self.exceptions = [ s[1:] for s in ignores if s[0] == '!' ] + ignores = weechat.config_get_plugin(self.ignore_type).split(",") + ignores = [s.lower() for s in ignores if s] + self.ignores = [s for s in ignores if s[0] != "!"] + self.exceptions = [s[1:] for s in ignores if s[0] == "!"] def __contains__(self, s): s = s.lower() @@ -289,36 +331,51 @@ def __contains__(self, s): return True return False + def ignore_update(*args): ignore_buffers._get_ignores() - return w.WEECHAT_RC_OK + return weechat.WEECHAT_RC_OK if __name__ == "__main__": - if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, "", ""): + if weechat.register( + SCRIPT_NAME, + SCRIPT_AUTHOR, + SCRIPT_VERSION, + SCRIPT_LICENSE, + SCRIPT_DESC, + "", + "", + ): # Set default settings - for option, default_value in settings.iteritems(): - if not w.config_is_set_plugin(option): - w.config_set_plugin(option, default_value) - ignore_buffers = Ignores('ignore_buffers') - - w.hook_print("", "", "://", 1, "url_print_cb", "") - w.hook_timer(\ - int(w.config_get_plugin('reannounce_wait')) * 1000 * 60, + for option, default_value in settings.items(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, default_value) + ignore_buffers = Ignores("ignore_buffers") + + weechat.hook_print("", "", "://", 1, "url_print_cb", "") + weechat.hook_timer( + int(weechat.config_get_plugin("reannounce_wait")) * 1000 * 60, 0, 0, "purge_cb", - '') - weechat.hook_config('plugins.var.python.%s.ignore_buffers' %SCRIPT_NAME, 'ignore_update', '') - color_chat_delimiters = weechat.color('chat_delimiters') - color_chat_nick = weechat.color('chat_nick') - color_reset = weechat.color('reset') - color_chat_buffer = weechat.color('chat_buffer') + "", + ) + weechat.hook_config( + "plugins.var.python.%s.ignore_buffers" % SCRIPT_NAME, + "ignore_update", + "", + ) + color_chat_delimiters = weechat.color("chat_delimiters") + color_chat_nick = weechat.color("chat_nick") + color_reset = weechat.color("reset") + color_chat_buffer = weechat.color("chat_buffer") # pretty printing - script_nick = '%s[%s%s%s]%s' %(color_chat_delimiters, - color_chat_nick, - w.config_get_plugin('global_prefix'), - color_chat_delimiters, - color_reset) + script_nick = "%s[%s%s%s]%s" % ( + color_chat_delimiters, + color_chat_nick, + weechat.config_get_plugin("global_prefix"), + color_chat_delimiters, + color_reset, + ) diff --git a/python/anotify.py b/python/anotify.py index c83f62b8..88e020e5 100644 --- a/python/anotify.py +++ b/python/anotify.py @@ -28,7 +28,7 @@ SCRIPT_NAME = 'anotify' SCRIPT_AUTHOR = 'magnific0' -SCRIPT_VERSION = '1.0.1' +SCRIPT_VERSION = '1.0.2' SCRIPT_LICENSE = 'MIT' SCRIPT_DESC = 'Sends libnotify notifications upon events.' @@ -66,7 +66,7 @@ import re import os import weechat - import pynotify + import notify2 IMPORT_OK = True except ImportError as error: IMPORT_OK = False @@ -176,7 +176,7 @@ def notify_highlighted_message(prefix, message): 'Highlight', 'Highlighted Message', "{0}: {1}".format(prefix, message), - priority=pynotify.URGENCY_CRITICAL) + priority=notify2.URGENCY_CRITICAL) def notify_public_message_or_action(prefix, message, highlighted): @@ -231,7 +231,7 @@ def notify_public_action_message(prefix, message, highlighted): 'Action', 'Public Action Message', '{0}: {1}'.format(prefix, message), - priority=pynotify.URGENCY_NORMAL) + priority=notify2.URGENCY_NORMAL) def notify_private_action_message(prefix, message, highlighted): @@ -243,7 +243,7 @@ def notify_private_action_message(prefix, message, highlighted): 'Action', 'Private Action Message', '{0}: {1}'.format(prefix, message), - priority=pynotify.URGENCY_NORMAL) + priority=notify2.URGENCY_NORMAL) def notify_notice_message(prefix, message, highlighted): @@ -403,7 +403,7 @@ def cb_process_message( return weechat.WEECHAT_RC_OK -def a_notify(notification, title, description, priority=pynotify.URGENCY_LOW): +def a_notify(notification, title, description, priority=notify2.URGENCY_LOW): '''Returns whether notifications should be sticky.''' is_away = STATE['is_away'] icon = STATE['icon'] @@ -413,8 +413,8 @@ def a_notify(notification, title, description, priority=pynotify.URGENCY_LOW): if weechat.config_get_plugin('sticky_away') == 'on' and is_away: time_out = 0 try: - pynotify.init("wee-notifier") - wn = pynotify.Notification(title, description, icon) + notify2.init("wee-notifier") + wn = notify2.Notification(title, description, icon) wn.set_urgency(priority) wn.set_timeout(time_out) wn.show() diff --git a/python/anti_password.py b/python/anti_password.py new file mode 100644 index 00000000..49279b0d --- /dev/null +++ b/python/anti_password.py @@ -0,0 +1,231 @@ +# +# Copyright (C) 2021 Sébastien Helleu +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# Prevent a password from being accidentally sent to a buffer +# (requires WeeChat ≥ 0.4.0 and WeeChat ≥ 3.1 to check secured data). +# +# History: +# +# 2021-03-13, Sébastien Helleu : +# version 1.2.1: simplify regex condition +# 2021-03-12, Sébastien Helleu : +# version 1.2.0: add option "allowed_regex" +# 2021-02-26, Sébastien Helleu : +# version 1.1.0: add options "check_secured_data" and "max_rejects" +# 2021-02-24, Sébastien Helleu : +# version 1.0: first official version + +"""Anti password script.""" + +import re + +try: + import weechat + IMPORT_OK = True +except ImportError: + print('This script must be run under WeeChat.') + print('Get WeeChat now at: https://weechat.org/') + IMPORT_OK = False + +SCRIPT_NAME = 'anti_password' +SCRIPT_AUTHOR = 'Sébastien Helleu ' +SCRIPT_VERSION = '1.2.1' +SCRIPT_LICENSE = 'GPL3' +SCRIPT_DESC = 'Prevent a password from being accidentally sent to a buffer' + +# script options +ap_settings_default = { + 'allowed_regex': { + 'default': '^(http|https|ftp|file|irc)://', + 'help': ( + 'allowed regular expression (case is ignored); this is checked ' + 'first: if the input matched this regular expression, it is ' + 'considered safe and sent immediately to the buffer; ' + 'if empty, nothing specific is allowed' + ), + }, + 'password_condition': { + 'default': ( + '${words} == 1 && ${lower} >= 1 && ${upper} >= 1 ' + '&& ${digits} >= 1 && ${special} >= 1' + ), + 'help': ( + 'condition evaluated to check if the input string is a password; ' + 'allowed variables: ' + '${words} = number of words, ' + '${lower} = number of lower case letters, ' + '${upper} = number of upper case letters, ' + '${digits} = number of digits, ' + '${special} = number of other chars (not letter/digits/spaces), ' + ), + }, + 'check_secured_data': { + 'default': 'equal', + 'help': ( + 'consider that all secured data values are passwords and can not ' + 'be sent to buffers, possible values: ' + 'off = do not check secured data at all, ' + 'equal = reject input if stripped input is equal to a secured ' + 'data value, ' + 'include = reject input if a secured data value is part of input' + ), + }, + 'max_rejects': { + 'default': '3', + 'help': ( + 'max number of rejects for a given input text; if you press Enter ' + 'more than N times with exactly the same input, the text is ' + 'finally sent to the buffer; ' + 'if set to 0, the input is never sent to the buffer when it is ' + 'considered harmful (be careful, according to the other settings, ' + 'this can completely block the input)' + ), + }, +} +ap_settings = {} +ap_reject = { + 'input': '', + 'count': 0, +} + + +def ap_config_cb(data, option, value): + """Called when a script option is changed.""" + pos = option.rfind('.') + if pos > 0: + name = option[pos+1:] + if name in ap_settings: + ap_settings[name] = value + return weechat.WEECHAT_RC_OK + + +def ap_input_is_secured_data(input_text): + """Check if input_text is any value of a secured data.""" + check = ap_settings['check_secured_data'] + if check == 'off': + return False + sec_data = weechat.info_get_hashtable('secured_data', {}) or {} + if check == 'include': + for value in sec_data.values(): + if value and value in input_text: + return True + if check == 'equal': + return input_text.strip() in sec_data.values() + return False + + +def ap_input_matches_condition(input_text): + """Check if input_text matches the password condition.""" + # count chars in the input text + words = len(list(filter(None, re.split(r'\s+', input_text)))) + lower = sum(1 for c in input_text if c.islower()) + upper = sum(1 for c in input_text if c.isupper()) + digits = sum(1 for c in input_text if c.isdigit()) + special = sum(1 for c in input_text if not (c.isalnum() or c.isspace())) + + # evaluate password condition + extra_vars = { + 'words': str(words), + 'lower': str(lower), + 'upper': str(upper), + 'digits': str(digits), + 'special': str(special), + } + ret = weechat.string_eval_expression( + ap_settings['password_condition'], + {}, + extra_vars, + {'type': 'condition'}, + ) + return ret == '1' + + +def ap_input_is_password(input_text): + """Check if input_text looks like a password.""" + return (ap_input_is_secured_data(input_text) + or ap_input_matches_condition(input_text)) + + +def ap_input_return_cb(data, buf, command): + """Callback called when Return key is pressed in a buffer.""" + try: + max_rejects = int(ap_settings['max_rejects']) + except ValueError: + max_rejects = 3 + + input_text = weechat.buffer_get_string(buf, 'input') + + # check if input is a command + if not weechat.string_input_for_buffer(input_text): + # commands are ignored + ap_reject['input'] = '' + ap_reject['count'] = 0 + return weechat.WEECHAT_RC_OK + + # check if input matches the allowed regex + regex = ap_settings['allowed_regex'] + if regex and re.search(regex, input_text, re.IGNORECASE): + # allowed regex + ap_reject['input'] = '' + ap_reject['count'] = 0 + return weechat.WEECHAT_RC_OK + + if ap_input_is_password(input_text): + if ap_reject['input'] == input_text: + if ap_reject['count'] >= max_rejects > 0: + # it looks like a password but send anyway after N rejects + ap_reject['input'] = '' + ap_reject['count'] = 0 + return weechat.WEECHAT_RC_OK + ap_reject['count'] += 1 + else: + ap_reject['input'] = input_text + ap_reject['count'] = 1 + # password detected, do NOT send it to the buffer! + return weechat.WEECHAT_RC_OK_EAT + + # not a password + ap_reject['input'] = '' + ap_reject['count'] = 0 + return weechat.WEECHAT_RC_OK + + +def main(): + """Main function.""" + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, + SCRIPT_LICENSE, SCRIPT_DESC, '', ''): + # set default settings + for name, option in ap_settings_default.items(): + if weechat.config_is_set_plugin(name): + ap_settings[name] = weechat.config_get_plugin(name) + else: + weechat.config_set_plugin(name, option['default']) + ap_settings[name] = option['default'] + weechat.config_set_desc_plugin( + name, + '%s (default: "%s")' % (option['help'], option['default'])) + + # detect config changes + weechat.hook_config('plugins.var.python.%s.*' % SCRIPT_NAME, + 'ap_config_cb', '') + + # hook Return key + weechat.hook_command_run('/input return', 'ap_input_return_cb', '') + + +if __name__ == '__main__' and IMPORT_OK: + main() diff --git a/python/apply_corrections.py b/python/apply_corrections.py index 2fd8b5c6..078fc1bb 100644 --- a/python/apply_corrections.py +++ b/python/apply_corrections.py @@ -58,9 +58,14 @@ # History: # +# 2018-06-21, Chris Johnson : +# version 1.3: support python3 # 2014-05-10, Sébastien Helleu # version 1.2: change hook_print callback argument type of # displayed/highlight (WeeChat >= 1.0) +# 2013-06-24, Chris Johnson : +# version 1.2: declare SCRIPT_ variables before imports since one was being +# used there # 2012-10-09, Chris Johnson : # version 1.1: change some more variable names for clarity/consistency # 2012-10-08, Chris Johnson : @@ -91,6 +96,13 @@ # 2012-08-30, Chris Johnson : # version 0.1: initial release +SCRIPT_NAME = 'apply_corrections' +SCRIPT_AUTHOR = 'Chris Johnson ' +SCRIPT_VERSION = '1.3' +SCRIPT_LICENSE = 'GPL3' +SCRIPT_DESC = "When a correction (ex: s/typo/replacement) is sent, print the "\ + "user's previous message(s) with the corrected text instead." + import_ok = True try: @@ -106,16 +118,9 @@ from operator import itemgetter from collections import defaultdict except ImportError as message: - print('Missing package(s) for %s: %s' % (SCRIPT_NAME, message)) + print('Missing package(s) for {}: {}'.format(SCRIPT_NAME, message)) import_ok = False -SCRIPT_NAME = 'apply_corrections' -SCRIPT_AUTHOR = 'Chris Johnson ' -SCRIPT_VERSION = '1.2' -SCRIPT_LICENSE = 'GPL3' -SCRIPT_DESC = "When a correction (ex: s/typo/replacement) is sent, print the "\ - "user's previous message(s) with the corrected text instead." - # Default settings for the plugin. settings = {'check_every': '5', 'data_timeout': '60', @@ -158,7 +163,7 @@ def get_corrected_messages(nick, log, correction): original = message.get('message', '') if original: try: - match = re.match(re.compile('.*%s.*' % pattern), original) + match = re.match(re.compile('.*{}.*'.format(pattern)), original) except: match = original.find(pattern) != -1 finally: @@ -257,7 +262,7 @@ def handle_message_cb(data, buffer, date, tags, disp, hl, nick, message): valid_nick = r'([@~&!%+])?([-a-zA-Z0-9\[\]\\`_^\{|\}]+)' valid_correction = r's/[^/]*/[^/]*' correction_message_pattern = re.compile( - r'(%s:\s*)?(%s)(/)?$' % (valid_nick, valid_correction)) + r'({}:\s*)?({})(/)?$'.format(valid_nick, valid_correction)) match = re.match(correction_message_pattern, message) if match: @@ -269,8 +274,8 @@ def handle_message_cb(data, buffer, date, tags, disp, hl, nick, message): print_format = weechat.config_get_plugin('print_format') for cm in get_corrected_messages(nick, log, correction): corrected_msg = print_format - for k, v in cm.iteritems(): - corrected_msg = corrected_msg.replace('[%s]' % k, v) + for k, v in cm.items(): + corrected_msg = corrected_msg.replace('[{}]'.format(k), v) weechat.prnt_date_tags(buffer, 0, 'no_log', corrected_msg) else: # If it's not a correction, store the message in LASTWORDS. @@ -292,7 +297,7 @@ def load_config(data=None, option=None, value=None): # On initial load set any unset options to the defaults. if not option: - for option, default in settings.iteritems(): + for option, default in settings.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, default) @@ -344,7 +349,7 @@ def desc_options(): desc_options() # Register hook to run load_config when options are changed. - weechat.hook_config('plugins.var.python.%s.*' % SCRIPT_NAME, + weechat.hook_config('plugins.var.python.{}.*'.format(SCRIPT_NAME), 'load_config', '') # Register hook_print to process each new message as it comes in. diff --git a/python/arespond.py b/python/arespond.py index a6d60864..75463ccf 100644 --- a/python/arespond.py +++ b/python/arespond.py @@ -23,11 +23,16 @@ # # History: # -# - Sample History-Entry +# - 2022-01-25, Sébastien Helleu +# 0.1.2 - Fix mixed spaces and tabs for indentation +# - 2020-09-06, mumixam +# 0.1.1 - Added supported for python3 while keeping support for python2 +# - fixed issue with command not being executed in the right buffer +# - renamed option 'muted' to 'enabled' so its more intuitive SCR_NAME = "arespond" SCR_AUTHOR = "Stephan Huebner " -SCR_VERSION = "0.1.0" +SCR_VERSION = "0.1.2" SCR_LICENSE = "GPL3" SCR_DESC = "An autoresponder (sending a notice on other users' messages)" SCR_COMMAND = "arespond" @@ -37,7 +42,7 @@ try: import weechat as w except: - print "Script must be run under weechat. http://www.weechat.org" + print("Script must be run under weechat. http://www.weechat.org") import_ok = False import time @@ -46,7 +51,7 @@ "responderText" : "Hello. %s isn't available at the moment. This message " + "won't appear anymore for the next %d minutes.", "respondAfterMinutes" : "10", - "muted" : "off" + "enabled" : "off" } def errMsg(myMsg): @@ -57,7 +62,7 @@ def fn_privmsg(data, bufferp, tm, tags, display, is_hilight, prefix, msg): global settings servername = (w.buffer_get_string(bufferp, "name").split("."))[0] ownNick = w.info_get("irc_nick", servername) - if prefix != ownNick and settings["muted"] != "off": + if prefix != ownNick and settings["enabled"] != "off": # alert("messagetags: " + tags) if w.buffer_get_string(bufferp, "localvar_type") == "private": oldTime = w.buffer_get_string(bufferp, "localvar_btime") @@ -67,11 +72,11 @@ def fn_privmsg(data, bufferp, tm, tags, display, is_hilight, prefix, msg): nowTime = time.time() tdelta = int(nowTime)-int(oldTime) if int(settings["respondAfterMinutes"])*60 <= tdelta: - w.command("", "/notice " + prefix + " " + rmdTxt) + w.command(bufferp, "/notice " + prefix + " " + rmdTxt) w.buffer_set(bufferp, "localvar_set_btime", str(int(nowTime))) else: w.buffer_set(bufferp, "localvar_set_btime", str(int(time.time()))) - w.command("", "/notice " + prefix + " " + rmdTxt) + w.command(bufferp, "/notice " + prefix + " " + rmdTxt) return w.WEECHAT_RC_OK def fn_command(data, buffer, args): @@ -79,7 +84,7 @@ def fn_command(data, buffer, args): args = args.split() for listIndex in range(len(args)): if "on" in args[listIndex] or "off" in args[listIndex]: - w.config_set_plugin("muted", args[listIndex]) + w.config_set_plugin("enabled", args[listIndex]) else: try: myMinutes = float(args[listIndex]) @@ -90,8 +95,8 @@ def fn_command(data, buffer, args): return w.WEECHAT_RC_OK def alert(myString): - w.prnt("", myString) - return + w.prnt("", myString) + return def fn_configchange(data, option, value): global settings @@ -99,7 +104,7 @@ def fn_configchange(data, option, value): myOption = fields[-1] try: settings[myOption] = value - alert("Option {0} is now {1}".format(myOption, settings[myOption])) + alert("Option {0} is now {1}:".format(myOption, settings[myOption])) except KeyError: errMsg("There is no option named %s" %myOption) return w.WEECHAT_RC_OK @@ -116,10 +121,10 @@ def fn_configchange(data, option, value): w.hook_print("", "", "", 1, "fn_privmsg", "") # catch prvmsg w.hook_config("plugins.var.python." + SCR_NAME + ".*", "fn_configchange", "") # catch configchanges - w.hook_command(SCR_COMMAND, SCR_DESC, "[muted] [n] [text]", + w.hook_command(SCR_COMMAND, SCR_DESC, "[enabled] [n] [text]", """ Available options are: -- muted: can be "on" or "off" +- enabled: can be "on" or "off" (controls auto responds) - respondAfterMinutes: integer (in minutes), after which responderText is sent again - responderText: Text to be shown when necessary conditions are met. diff --git a/python/auto_away.py b/python/auto_away.py index 645412ff..c1e3e34b 100644 --- a/python/auto_away.py +++ b/python/auto_away.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# auto_away.py : A simple auto-away script for Weechat in Python +# auto_away.py : A simple auto-away script for Weechat in Python # Copyright (c) 2010 by Specimen # # Inspired in yaaa.pl by jnbek @@ -22,12 +22,12 @@ # # # This script requires WeeChat 0.3.0 or newer. -# +# # --------------------------------------------------------------------- # # Summary: # -# Sets status to away automatically after a given period of +# Sets status to away automatically after a given period of # inactivity. Returns from away when you start typing, but not when # you change buffer or scroll. This script should work with any # plugin and it's not IRC specific. @@ -47,15 +47,15 @@ # Configuration options via /set: # # 'idletime' -# description: Period in minutes (n) of keyboard inactivity until +# description: Period in minutes (n) of keyboard inactivity until # being marked as being away. # Setting idletime to "0", a negative number or a string -# such as "off" disables auto-away. All positive values +# such as "off" disables auto-away. All positive values # are treated as they integers. # command: /set plugins.var.python.auto_away.idletime n -# +# # 'message' -# description: Away message. The /away command requires this setting +# description: Away message. The /away command requires this setting # not to be empty. # command: /set plugins.var.python.auto_away.message "message" # @@ -74,7 +74,7 @@ # 2010-02-20 - 0.2.5 - Specimen: # Use hook_config to check idletime and # enable/disable hook_timer. -# Removed away_status. +# Removed away_status. # 2010-02-21 - 0.3 - Specimen: # Implemented /autoaway command. # 2010-02-22 - 0.3.3 - Specimen: @@ -87,19 +87,21 @@ # /autoaway without arguments outputs current # settings. # Code rewrite. +# 2018-10-02 - 0.4 - Pol Van Aubel : +# Make Python3 compatible. +from __future__ import print_function try: import weechat as w - -except Exception: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" +except: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org/") quit() # Script Properties SCRIPT_NAME = "auto_away" SCRIPT_AUTHOR = "Specimen" -SCRIPT_VERSION = "0.3.3" +SCRIPT_VERSION = "0.4" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Simple auto-away script in Python" @@ -130,20 +132,20 @@ def idle_chk(data, remaining_calls): w.unhook(timer_hook) if not w.config_get_plugin('message'): w.config_set_plugin('message', message) - w.command("", "/away -all %s" + w.command("", "/away -all %s" % w.config_get_plugin('message')) if int(version) < 0x00030200: ''' Workaround for /away -all bug in v. < 0.3.2 ''' servers = irc_servers() if servers: for server in servers: - w.command(server, "/away %s" + w.command(server, "/away %s" % w.config_get_plugin('message')) input_hook_function() return w.WEECHAT_RC_OK def irc_servers(): - ''' Disconnected IRC servers, workaround for /away -all bug + ''' Disconnected IRC servers, workaround for /away -all bug in v. < 0.3.2 ''' serverlist = w.infolist_get('irc_server','','') buffers = [] @@ -185,16 +187,16 @@ def autoaway_cmd(data, buffer, args): if value[2]: w.config_set_plugin('message', value[2]) if val_idletime() > 0: - w.prnt(w.current_buffer(), + w.prnt(w.current_buffer(), "%sauto-away%s settings:\n" " Time: %s%s%s minute(s)\n" " Message: %s%s\n" % (w.color("bold"), w.color("-bold"), - w.color("bold"), w.config_get_plugin('idletime'), - w.color("-bold"), w.color("bold"), + w.color("bold"), w.config_get_plugin('idletime'), + w.color("-bold"), w.color("bold"), w.config_get_plugin('message'))) else: - w.prnt(w.current_buffer(), + w.prnt(w.current_buffer(), "%sauto-away%s is disabled.\n" % (w.color("bold"), w.color("-bold"))) return w.WEECHAT_RC_OK @@ -213,16 +215,16 @@ def switch_chk(data, option, value): if __name__ == "__main__": if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - - if not w.config_get_plugin('idletime'): - w.config_set_plugin('idletime', idletime) - if not w.config_get_plugin('message'): + + if not w.config_get_plugin('idletime'): + w.config_set_plugin('idletime', idletime) + if not w.config_get_plugin('message'): w.config_set_plugin('message', message) - - w.hook_command("autoaway", + + w.hook_command("autoaway", "Set away status automatically after a period of " - "inactivity.", - "[time|off] [message]", + "inactivity.", + "[time|off] [message]", " time: minutes of inactivity to set away\n" " off: disable auto-away (0 also disables)\n" " message: away message (optional)\n" @@ -242,11 +244,11 @@ def switch_chk(data, option, value): "/autoaway off\n" "/autoaway 0\n" "Disables auto-away.\n", - "", + "", "autoaway_cmd", "") w.hook_config("plugins.var.python.auto_away.idletime", "switch_chk", "") - + version = w.info_get("version_number", "") or 0 timer_hook = None input_hook = None diff --git a/python/autoauth.py b/python/autoauth.py index b8f2e102..110a0c8d 100644 --- a/python/autoauth.py +++ b/python/autoauth.py @@ -11,6 +11,11 @@ # # ### changelog ### # +# * version 1.3 (Sébastien Helleu ) +# - make script compatible with WeeChat >= 3.4 +# (new parameters in function hdata_search) +# * version 1.2 (Sébastien Helleu ) +# - make script compatible with Python 3 # * version 1.1 (CrazyCat ) # - add a way to manage NickServ nick and host # * version 1.0 (Simmo Saan ) @@ -44,54 +49,72 @@ # # ============================================================================= +from __future__ import print_function -VERSION="1.1" -NAME="autoauth" -AUTHOR="Kolter" +VERSION = "1.3" +NAME = "autoauth" +AUTHOR = "Kolter" -DELIMITER="|@|" +DELIMITER = "|@|" import_ok = True try: import weechat -except: - print "Script must be run under weechat. http://www.weechat.org" +except ImportError: + print("Script must be run under weechat. https://weechat.org/") import_ok = False import re -weechat.register (NAME, AUTHOR, VERSION, "GPL2", "Auto authentification while changing nick", "", "") +weechat.register( + NAME, + AUTHOR, + VERSION, + "GPL2", + "Auto authentification while changing nick", + "", + "", +) weechat.hook_signal("*,irc_in2_notice", "auth_notice_check", "") weechat.hook_command( "autoauth", "Auto authentification while changing nick", - "{ add $nick $pass [$server=current] | del $nick [$server=current] | list | cmd [$command [$server=current]] | ns [$Nick[!username[@host]]] [$server=current] }", + "{ add $nick $pass [$server=current] | del $nick [$server=current] | list " + "| cmd [$command [$server=current]] " + "| ns [$Nick[!username[@host]]] [$server=current] }", " add : add authorization for $nick with password $pass for $server\n" " del : del authorization for $nick for $server\n" " list : list all authorization settings\n" - " cmd : command(s) (separated by '|') to run when identified for $server\n" + " cmd : command(s) (separated by '|') to run when identified for " + "$server\n" " %n will be replaced by current nick in each command\n" - " ns : set NickServ mask (or part of mask) for $server, the NickServ nick is mandatory", + " ns : set NickServ mask (or part of mask) for $server, the NickServ " + "nick is mandatory", "add|del|list|cmd %- %S %S", "auth_command", - "" - ) + "", +) + def auth_cmdlist(): - cmd = '' + cmd = "" cmds = weechat.config_get_plugin("commands") - if cmds == '': + if cmds == "": weechat.prnt("", "[%s] commands (empty)" % (NAME)) else: weechat.prnt("", "[%s] commands (list)" % (NAME)) for c in cmds.split("####"): - weechat.prnt("", " --> %s : '%s' " % (c.split(":::")[0], c.split(":::")[1])) + weechat.prnt( + "", + " --> %s : '%s' " % (c.split(":::")[0], c.split(":::")[1]), + ) + def auth_cmdget(server): - cmd = '' + cmd = "" cmds = weechat.config_get_plugin("commands") - if cmds != '': + if cmds != "": for c in cmds.split("####"): if c.find(":::") != -1: if c.split(":::")[0] == server: @@ -99,12 +122,13 @@ def auth_cmdget(server): break return cmd + def auth_cmdset(server, command): cmds = weechat.config_get_plugin("commands") found = False conf = [] - if cmds != '': + if cmds != "": for c in cmds.split("####"): if c.find(":::") != -1: if c.split(":::")[0] == server: @@ -116,14 +140,19 @@ def auth_cmdset(server, command): conf.append("%s:::%s" % (server, command)) weechat.config_set_plugin("commands", "####".join(conf)) - weechat.prnt("", "[%s] command '%s' successfully added for server %s" % (NAME, command, server)) + weechat.prnt( + "", + "[%s] command '%s' successfully added for server %s" + % (NAME, command, server), + ) + def auth_cmdunset(server): cmds = weechat.config_get_plugin("commands") found = False conf = [] - if cmds != '': + if cmds != "": for c in cmds.split("####"): if c.find(":::") != -1: if c.split(":::")[0] != server: @@ -131,21 +160,31 @@ def auth_cmdunset(server): else: found = True if found: - weechat.prnt("", "[%s] command for server '%s' successfully removed" % (NAME, server)) + weechat.prnt( + "", + "[%s] command for server '%s' successfully removed" + % (NAME, server), + ) weechat.config_set_plugin("commands", "####".join(conf)) + def auth_cmd(args, server): - if server == '': - if args == '': + if server == "": + if args == "": auth_cmdlist() else: - weechat.prnt("", "[%s] error while setting command, can't find a server" % (NAME)) + weechat.prnt( + "", + "[%s] error while setting command, can't find a server" + % (NAME), + ) else: - if args == '': + if args == "": auth_cmdunset(server) else: auth_cmdset(server, args) + def auth_list(): data = weechat.config_get_plugin("data") @@ -160,22 +199,35 @@ def auth_list(): (server, nick) = serv_nick.split(".") weechat.prnt("", " --> %s@%s " % (nick, server)) + def auth_notice_check(data, buffer, args): - server = buffer.split(',')[0] + server = buffer.split(",")[0] nickserv = auth_nsget(server) (nnick, nhost) = nickserv.split("!") - if args.startswith(":"+nickserv) and args.find("/msg NickServ IDENTIFY") != -1: - #args.find("If this is your nickname, type /msg NickServ") != -1 or args.find("This nickname is registered") != -1 : + if ( + args.startswith(":" + nickserv) + and args.find("/msg NickServ IDENTIFY") != -1 + ): + # args.find("If this is your nickname, type /msg NickServ") != -1 or args.find("This nickname is registered") != -1 : passwd = auth_get(weechat.info_get("irc_nick", server), server) if passwd != None: - weechat.command(server, "/quote -server %s %s identify %s" % (server, nnick, passwd)) + weechat.command( + server, + "/quote -server %s %s identify %s" % (server, nnick, passwd), + ) commands = auth_cmdget(server) - if commands != '': + if commands != "": for c in commands.split("|"): - weechat.command(server, c.strip().replace("%n", weechat.info_get('irc_nick', server))) + weechat.command( + server, + c.strip().replace( + "%n", weechat.info_get("irc_nick", server) + ), + ) return weechat.WEECHAT_RC_OK + def auth_del(the_nick, the_server): data = weechat.config_get_plugin("data") @@ -193,9 +245,18 @@ def auth_del(the_nick, the_server): if found: weechat.config_set_plugin("data", DELIMITER.join(conf)) - weechat.prnt("", "[%s] nick '%s@%s' successfully remove" % (NAME, the_nick, the_server)) + weechat.prnt( + "", + "[%s] nick '%s@%s' successfully remove" + % (NAME, the_nick, the_server), + ) else: - weechat.prnt("", "[%s] an error occured while removing nick '%s@%s' (not found)" % (NAME, the_nick, the_server)) + weechat.prnt( + "", + "[%s] an error occured while removing nick '%s@%s' (not found)" + % (NAME, the_nick, the_server), + ) + def auth_add(the_nick, the_passwd, the_server): data = weechat.config_get_plugin("data") @@ -216,7 +277,11 @@ def auth_add(the_nick, the_passwd, the_server): conf.append("%s.%s=%s" % (the_server, the_nick, the_passwd)) weechat.config_set_plugin("data", DELIMITER.join(conf)) - weechat.prnt("", "[%s] nick '%s@%s' successfully added" % (NAME, the_nick, the_server)) + weechat.prnt( + "", + "[%s] nick '%s@%s' successfully added" % (NAME, the_nick, the_server), + ) + def auth_get(the_nick, the_server): data = weechat.config_get_plugin("data") @@ -229,54 +294,79 @@ def auth_get(the_nick, the_server): return passwd return None + def get_channel_from_buffer_args(buffer, args): server_name = weechat.buffer_get_string(buffer, "localvar_server") channel_name = args if not channel_name: channel_name = weechat.buffer_get_string(buffer, "localvar_channel") - match_data = re.match('\A(irc.)?([^.]+)\.(#\S+)\Z', channel_name) + match_data = re.match("\A(irc.)?([^.]+)\.(#\S+)\Z", channel_name) if match_data: channel_name = match_data.group(3) server_name = match_data.group(2) return server_name, channel_name + +def search_server(hdata, servers, server_name): + """Search the IRC server using hdata_search function.""" + weechat_version = int(weechat.info_get("version_number", "") or 0) + if weechat_version >= 0x03040000: + return weechat.hdata_search( + hdata, + servers, + "${irc_server.name} == ${server_name}", + {}, + {"server_name": server_name}, + {}, + 1, + ) + return weechat.hdata_search( + hdata, servers, "${irc_server.name} == " + server_name, 1 + ) + + def auth_command(data, buffer, args): list_args = args.split(" ") server, channel = get_channel_from_buffer_args(buffer, args) - #strip spaces - while '' in list_args: - list_args.remove('') - while ' ' in list_args: - list_args.remove(' ') + # strip spaces + while "" in list_args: + list_args.remove("") + while " " in list_args: + list_args.remove(" ") h_servers = weechat.hdata_get("irc_server") l_servers = weechat.hdata_get_list(h_servers, "irc_servers") - if len(list_args) == 0: + if len(list_args) == 0: weechat.command(buffer, "/help autoauth") elif list_args[0] not in ["add", "del", "list", "cmd", "ns"]: - weechat.prnt(buffer, "[%s] bad option while using /autoauth command, try '/help autoauth' for more info" % (NAME)) + weechat.prnt( + buffer, + "[%s] bad option while using /autoauth command, " + "try '/help autoauth' for more info" + % (NAME), + ) elif list_args[0] == "cmd": - if len(list_args[1:]) == 1 and weechat.hdata_search(h_servers, l_servers, "${irc_server.name} == "+list_args[1], 1): + if len(list_args[1:]) == 1 and search_server(h_servers, l_servers, list_args[1]): auth_cmd("", list_args[1]) elif len(list_args[1:]) == 1: auth_cmd(list_args[1], server) elif len(list_args[1:]) >= 2: - if weechat.hdata_search(h_servers, l_servers, "${irc_server.name} == "+list_args[-1], 1): + if search_server(h_servers, l_servers, list_args[-1]): auth_cmd(" ".join(list_args[1:-1]), list_args[-1]) else: auth_cmd(" ".join(list_args[1:]), server) else: auth_cmd(" ".join(list_args[1:]), server) elif list_args[0] == "ns": - if len(list_args[1:]) == 1 and weechat.hdata_search(h_servers, l_servers, "${irc_server.name} == "+list_args[1], 1): + if len(list_args[1:]) == 1 and search_server(h_servers, l_servers, list_args[1]): auth_ns("", list_args[1]) elif len(list_args[1:]) == 1: auth_ns(list_args[1], server) elif len(list_args[1:]) == 2: - if weechat.hdata_search(h_servers, l_servers, "${irc_server.name} == "+list_args[-1], 1): + if search_server(h_servers, l_servers, list_args[-1]): auth_ns(" ".join(list_args[1:-1]), list_args[-1]) else: auth_ns(" ".join(list_args[1:]), server) @@ -285,17 +375,25 @@ def auth_command(data, buffer, args): elif list_args[0] == "list": auth_list() elif list_args[0] == "add": - if len(list_args) < 3 or (len(list_args) == 3 and server == ''): - weechat.prnt(buffer, "[%s] bad option while using /autoauth command, try '/help autoauth' for more info" % (NAME)) + if len(list_args) < 3 or (len(list_args) == 3 and server == ""): + weechat.prnt( + buffer, + "[%s] bad option while using /autoauth command, try '/help autoauth' for more info" + % (NAME), + ) else: if len(list_args) == 3: auth_add(list_args[1], list_args[2], server) else: auth_add(list_args[1], list_args[2], list_args[3]) elif list_args[0] == "del": - if len(list_args) < 2: - weechat.prnt(buffer, "[%s] bad option while using /autoauth command, try '/help autoauth' for more info" % (NAME)) - else: + if len(list_args) < 2: + weechat.prnt( + buffer, + "[%s] bad option while using /autoauth command, try '/help autoauth' for more info" + % (NAME), + ) + else: if len(list_args) == 2: auth_del(list_args[1], server) else: @@ -304,20 +402,27 @@ def auth_command(data, buffer, args): pass return weechat.WEECHAT_RC_OK + def auth_nslist(): - ns = 'NickServ!services@services' + ns = "NickServ!services@services" nss = weechat.config_get_plugin("nickservs") - if nss == '': - weechat.prnt("", "[%s] NickServ : NickServ!services@services" % (NAME)) + if nss == "": + weechat.prnt( + "", "[%s] NickServ : NickServ!services@services" % (NAME) + ) else: weechat.prnt("", "[%s] NickServ (list)" % (NAME)) for n in nss.split("####"): - weechat.prnt("", " --> %s : '%s' " % (n.split(":::")[0], n.split(":::")[1])) + weechat.prnt( + "", + " --> %s : '%s' " % (n.split(":::")[0], n.split(":::")[1]), + ) + def auth_nsget(server): - ns = 'NickServ!services@services' + ns = "NickServ!services@services" nss = weechat.config_get_plugin("nickservs") - if nss != '': + if nss != "": for n in nss.split("####"): if n.find(":::") != -1: if n.split(":::")[0] == server: @@ -325,12 +430,13 @@ def auth_nsget(server): break return ns + def auth_nsset(server, nickserv): nss = weechat.config_get_plugin("nickservs") found = False conf = [] - if nss != '': + if nss != "": for n in nss.split("####"): if n.find(":::") != -1: if n.split(":::")[0] == server: @@ -342,14 +448,19 @@ def auth_nsset(server, nickserv): conf.append("%s:::%s" % (server, nickserv)) weechat.config_set_plugin("nickservs", "####".join(conf)) - weechat.prnt("", "[%s] NickServ '%s' successfully added for server %s" % (NAME, nickserv, server)) + weechat.prnt( + "", + "[%s] NickServ '%s' successfully added for server %s" + % (NAME, nickserv, server), + ) + def auth_nsunset(server): nss = weechat.config_get_plugin("nickservs") found = False conf = [] - if nss != '': + if nss != "": for n in nss.split("####"): if n.find(":::") != -1: if n.split(":::")[0] != server: @@ -357,17 +468,26 @@ def auth_nsunset(server): else: found = True if found: - weechat.prnt("", "[%s] NickServ for server '%s' successfully removed" % (NAME, server)) + weechat.prnt( + "", + "[%s] NickServ for server '%s' successfully removed" + % (NAME, server), + ) weechat.config_set_plugin("nickservs", "####".join(conf)) + def auth_ns(args, server): - if server == '': - if args == '': + if server == "": + if args == "": auth_nslist() else: - weechat.prnt("", "[%s] error while setting NickServ, can't find a server" % (NAME)) + weechat.prnt( + "", + "[%s] error while setting NickServ, can't find a server" + % (NAME), + ) else: - if args == '': + if args == "": auth_nsunset(server) else: auth_nsset(server, args) diff --git a/python/autobump.py b/python/autobump.py new file mode 100644 index 00000000..0a9a7a33 --- /dev/null +++ b/python/autobump.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 Daniel Kessler +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# This script bumps buffers when there is activity on them, +# replicating the functionality of most mainstream chat programs. + +# Changelog: +# Ver: 0.1.0 improve activity detection: + +# TODO: combine priorities of merged buffers + +import weechat + +SCRIPT_NAME = 'autobump' +SCRIPT_AUTHOR = 'Daniel Kessler ' +SCRIPT_VERSION = '0.1.0' +SCRIPT_LICENSE = 'GPL3' +SCRIPT_DESC = 'Bump buffers upon activity.' + +DEFAULTS = { + 'lowprio_buffers': ('', 'List of buffers to be sorted with low priority'), + 'highprio_buffers': ('irc.server.*,core.weechat', + 'List of buffers to be sorted with high priority'), + 'tags': ('notify_message,notify_private,self_msg', + 'List of message tags that are considered activity') +} + +def on_autobump_command(data, buffer, args): + argv = args.split() + if len(argv) < 2 or argv[0] not in {'add', 'del'} or argv[1] not in {'high', 'low'}: + return weechat.WEECHAT_RC_ERROR + + bufname = weechat.buffer_get_string(buffer, 'full_name') + + key = argv[1] + 'prio_buffers' + buflist = weechat.config_get_plugin(key).split(',') + + if argv[0] == 'add': + if bufname not in buflist: + buflist.append(bufname) + elif bufname in buflist: + buflist.remove(bufname) + else: + return weechat.WEECHAT_RC_ERROR + + weechat.config_set_plugin(key, ','.join(buflist)) + + on_buffer_activity(buffer) + + return weechat.WEECHAT_RC_OK + +def get_buffers(): + '''Get a list of all the buffers in weechat.''' + hdata = weechat.hdata_get('buffer') + buffer = weechat.hdata_get_list(hdata, "gui_buffers"); + + result = [] + while buffer: + number = weechat.hdata_integer(hdata, buffer, 'number') + result.append((number, buffer)) + buffer = weechat.hdata_pointer(hdata, buffer, 'next_buffer') + return hdata, result + +def buffer_priority(buffer): + '''Get a buffer's priority. Higher number means higher priority.''' + lowprio_match = weechat.config_get_plugin('lowprio_buffers') + if weechat.buffer_match_list(buffer, lowprio_match): + return 0 + + highprio_match = weechat.config_get_plugin('highprio_buffers') + if weechat.buffer_match_list(buffer, highprio_match): + return 2 + + return 1 + +def on_buffer_activity(buffer): + prio = buffer_priority(buffer) + if prio == 2: + weechat.buffer_set(buffer, 'number', '1') + return + + hdata, buffers = get_buffers() + for num, buf in reversed(buffers): + if prio < buffer_priority(buf): + weechat.buffer_set(buffer, 'number', str(num + 1)) + return + + weechat.buffer_set(buffer, 'number', '1') + +def on_print(data, buffer, date, tags, displayed, highlight, prefix, message): + if int(displayed): + on_buffer_activity(buffer) + return weechat.WEECHAT_RC_OK + +def on_buffer_open(data, signal, signal_data): + on_buffer_activity(signal_data) + return weechat.WEECHAT_RC_OK + +the_print_hook = None +def update_hook(*args): + global the_print_hook + if the_print_hook: + weechat.unhook(the_print_hook) + value = weechat.config_get_plugin('tags') + the_print_hook = weechat.hook_print('', value, '', 0, 'on_print', '') + return weechat.WEECHAT_RC_OK + +command_description = r'''/autobump add high: Add the current buffer to the high priority list +/autobump add low: Add the current buffer to the low priority list +/autobump del high: Remove the current buffer from the high priority list +/autobump del low: Remove the current buffer from the low priority list + +You can manually modify the high/low priority lists (for instance, with custom patterns) with /set var.plugins.python.autobump.highprio_buffers and /set var.plugins.python.autobump.lowprio_buffers. + +See /help filter for documentation on writing buffer lists. +''' + +command_completion = 'add high || add low || del high || del low' + +if __name__ == '__main__': + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, + SCRIPT_LICENSE, SCRIPT_DESC, '', ''): + for option, value in DEFAULTS.items(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value[0]) + weechat.config_set_desc_plugin(option, value[1]) + + update_hook() + weechat.hook_config('plugins.var.python.'+SCRIPT_NAME+'.tags', + 'update_hook', + '') + weechat.hook_signal('buffer_opened', 'on_buffer_open', '') + + weechat.hook_command('autobump', + command_description, + '', + '', + command_completion, + 'on_autobump_command', + '') diff --git a/python/autoconf.py b/python/autoconf.py new file mode 100644 index 00000000..54c74b77 --- /dev/null +++ b/python/autoconf.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Manu Koell +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# 2017-11-03: fix script/issue #236 +# v0.2: add "%h" variable in option 'file' +# 2018-10-23: fix script/issue #297 +# v0.3: make script python 3 compatible +# 2021-05-02: +# v0.4: add compatibility with WeeChat >= 3.2 (XDG directories) + +from __future__ import print_function + +import os +import re + +from fnmatch import fnmatch + +try: + import weechat as w + +except Exception: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") + quit() + +NAME = "autoconf" +AUTHOR = "Manu Koell " +VERSION = "0.4" +LICENSE = "GPL3" +DESCRIPTION = "auto save/load changed options in a ~/.weerc file, useful to share dotfiles with" + +EXCLUDES = [ + '*.nicks', + '*.username', '*.sasl_username', + '*.password', '*.sasl_password', + 'irc.server.*.autoconnect', + 'irc.server.*.autojoin' +] + +SETTINGS = { + 'autosave': ('on', 'auto save config on quit'), + 'autoload': ('on', 'auto load config on start'), + 'ignore': ( + ','.join(EXCLUDES), + 'comma separated list of patterns to exclude'), + 'file': ('%h/.weerc', 'config file location ("%h" will be replaced by WeeChat config home)') +} + +def cstrip(text): + """strip color codes""" + + return w.string_remove_color(text, '') + +def get_config(args): + """get path to config file""" + + try: + conf = args[1] + except Exception: + conf = w.config_get_plugin('file') + options = { + 'directory': 'config', + } + return w.string_eval_path_home(conf, {}, {}, options) + +def load_conf(args): + """send config to fifo pipe""" + + fifo = w.info_get('fifo_filename', '') + conf = get_config(args) + + if os.path.isfile(conf): + w.command('', '/exec -sh -norc cat | grep */set %s > %s' % (conf, fifo)) + +def save_conf(args): + """match options and save to config file""" + + try: + f = open(get_config(args), 'w+') + + except Exception as e: + w.prnt('', '%sError: %s' % (w.prefix('error'), e)) + + return w.WEECHAT_RC_ERROR + + header = [ + '#', + '# WeeChat %s (compiled on %s)' % (w.info_get('version', ''), w.info_get('date', '')), + '#', + '# Use /autoconf load or cat this file to the FIFO pipe.', + '#', + '# For more info, see https://weechat.org/scripts/source/autoconf.py.html', + '#', + '' + ] + + for ln in header: + f.write('%s\n' % ln) + + w.command('', '/buffer clear') + w.command('', '/set diff') + + infolist = w.infolist_get('buffer_lines', '', '') + + while w.infolist_next(infolist): + message = cstrip(w.infolist_string(infolist, 'message')) + ignore = w.config_get_plugin('ignore').split(',') + option = re.match(RE['option'], message) + + if option: + if not any(fnmatch(option.group(1), p.strip()) for p in ignore): + f.write('*/set %s %s\n' % (option.group(1), option.group(2))) + + f.close() + + w.infolist_free(infolist) + +def autoconf_cb(data, buffer, args): + """the /autoconf command""" + + args = args.split() + + if 'save' in args: + save_conf(args) + + elif 'load' in args: + load_conf(args) + + else: + # show help message + w.command('', '/help ' + NAME) + + return w.WEECHAT_RC_OK + +def quit_cb(data, signal, signal_data): + """save config on quit""" + + save_conf(None) + + return w.WEECHAT_RC_OK + +if __name__ == '__main__': + if w.register(NAME, AUTHOR, VERSION, LICENSE, DESCRIPTION, "", ""): + w.hook_command(NAME, DESCRIPTION, 'save [path] || load [path]', '', 'save || load', 'autoconf_cb', '') + default_txt = w.gettext("default: ") # check if string is translated + RE = { + 'option': re.compile('\s*(.*) = (.*) \(%s' % default_txt) + } + + # set default config + for option, value in SETTINGS.items(): + if not w.config_is_set_plugin(option): + w.config_set_plugin(option, value[0]) + w.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) + + if 'on' in w.config_get_plugin('autoload'): + load_conf(None) + + if 'on' in w.config_get_plugin('autosave'): + w.hook_signal('quit', 'quit_cb', '') diff --git a/python/autoconnect.py b/python/autoconnect.py index 2bd179a5..1d3b27ec 100644 --- a/python/autoconnect.py +++ b/python/autoconnect.py @@ -17,9 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import print_function + SCRIPT_NAME = "autoconnect" SCRIPT_AUTHOR = "arno " -SCRIPT_VERSION = "0.3.1" +SCRIPT_VERSION = "0.3.3" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "reopens servers and channels opened last time weechat closed" SCRIPT_COMMAND = "autoconnect" @@ -27,8 +29,8 @@ try: import weechat except: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org/") quit() @@ -59,7 +61,7 @@ def joinpart_cb(data, signal, signal_data): weechat.command("", "/mute /set irc.server.%s.autoconnect on" % (server,)) # get all channels joined (without passphrases) - chans = [j.split()[0].strip() for j in signal_data.split(None, 2)[2].split(',')] + chans = [j.split()[0].strip().lstrip(':') for j in signal_data.split(None, 2)[2].split(',')] autojoin_channels.add(','.join(chans)) elif signal.endswith("irc_in2_PART"): diff --git a/python/autojoin.py b/python/autojoin.py deleted file mode 100644 index 61449ffb..00000000 --- a/python/autojoin.py +++ /dev/null @@ -1,194 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2009 by xt -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -# (this script requires WeeChat 0.3.0 or newer) -# -# History: -# 2009-06-18, xt -# version 0.1: initial release -# -# 2009-10-18, LBo -# version 0.2: added autosaving of join channels -# /set plugins.var.python.autojoin.autosave 'on' -# -# 2009-10-19, LBo -# version 0.2.1: now only responds to part messages from self -# find_channels() only returns join'ed channels, not all the buffers -# updated description and docs -# -# 2009-10-20, LBo -# version 0.2.2: fixed quit callback -# removed the callbacks on part & join messages -# -# 2012-04-14, Filip H.F. "FiXato" Slagter -# version 0.2.3: fixed bug with buffers of which short names were changed. -# Now using 'name' from irc_channel infolist. -# version 0.2.4: Added support for key-protected channels -# -# 2014-05-22, Nathaniel Wesley Filardo -# version 0.2.5: Fix keyed channel support -# -# 2016-01-13, The fox in the shell -# version 0.2.6: Support keeping chan list as secured data -# -# @TODO: add options to ignore certain buffers -# @TODO: maybe add an option to enable autosaving on part/join messages - -import weechat as w -import re - -SCRIPT_NAME = "autojoin" -SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "0.2.6" -SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Configure autojoin for all servers according to currently joined channels" -SCRIPT_COMMAND = "autojoin" - -# script options -settings = { - "autosave": "off", -} - -if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - - w.hook_command(SCRIPT_COMMAND, - SCRIPT_DESC, - "[--run]", - " --run: actually run the commands instead of displaying\n", - "--run", - "autojoin_cb", - "") - - #w.hook_signal('*,irc_in2_join', 'autosave_channels_on_activity', '') - #w.hook_signal('*,irc_in2_part', 'autosave_channels_on_activity', '') - w.hook_signal('quit', 'autosave_channels_on_quit', '') - -# Init everything -for option, default_value in settings.items(): - if w.config_get_plugin(option) == "": - w.config_set_plugin(option, default_value) - -def autosave_channels_on_quit(signal, callback, callback_data): - ''' Autojoin current channels ''' - if w.config_get_plugin(option) != "on": - return w.WEECHAT_RC_OK - - items = find_channels() - - # print/execute commands - for server, channels in items.iteritems(): - process_server(server, channels) - - return w.WEECHAT_RC_OK - - -def autosave_channels_on_activity(signal, callback, callback_data): - ''' Autojoin current channels ''' - if w.config_get_plugin(option) != "on": - return w.WEECHAT_RC_OK - - items = find_channels() - - # print/execute commands - for server, channels in items.iteritems(): - nick = w.info_get('irc_nick', server) - - pattern = "^:%s!.*(JOIN|PART) :?(#[^ ]*)( :.*$)?" % nick - match = re.match(pattern, callback_data) - - if match: # check if nick is my nick. In that case: save - process_server(server, channels) - else: # someone else: ignore - continue - - return w.WEECHAT_RC_OK - -def autojoin_cb(data, buffer, args): - """Old behaviour: doesn't save empty channel list""" - """In fact should also save open buffers with a /part'ed channel""" - """But I can't believe somebody would want that behaviour""" - items = find_channels() - - if args == '--run': - run = True - else: - run = False - - # print/execute commands - for server, channels in items.iteritems(): - process_server(server, channels, run) - - return w.WEECHAT_RC_OK - -def process_server(server, channels, run=True): - option = "irc.server.%s.autojoin" % server - channels = channels.rstrip(',') - oldchans = w.config_string(w.config_get(option)) - - if not channels: # empty channel list - return - - # Note: re already caches the result of regexp compilation - sec = re.match('^\${sec\.data\.(.*)}$', oldchans) - if sec: - secvar = sec.group(1) - command = "/secure set %s %s" % (secvar, channels) - else: - command = "/set irc.server.%s.autojoin '%s'" % (server, channels) - - if run: - w.command('', command) - else: - w.prnt('', command) - -def find_channels(): - """Return list of servers and channels""" - #@TODO: make it return a dict with more options like "nicks_count etc." - items = {} - infolist = w.infolist_get('irc_server', '', '') - # populate servers - while w.infolist_next(infolist): - items[w.infolist_string(infolist, 'name')] = '' - - w.infolist_free(infolist) - - # populate channels per server - for server in items.keys(): - keys = [] - keyed_channels = [] - unkeyed_channels = [] - items[server] = '' #init if connected but no channels - infolist = w.infolist_get('irc_channel', '', server) - while w.infolist_next(infolist): - if w.infolist_integer(infolist, 'nicks_count') == 0: - #parted but still open in a buffer: bit hackish - continue - if w.infolist_integer(infolist, 'type') == 0: - key = w.infolist_string(infolist, "key") - if len(key) > 0: - keys.append(key) - keyed_channels.append(w.infolist_string(infolist, "name")) - else : - unkeyed_channels.append(w.infolist_string(infolist, "name")) - items[server] = ','.join(keyed_channels + unkeyed_channels) - if len(keys) > 0: - items[server] += ' %s' % ','.join(keys) - w.infolist_free(infolist) - - return items - diff --git a/python/autojoin_on_invite.py b/python/autojoin_on_invite.py index 45adab5d..fa85aa12 100644 --- a/python/autojoin_on_invite.py +++ b/python/autojoin_on_invite.py @@ -20,6 +20,17 @@ # (this script requires WeeChat 0.3.0 or newer) # # History: +# 2022-10-19, Guillermo Castro +# version 0.9: fix regex parsing of INVITE to allow non-ircv3 matching +# 2022-03-2, h-0-s-h +# version 0.8: fix reged parsing of INVITE message to account for ircv3/CAPS style/etc +# (@time=2022-03-02T19:00:30.041Z :XXXXX!~XXXXX@xxxxx INVITE h-0-s-h :#xxxxx) +# 2018-10-03, Pol Van Aubel +# version 0.7: Python3 compatibility. Considerations: +# - Settings during registration are iterated over only once, so +# code should be succinct rather than efficient on both Py2/3. +# - The autojoin_keys zip is turned into a dict, so wouldn't +# make sense to first turn into a list as futurize suggests. # 2015-10-11, Simmo Saan # version 0.6: allow joining channels with keys in autojoin # 2013-12-21, Sebastien Helleu @@ -38,7 +49,7 @@ SCRIPT_NAME = "autojoin_on_invite" SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "0.6" +SCRIPT_VERSION = "0.9" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Auto joins channels when invited" @@ -56,7 +67,7 @@ if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - for option, default_value in settings.iteritems(): + for option, default_value in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_value) @@ -80,7 +91,9 @@ def join(server, channel): def invite_cb(data, signal, signal_data): server = signal.split(',')[0] # EFNet,irc_in_INVITE channel = signal_data.split()[-1].lstrip(':') # :nick!ident@host.name INVITE yournick :#channel - from_nick = re.match(':(?P.+)!', signal_data).groups()[0] + from_nick = '' + SearchStr = '(?:\@.*)?:(?P.+)!' #@time=2022-03-02T19:00:30.041Z :XX-XXXX!~XX-XXXX@xx.xxxx INVITE yournick :#xxxx-xxx (works also when no message-tag is present) + from_nick = re.search(SearchStr, signal_data).groups()[0] if len(w.config_get_plugin('whitelist_nicks')) > 0 and len(w.config_get_plugin('whitelist_channels')) > 0: # if there's two whitelists, accept both if from_nick in w.config_get_plugin('whitelist_nicks').split(',') or channel in w.config_get_plugin('whitelist_channels').split(','): diff --git a/python/autojoinem.py b/python/autojoinem.py deleted file mode 100644 index 632537bc..00000000 --- a/python/autojoinem.py +++ /dev/null @@ -1,395 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2013-2017 by nils_2 -# -# add/del channel(s) to/from autojoin option -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# idea by azizLIGHTS -# -# 2017-01-06: nils_2, (freenode.#weechat) -# 0.6 : fix problem with non existing server (reported by Niols) -# 2016-12-19: nils_2, (freenode.#weechat) -# 0.5 : fix problem with empty autojoin (reported by Caelum) -# 2016-06-05: nils_2, (freenode.#weechat) -# 0.4 : make script python3 compatible -# 2015-11-14: nils_2, (freenode.#weechat) -# 0.3 : fix: problem with (undef) option -# 2014-01-19: nils_2, (freenode.#weechat) -# 0.2 : fix: adding keys to already existing keys failed -# 2013-12-22: nils_2, (freenode.#weechat) -# 0.1 : initial release -# -# requires: WeeChat version 0.3.x -# -# Development is currently hosted at -# https://github.com/weechatter/weechat-scripts - -try: - import weechat,re - -except Exception: - print("This script must be run under WeeChat.") - print("Get WeeChat now at: http://www.weechat.org/") - quit() - -SCRIPT_NAME = "autojoinem" -SCRIPT_AUTHOR = "nils_2 " -SCRIPT_VERSION = "0.6" -SCRIPT_LICENSE = "GPL" -SCRIPT_DESC = "add/del channel(s) to/from autojoin option" - -OPTIONS = { 'sorted' : ('off','channels will be sorted in autojoin-option. if autojoin-option contains channel-keys, this option will be ignored.'), - } - -def add_autojoin_cmd_cb(data, buffer, args): - if args == "": # no args given. quit - return weechat.WEECHAT_RC_OK - - argv = args.strip().split(' ') - -# if (len(argv) <= 1): -# weechat.prnt(buffer,"%s%s: too few arguments." % (weechat.prefix('error'),SCRIPT_NAME)) -# return weechat.WEECHAT_RC_OK - - server = weechat.buffer_get_string(buffer, 'localvar_server') # current server - channel = weechat.buffer_get_string(buffer, 'localvar_channel') # current channel - buf_type = weechat.buffer_get_string(buffer, 'localvar_type') - - # only "add " given by user - if (len(argv) == 2): - weechat.prnt(buffer,"%s%s: invalid number of arguments." % (weechat.prefix('error'),SCRIPT_NAME)) - return weechat.WEECHAT_RC_OK - - # '-key' keyword in command line? - if '-key' in argv: - found_key_word = argv.index('-key') - key_words = argv[int(found_key_word)+1:] - # don't use "-key" in argv - argv = argv[:int(found_key_word)] - - # ADD argument - if (argv[0].lower() == 'add'): - # add current channel to autojoin. Only option "add" was given.. - if (len(argv) == 1): - if server == "" or channel == "" or server == channel or buf_type == "" or buf_type != 'channel': - weechat.prnt(buffer,"%s%s: current buffer is not a channel buffer." % (weechat.prefix('error'),SCRIPT_NAME)) - return weechat.WEECHAT_RC_OK - list_of_channels, list_of_current_keys = get_autojoin_list(buffer,server) - # no channels in option! - if list_of_channels == 1 and list_of_current_keys == 1: - ptr_config_autojoin = weechat.config_get('irc.server.%s.autojoin' % server) - rc = weechat.config_option_set(ptr_config_autojoin,channel,1) - return weechat.WEECHAT_RC_OK - if channel in list_of_channels: - weechat.prnt(buffer,"%s%s: channel '%s' already in autojoin for server '%s'" % (weechat.prefix("error"),SCRIPT_NAME,channel,server)) - else: - # first char of channel '#' ? - if channel[0] == '#': - if '-key' in args and len(key_words) > 1: - weechat.prnt(buffer,"%s%s: too many key(s) for given channel(s) " % (weechat.prefix('error'),SCRIPT_NAME)) - return weechat.WEECHAT_RC_OK - elif '-key' in args and len(key_words) == 1: - list_of_channels.insert(0,channel) - list_of_current_keys = ','.join(key_words) - # strip leading ',' - if list_of_current_keys[0] == ',': - list_of_current_keys = list_of_current_keys.lstrip(',') - else: - list_of_channels.append(channel) - - if not set_autojoin_list(server,list_of_channels, list_of_current_keys): - weechat.prnt(buffer,"%s%s: set new value for option failed..." % (weechat.prefix('error'),SCRIPT_NAME)) - # server and channels given by user - elif (len(argv) >= 3): - server = argv[1] - list_of_channels = argv[2:] - if '-key' in args and len(list_of_channels) < len(key_words): - weechat.prnt(buffer,"%s%s: too many key(s) for given channel(s) " % (weechat.prefix('error'),SCRIPT_NAME)) - return weechat.WEECHAT_RC_OK - - list_of_current_channels,list_of_current_keys = get_autojoin_list(buffer,server) - # autojoin option is empty - if list_of_current_channels == 1: - # no channel -> no key! - list_of_current_keys = "" - if '-key' in args: - list_of_current_keys = ','.join(key_words) - # strip leading ',' - if list_of_current_keys[0] == ',': - list_of_current_keys = list_of_current_keys.lstrip(',') - if not set_autojoin_list(server,list_of_channels, list_of_current_keys): - weechat.prnt(buffer,"%s%s: set new value for option failed..." % (weechat.prefix('error'),SCRIPT_NAME)) - else: - if '-key' in args: - j = 0 - new_keys = [] - list_of_new_keys = [] - for i in list_of_channels: - if i not in list_of_current_channels and j <= len(key_words): -# weechat.prnt(buffer,"channel: %s, channel key is: '%s'" % (i,key_words[j])) - list_of_current_channels.insert(j,i) - new_keys.insert(j,key_words[j]) - j += 1 - missing_channels = list_of_current_channels - list_of_new_keys = ','.join(new_keys) - if list_of_current_keys: - list_of_current_keys = list_of_new_keys + ',' + list_of_current_keys - else: - list_of_current_keys = list_of_new_keys - # strip leading ',' - if list_of_current_keys[0] == ',': - list_of_current_keys = list_of_current_keys.lstrip(',') - else: - # check given channels with channels already set in option - missing_channels = get_difference(list_of_channels,list_of_current_channels) - missing_channels = list_of_current_channels + missing_channels - - if not set_autojoin_list(server,missing_channels, list_of_current_keys): - weechat.prnt(buffer,"%s%s: set new value for option failed..." % (weechat.prefix('error'),SCRIPT_NAME)) - return weechat.WEECHAT_RC_OK - - # DEL argument - if (argv[0].lower() == 'del'): - # del current channel from autojoin. Only option "del" was given.. - if (len(argv) == 1): - if server == "" or channel == "" or server == channel or buf_type == "" or buf_type != 'channel': - weechat.prnt(buffer,"%s%s: current buffer is not a channel buffer." % (weechat.prefix('error'),SCRIPT_NAME)) - return weechat.WEECHAT_RC_OK - list_of_channels, list_of_keys = get_autojoin_list(buffer,server) - # no channels in option, nothing to delete - if list_of_channels == 1 and list_of_current_keys == 1: - return weechat.WEECHAT_RC_OK - if channel not in list_of_channels: - weechat.prnt(buffer,"%s%s: channel '%s' not found in autojoin for server '%s'" % (weechat.prefix("error"),SCRIPT_NAME,channel,server)) - return weechat.WEECHAT_RC_OK - else: - # first char of channel '#' ? - if channel[0] == '#': - channel_key_index = list_of_channels.index(channel) - if not list_of_keys: - list_of_channels.remove(list_of_channels[channel_key_index]) - list_of_current_keys = '' - else: - list_of_keys_tup = list_of_keys.split(",") - list_of_current_keys = list_of_keys - # channel does not have a key (position of channel > number of keys!) - if channel_key_index + 1 > len(list_of_keys_tup): - list_of_channels.remove(list_of_channels[channel_key_index]) - # remove channel and key from autjoin option - else: - list_of_channels.remove(list_of_channels[channel_key_index]) - list_of_keys_tup.remove(list_of_keys_tup[channel_key_index]) - # does a key exists, after removing? - if len(list_of_keys_tup) > 0: - list_of_current_keys = ','.join(list_of_keys_tup) - # strip leading ',' - if list_of_current_keys[0] == ',': - list_of_current_keys = list_of_current_keys.lstrip(',') - else: # all keys deleted - list_of_current_keys = '' - - # unset option if everything is gone. - if not list_of_channels and not list_of_current_keys: - ptr_config_autojoin = weechat.config_get('irc.server.%s.autojoin' % server) - if ptr_config_autojoin: - rc = weechat.config_option_unset(ptr_config_autojoin) - return weechat.WEECHAT_RC_OK - - if not set_autojoin_list(server,list_of_channels, list_of_current_keys): - weechat.prnt(buffer,"%s%s: set new value for option failed..." % (weechat.prefix('error'),SCRIPT_NAME)) - - # server and channels given by user - elif (len(argv) >= 3): - server = argv[1] - list_of_current_channels,list_of_current_keys = get_autojoin_list(buffer,server) - - # autojoin option is empty - if list_of_current_channels == 1: - weechat.prnt(buffer,"%s%s: nothing to delete..." % (weechat.prefix('error'),SCRIPT_NAME)) - return weechat.WEECHAT_RC_OK - else: - list_of_channels = args.split(" ")[2:] - if list_of_current_keys: - list_of_current_keys_tup = list_of_current_keys.split(",") - else: - list_of_current_keys_tup = '' - - for i in list_of_channels: - # check if given channel is in list of options - if not i in list_of_current_channels: - continue - channel_key_index = list_of_current_channels.index(i) - # channel does not have a key (position of channel > number of keys!) - if channel_key_index + 1 > len(list_of_current_keys_tup): - list_of_current_channels.remove(i) -# if len(list_of_current_channels) <= 0: -# list_of_current_channels = '' - else: # remove channel and key from autjoin option - list_of_current_channels.remove(i) - list_of_current_keys_tup.remove(list_of_current_keys_tup[channel_key_index]) - # does an key exists, after removing? - if len(list_of_current_keys_tup) > 0: - list_of_current_keys = ','.join(list_of_current_keys_tup) - # strip leading ',' - if list_of_current_keys[0] == ',': - list_of_current_keys = list_of_current_keys.lstrip(',') - else: # all keys deleted - list_of_current_keys = '' - -# for j in list_of_current_channels: -# weechat.prnt(buffer,"chan:%s" % j) -# for j in list_of_current_keys_tup: -# weechat.prnt(buffer,"key :%s" % j) - - # unset option if everything is gone. - if not list_of_current_channels and not list_of_current_keys: - ptr_config_autojoin = weechat.config_get('irc.server.%s.autojoin' % server) - if ptr_config_autojoin: - rc = weechat.config_option_unset(ptr_config_autojoin) - return weechat.WEECHAT_RC_OK - - if not set_autojoin_list(server,list_of_current_channels, list_of_current_keys): - weechat.prnt(buffer,"%s%s: set new value for option failed..." % (weechat.prefix('error'),SCRIPT_NAME)) - - return weechat.WEECHAT_RC_OK - -def get_difference(list1, list2): - return list(set(list1).difference(set(list2))) - -# returns a list of channels and a list of keys -# 1 = something failed, 0 = channel found -def get_autojoin_list(buffer,server): - ptr_config_autojoin = weechat.config_get('irc.server.%s.autojoin' % server) - # option not found! server does not exist - if not ptr_config_autojoin: - weechat.prnt("","%s%s: server '%s' does not exist." % (weechat.prefix('error'),SCRIPT_NAME,server)) - return 1,1 - - # get value from autojoin option - channels = weechat.config_string(ptr_config_autojoin) - if not channels: - return 1,1 - - # check for keys - if len(re.findall(r" ", channels)) == 0: - list_of_channels = channels.split(",") - list_of_keys = [] - elif len(re.findall(r" ", channels)) == 1: - list_of_channels2,list_of_keys = channels.split(" ") - list_of_channels = list_of_channels2.split(",") - else: - weechat.prnt("","%s%s: irc.server.%s.autojoin not valid..." % (weechat.prefix('error'),SCRIPT_NAME,server)) - return 1,1 - - return list_of_channels, list_of_keys - -def set_autojoin_list(server,list_of_channels, list_of_keys): - ptr_config_autojoin = weechat.config_get('irc.server.%s.autojoin' % server) - if not ptr_config_autojoin: - return 0 - - if OPTIONS['sorted'].lower() == 'on' and not list_of_keys: - # no keys, sort the channel-list - channels = '%s' % ','.join(sorted(list_of_channels)) - else: - # don't sort channel-list with given key - channels = '%s' % ','.join(list_of_channels) - - # strip leading ',' - if channels[0] == ',': - channels = channels.lstrip(',') - - # add keys to list of channels - if list_of_keys: - channels = '%s %s' % (channels,list_of_keys) - - rc = weechat.config_option_set(ptr_config_autojoin,channels,1) - if not rc: - return 0 - return 1 - -def autojoinem_completion_cb(data, completion_item, buffer, completion): -# server = weechat.buffer_get_string(buffer, 'localvar_server') # current buffer - input_line = weechat.buffer_get_string(buffer, 'input') - - # get information out of the input_line - argv = input_line.strip().split(" ",3) - if (len(argv) >= 3 and argv[1] == 'del'): - server = argv[2] - - list_of_channels,list_of_keys = get_autojoin_list(buffer,server) - if list_of_channels == 1: - return weechat.WEECHAT_RC_OK - - if (len(argv) >= 4 and argv[1] == 'del'): - list_of_current_channels = argv[3].split(' ') - missing_channels = get_difference(list_of_channels,list_of_current_channels) - if not missing_channels: - return weechat.WEECHAT_RC_OK - list_of_channels = missing_channels - - for i, elem in enumerate(list_of_channels): - weechat.hook_completion_list_add(completion, list_of_channels[i], 0, weechat.WEECHAT_LIST_POS_END) - return weechat.WEECHAT_RC_OK -# ================================[ weechat options & description ]=============================== -def init_options(): - for option,value in OPTIONS.items(): - weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) - if not weechat.config_is_set_plugin(option): - weechat.config_set_plugin(option, value[0]) - OPTIONS[option] = value[0] - else: - OPTIONS[option] = weechat.config_get_plugin(option) - -def toggle_refresh(pointer, name, value): - global OPTIONS - option = name[len('plugins.var.python.' + SCRIPT_NAME + '.'):] # get optionname - OPTIONS[option] = value # save new value - return weechat.WEECHAT_RC_OK -# ================================[ main ]=============================== -if __name__ == "__main__": - if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): - version = weechat.info_get("version_number", "") or 0 - weechat.hook_command(SCRIPT_NAME,SCRIPT_DESC, - 'add [[ ...]] | [-key [...]] ||' - 'del [[ ...]]', - 'add : add channel to irc.server..autojoin\n' - ' -key : name of channelkey\n' - 'del : del channel from irc.server..autojoin\n' - '\n' - 'Examples:\n' - ' add current channel to corresponding server option:\n' - ' /' + SCRIPT_NAME + ' add\n' - ' add all channels from all server to corresponding server option:\n' - ' /allchan /' + SCRIPT_NAME + ' add\n' - ' add channel #weechat to autojoin option on server freenode:\n' - ' /' + SCRIPT_NAME + ' add freenode #weechat\n' - ' add channel #weechat and #weechat-de to autojoin option on server freenode, with channel key for channel #weechat:\n' - ' /' + SCRIPT_NAME + ' add freenode #weechat #weechat-de -key my_channel_key\n' - ' del channels #weechat and #weechat-de from autojoin option on server freenode:\n' - ' /' + SCRIPT_NAME + ' del freenode #weechat #weechat-de', - 'add %(irc_servers) %(irc_server_channels)|%*||' - 'del %(irc_servers) %(plugin_autojoinem)|%*', - 'add_autojoin_cmd_cb', '') - - init_options() - weechat.hook_completion('plugin_autojoinem', 'autojoin_completion', 'autojoinem_completion_cb', '') - weechat.hook_config('plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '') - -# if int(version) >= 0x00030600: -# else: -# weechat.prnt("","%s%s %s" % (weechat.prefix("error"),SCRIPT_NAME,": needs version 0.3.6 or higher")) -# weechat.command("","/wait 1ms /python unload %s" % SCRIPT_NAME) diff --git a/python/automerge.py b/python/automerge.py new file mode 100644 index 00000000..5c6ccb6b --- /dev/null +++ b/python/automerge.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 by Ricky Brent +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +"""Automatically merge new irc buffers according to defined rules. + +History: + * 2017-03-22, Ricky Brent : + version 0.1: initial release + * 2018-03-02, Brady Trainor : + version 0.2: fix the merge command +""" + +from __future__ import print_function +import re +try: + import weechat + IMPORT_OK = True +except ImportError: + print('Script must be run under weechat. http://www.weechat.org') + IMPORT_OK = False + +VERSION = '0.2' +NAME = 'automerge' +AUTHOR = 'Ricky Brent ' +DESC = 'Merge new irc buffers according to defined rules.' + +DELIMITER1 = '|@|' +DELIMITER2 = '|!|' + +CMD_DESC = '''List, add, delete or apply automerge rules. + +Adding a rule takes two parameters: a regular expression to match the target, and \ +a regular expression, integer, or the special string 'server' to match the \ +destination. + +Optionally, the first parameter can be omitted; in this case, the active buffer name will be used. + +Rules can be deleted by their regular expression or their index.''' +CMD_LIST = ['list', 'add', 'delete', 'bufferlist', 'apply'] +CMD_COMPLETE = '||'.join(CMD_LIST) + +def find_merge_id(buf, merge): + """Find the id of the buffer to merge to.""" + mid = -1 + if merge.isdigit(): + mid = merge + elif merge == "server": + server = weechat.buffer_get_string(buf, 'localvar_server') + infolist = weechat.infolist_get("buffer", "", "") + while weechat.infolist_next(infolist) and mid < 0: + if weechat.infolist_string(infolist, "plugin_name") == "irc": + buf2 = weechat.infolist_pointer(infolist, "pointer") + server2 = weechat.buffer_get_string(buf2, 'localvar_server') + if server == server2: + mid = weechat.infolist_integer(infolist, 'number') + weechat.infolist_free(infolist) + else: + infolist = weechat.infolist_get("buffer", "", "") + prog = re.compile(merge) + while weechat.infolist_next(infolist) and mid < 0: + if prog.match(weechat.infolist_string(infolist, "full_name")): + mid = weechat.infolist_integer(infolist, 'number') + weechat.infolist_free(infolist) + return mid + +def get_rules(): + """Return a list of rules.""" + rules = weechat.config_get_plugin('rules') + if rules: + return rules.split(DELIMITER1) + else: + return [] + +def cb_signal_apply_rules(data, signal, buf): + """Callback for signal applying rules to the buffer.""" + name = weechat.buffer_get_string(buf, "full_name") + rules = get_rules() + for rule in rules: + pattern, merge = rule.split(DELIMITER2) + if re.match(pattern, name): + mid = find_merge_id(buf, merge) + if mid >= 0: + weechat.command(buf, "/buffer merge " + str(mid)) + return weechat.WEECHAT_RC_OK + +def cb_command(data, buf, args): + """Handle user commands; add/remove/list rules.""" + list_args = args.split(" ") + commands = { + 'list': cb_command_list, + 'bufferlist': cb_command_bufferlist, + 'add': cb_command_add, + 'delete': cb_command_delete, + 'del': cb_command_delete, + 'apply': cb_command_apply + } + if len(list_args) == 0: + weechat.command(buf, '/help ' + NAME) + return weechat.WEECHAT_RC_OK + elif list_args[0] in commands: + commands[list_args[0]](data, buf, list_args) + else: + weechat.prnt(buf, ("[" + NAME + "] Bad option for /" + NAME + " " + "command, try '/help " + NAME + "' for more info.")) + return weechat.WEECHAT_RC_OK + +def cb_command_list(data, buf, list_args): + """Print a list all rules.""" + weechat.prnt('', "[" + NAME + "] rules (list)") + rules = get_rules() + if len(rules) == 0: + return weechat.WEECHAT_RC_OK + for idx, rule in enumerate(rules): + pattern, merge = rule.split(DELIMITER2) + weechat.prnt('', ' ' + str(idx) + ": " + pattern + ' = ' + merge) + return weechat.WEECHAT_RC_OK + +def cb_command_bufferlist(data, buf, list_args): + """Print a list of all buffer names.""" + infolist = weechat.infolist_get("buffer", "", "") + weechat.prnt('', "[" + NAME + "] buffer list") + while weechat.infolist_next(infolist): + weechat.prnt('', ' ' + weechat.infolist_string(infolist, "full_name")) + weechat.infolist_free(infolist) + return weechat.WEECHAT_RC_OK + +def cb_command_add(data, buf, list_args): + """Add a rule.""" + rules = get_rules() + if len(list_args) == 3: + rule = list_args[1] + match = list_args[2] + elif len(list_args) == 2: + rule = weechat.buffer_get_string(buf, "name") + match = list_args[1] + else: + return bad_command(buf) + rules.append(DELIMITER2.join([rule, match])) + weechat.config_set_plugin('rules', DELIMITER1.join(rules)) + weechat.prnt('', "[" + NAME + "] rule added: " + rule + " => " + match) + return weechat.WEECHAT_RC_OK + +def cb_command_delete(data, buf, list_args): + """Delete a rule.""" + rules = get_rules() + if len(list_args) == 2: + rules2 = [] + for idx, rule in enumerate(rules): + pattern, dummy = rule.split(DELIMITER2) + if str(idx) != list_args[1] and pattern != list_args[1]: + rules2.append(rule) + weechat.config_set_plugin('rules', DELIMITER1.join(rules2)) + if len(rules2) == len(rules): + weechat.prnt('', "[" + NAME + "] rule not found") + else: + weechat.prnt('', "[" + NAME + "] rule deleted") + return weechat.WEECHAT_RC_OK + +def cb_command_apply(data, buf, list_args): + """Apply the rules the all existing buffers; useful when testing a new rule.""" + infolist = weechat.infolist_get("buffer", "", "") + while weechat.infolist_next(infolist): + buf2 = weechat.infolist_pointer(infolist, "pointer") + cb_signal_apply_rules(data, None, buf2) + weechat.infolist_free(infolist) + return weechat.WEECHAT_RC_OK + +def bad_command(buf): + """Print an error message about the command.""" + weechat.prnt(buf, ("[" + NAME + "] Bad option for /" + NAME + " " + "command, try '/help " + NAME + "' for more info.")) + return weechat.WEECHAT_RC_OK + +if IMPORT_OK: + weechat.register(NAME, AUTHOR, VERSION, 'GPL2', DESC, '', '') + weechat.hook_signal('irc_channel_opened', 'cb_signal_apply_rules', '') + weechat.hook_signal('irc_pv_opened', 'cb_signal_apply_rules', '') + weechat.config_set_desc_plugin('rules', 'Rules to follow when automerging.') + if not weechat.config_is_set_plugin('rules'): + weechat.config_set_plugin('rules', '') + weechat.hook_command(NAME, CMD_DESC, '[' + '|'.join(CMD_LIST) + ']', + '', CMD_COMPLETE, 'cb_command', '') diff --git a/python/automode.py b/python/automode.py index 217b88a1..dbf1495f 100644 --- a/python/automode.py +++ b/python/automode.py @@ -38,11 +38,16 @@ # # 2016-06-28 # version 0.1.3: support extended-join messages +# +# 2019-03-09 +# version 0.1.4: support python3 ### +from __future__ import print_function + SCRIPT_NAME = "automode" SCRIPT_AUTHOR = "Elián Hanisch " -SCRIPT_VERSION = "0.1.3" +SCRIPT_VERSION = "0.1.4" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Script for auto op/voice users when they join." @@ -52,8 +57,8 @@ WEECHAT_RC_OK = weechat.WEECHAT_RC_OK import_ok = True except ImportError: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://weechat.flashtux.org/" + print ("This script must be run under WeeChat.") + print ("Get WeeChat now at: http://weechat.flashtux.org/") import_ok = False from fnmatch import fnmatch @@ -165,14 +170,14 @@ def join_cb(data, signal, signal_data): if channel[0] == ':': channel = channel[1:] server = signal[:signal.find(',')] - for mode_type, shorthand in {'op':'o', 'halfop':'h', 'voice':'v'}.iteritems(): + for mode_type, shorthand in {'op':'o', 'halfop':'h', 'voice':'v'}.items(): l = get_config_list('.'.join((server.lower(), channel.lower(), mode_type))) for pattern in l: #debug('checking: %r - %r', prefix, pattern) if fnmatch(prefix, pattern): buf = weechat.buffer_search('irc', '%s.%s' %(server, channel)) if buf: - weechat.command(buf, '/mode {} +{} {}'.format(channel, shorthand, prefix[:prefix.find('!')])) + weechat.command(buf, '/wait 1 /mode {} +{} {}'.format(channel, shorthand, prefix[:prefix.find('!')])) return WEECHAT_RC_OK return WEECHAT_RC_OK @@ -259,12 +264,12 @@ def command(data, buffer, args): else: say('No automodes.', buffer) return WEECHAT_RC_OK - for key, items in patterns.iteritems(): + for key, items in patterns.items(): say('%s[%s%s.%s%s]' %(color_chat_delimiters, color_chat_buffer, key[0], key[1], color_chat_delimiters), buffer) - for type, masks in items.iteritems(): + for type, masks in items.items(): for mask in masks: say(' %s%s%s: %s%s' %(color_chat_nick, type, color_chat_delimiters, @@ -272,7 +277,7 @@ def command(data, buffer, args): mask), buffer) else: raise ValueError("'%s' isn't a valid option. See /help %s" %(cmd, SCRIPT_NAME)) - except ValueError, e: + except ValueError as e: error('Bad argument: %s' %e) return WEECHAT_RC_OK @@ -315,7 +320,7 @@ def completer(data, completion_item, buffer, completion): color_chat_delimiters, color_reset) - for opt, val in settings.iteritems(): + for opt, val in list(settings.items()): if not weechat.config_is_set_plugin(opt): weechat.config_set_plugin(opt, val) diff --git a/python/autopong.py b/python/autopong.py deleted file mode 100644 index 56d0c0f1..00000000 --- a/python/autopong.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2013 Wil Clouser -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -# Passive aggressively auto-replies to private messages which say only the word -# 'ping' thus escalating the unwinnable war of people pinging without saying anything -# else. -# -# History: -# -# 2013-01-11, Wil Clouser : -# v0.1: Initial release -# 2015-10-30, Stanislav Ochotnicky -# v0.2: Reply to public pings as well and add more informative pong -# - -SCRIPT_NAME = "autopong" -SCRIPT_AUTHOR = "Wil Clouser " -SCRIPT_VERSION = "0.2" -SCRIPT_LICENSE = "MIT" -SCRIPT_DESC = "Auto-replies to 'ping' queries" - -import_ok = True - -# This can be changed with `/set plugins.var.python.autopong.reply_text` -defaults = { - "reply_text": "pong (https://blogs.gnome.org/markmc/2014/02/20/naked-pings/)" -} - -try: - import weechat as w -except: - print "Script must be run under weechat. http://www.weechat.org" - import_ok = False - - -def msg_cb(data, buffer, date, tags, displayed, is_hilight, prefix, msg): - reply = w.config_get_plugin('reply_text') - if not w.buffer_get_string(buffer, "localvar_type") == "private": - reply = prefix + ": " + reply - if is_hilight and msg.endswith('ping'): - w.command(buffer, reply) - elif msg == 'ping': - w.command(buffer, reply) - - return w.WEECHAT_RC_OK - -if __name__ == "__main__" and import_ok: - if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, "", ""): - for k, v in defaults.iteritems(): - if not w.config_is_set_plugin(k): - w.config_set_plugin(k, v) - - w.hook_print("", "notify_message", "ping", 1, "msg_cb", "") - w.hook_print("", "notify_private", "ping", 1, "msg_cb", "") diff --git a/python/autosavekey.py b/python/autosavekey.py index bc8b4bfe..f773d8e2 100644 --- a/python/autosavekey.py +++ b/python/autosavekey.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2013-2015 by nils_2 +# Copyright (c) 2013-2019 by nils_2 # # save channel key from protected channel(s) to autojoin or secure data # @@ -19,6 +19,14 @@ # # idea by freenode.elsae # +# 2019-10-03: nils_2, (freenode.#weechat) +# 0.5 : channel wasn't added when autojoin was empty (reported by jackie123) +# : bump min. version to 0.4.2 +# +# 2018-05-11: nils_2, (freenode.#weechat) +# 0.4 : make script python3 compatible +# : add /help text +# # 2015-05-09: nils_2, (freenode.#weechat) # 0.3 : fix: ValueError (reported by: Darpa) # @@ -28,7 +36,7 @@ # 2013-10-03: nils_2, (freenode.#weechat) # 0.1 : initial release # -# requires: WeeChat version 0.3.2 +# requires: WeeChat version 0.4.2 # # Development is currently hosted at # https://github.com/weechatter/weechat-scripts @@ -37,15 +45,15 @@ import weechat,re except Exception: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") quit() SCRIPT_NAME = "autosavekey" SCRIPT_AUTHOR = "nils_2 " -SCRIPT_VERSION = "0.3" +SCRIPT_VERSION = "0.5" SCRIPT_LICENSE = "GPL" -SCRIPT_DESC = "save channel key from protected channel(s) to autojoin or secure data" +SCRIPT_DESC = "save channel key from protected channel(s) to autojoin option or secure data" OPTIONS = { 'mute' : ('off','execute command silently, only error messages will be displayed.'), 'secure' : ('off','change channel key in secure data.'), @@ -67,8 +75,6 @@ def irc_raw_in_324_cb(data, signal, signal_data): new_key = argv[3] autojoin_list = get_autojoin(server) - if not autojoin_list: - return weechat.WEECHAT_RC_OK # check autojoin for space if len(re.findall(r" ", autojoin_list)) > 1: @@ -112,7 +118,6 @@ def irc_raw_in_324_cb(data, signal, signal_data): argv_channels.insert(0, channel) argv_keys.insert(0,new_key) - # check weechat version and if secure option is on and secure data will be used for this key? if int(version) >= 0x00040200 and OPTIONS['secure'].lower() == 'on' and sec_data == 1: weechat.command('','%s/secure set %s %s' % (use_mute(),sec_data_name,new_key)) @@ -120,7 +125,10 @@ def irc_raw_in_324_cb(data, signal, signal_data): if sec_data == 1: weechat.prnt('', '%s%s: key for channel "%s.%s" not changed! option "plugins.var.python.%s.secure" is off and you are using secured data for key.' % (weechat.prefix('error'),SCRIPT_NAME,server,channel,SCRIPT_NAME) ) return weechat.WEECHAT_RC_OK - new_joined_option = '%s %s' % (','.join(argv_channels),','.join(argv_keys)) + if not autojoin_list: # autojoin option is empty! + new_joined_option = '%s %s' % (channel,new_key) + else: + new_joined_option = '%s %s' % (','.join(argv_channels),','.join(argv_keys)) save_autojoin_option(server,new_joined_option) return weechat.WEECHAT_RC_OK @@ -143,8 +151,6 @@ def irc_raw_in_mode_cb(data, signal, signal_data): def add_key_to_list(server,channel,new_key): autojoin_list = get_autojoin(server) - if not autojoin_list: - return weechat.WEECHAT_RC_OK # check autojoin for space if len(re.findall(r" ", autojoin_list)) == 0: @@ -179,7 +185,10 @@ def add_key_to_list(server,channel,new_key): weechat.prnt('', '%s%s: key for channel "%s.%s" not changed! option "plugins.var.python.%s.secure" is off and you are using secured data for key.' % (weechat.prefix('error'),SCRIPT_NAME,server,channel,SCRIPT_NAME) ) return weechat.WEECHAT_RC_OK argv_keys[channel_pos_in_list] = new_key - new_joined_option = '%s %s' % (','.join(argv_channels),','.join(argv_keys)) + if not autojoin_list: # autojoin option is empty! + new_joined_option = '%s %s' % (channel,new_key) + else: + new_joined_option = '%s %s' % (','.join(argv_channels),','.join(argv_keys)) save_autojoin_option(server,new_joined_option) return weechat.WEECHAT_RC_OK @@ -216,6 +225,11 @@ def check_key_for_secure(argv_keys,position): if argv_keys[position][0:11] == '${sec.data.': sec_data = 1 return sec_data + +def cmd_autosavekey(data, buffer, args): + weechat.command('', '/help %s' % SCRIPT_NAME) + return weechat.WEECHAT_RC_OK + # ================================[ weechat options & description ]=============================== def init_options(): for option,value in OPTIONS.items(): @@ -235,13 +249,20 @@ def toggle_refresh(pointer, name, value): # ================================[ main ]=============================== if __name__ == "__main__": if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): + weechat.hook_command(SCRIPT_NAME,SCRIPT_DESC, + '', + 'You have to edit options with: /set *autosavekey*\n' + 'I suggest using /iset script or /fset plugin.\n', + '', + 'cmd_autosavekey', + '') version = weechat.info_get("version_number", "") or 0 - if int(version) >= 0x00030200: + if int(version) >= 0x00040200: init_options() weechat.hook_config( 'plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '' ) weechat.hook_signal("*,irc_raw_in_mode","irc_raw_in_mode_cb","") weechat.hook_signal("*,irc_raw_in_324","irc_raw_in_324_cb","") else: - weechat.prnt("","%s%s %s" % (weechat.prefix("error"),SCRIPT_NAME,": needs version 0.3.2 or higher")) + weechat.prnt("","%s%s %s" % (weechat.prefix("error"),SCRIPT_NAME,": needs version 0.4.2 or higher")) weechat.command("","/wait 1ms /python unload %s" % SCRIPT_NAME) diff --git a/python/autosort.py b/python/autosort.py index 6854b9e6..73f27eb4 100644 --- a/python/autosort.py +++ b/python/autosort.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2013-2014 Maarten de Vries +# Copyright (C) 2013-2017 Maarten de Vries # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,11 +20,42 @@ # Autosort automatically keeps your buffers sorted and grouped by server. # You can define your own sorting rules. See /help autosort for more details. # -# http://github.com/de-vri.es/weechat-autosort +# https://github.com/de-vri-es/weechat-autosort # # # Changelog: +# 3.10: +# * Fix exception in `/autosort helpers swap`. +# 3.9: +# * Remove `buffers.pl` from recommended settings. +# 3,8: +# * Fix relative sorting on script name in default rules. +# * Document a useful property of stable sort algorithms. +# 3.7: +# * Make default rules work with bitlbee, matrix and slack. +# 3.6: +# * Add more documentation on provided info hooks. +# 3.5: +# * Add ${info:autosort_escape,...} to escape arguments for other info hooks. +# 3.4: +# * Fix rate-limit of sorting to prevent high CPU load and lock-ups. +# * Fix bug in parsing empty arguments for info hooks. +# * Add debug_log option to aid with debugging. +# * Correct a few typos. +# 3.3: +# * Fix the /autosort debug command for unicode. +# * Update the default rules to work better with Slack. +# 3.2: +# * Fix python3 compatiblity. +# 3.1: +# * Use colors to format the help text. +# 3.0: +# * Switch to evaluated expressions for sorting. +# * Add `/autosort debug` command. +# * Add ${info:autosort_replace,from,to,text} to replace substrings in sort rules. +# * Add ${info:autosort_order,value,first,second,third} to ease writing sort rules. +# * Make tab completion context aware. # 2.8: # * Fix compatibility with python 3 regarding unicode handling. # 2.7: @@ -49,242 +80,129 @@ # -import weechat -import re import json +import math +import re +import sys +import time +import weechat SCRIPT_NAME = 'autosort' SCRIPT_AUTHOR = 'Maarten de Vries ' -SCRIPT_VERSION = '2.8' +SCRIPT_VERSION = '3.10' SCRIPT_LICENSE = 'GPL3' -SCRIPT_DESC = 'Automatically or manually keep your buffers sorted and grouped by server.' - - -config = None -hooks = [] - -class HumanReadableError(Exception): - pass - - -def parse_int(arg, arg_name = 'argument'): - ''' Parse an integer and provide a more human readable error. ''' - arg = arg.strip() - try: - return int(arg) - except ValueError: - raise HumanReadableError('Invalid {0}: expected integer, got "{1}".'.format(arg_name, arg)) - - -class Pattern: - ''' A simple glob-like pattern for matching buffer names. ''' - - def __init__(self, pattern, case_sensitive): - ''' Construct a pattern from a string. ''' - escaped = False - char_class = 0 - chars = '' - regex = '' - for c in pattern: - if escaped and char_class: - escaped = False - chars += re.escape(c) - elif escaped: - escaped = False - regex += re.escape(c) - elif c == '\\': - escaped = True - elif c == '*' and not char_class: - regex += '[^.]*' - elif c == '?' and not char_class: - regex += '[^.]' - elif c == '[' and not char_class: - char_class = 1 - chars = '' - elif c == '^' and char_class and not chars: - chars += '^' - elif c == ']' and char_class and chars not in ('', '^'): - char_class = False - regex += '[' + chars + ']' - elif c == '-' and char_class: - chars += '-' - elif char_class: - chars += re.escape(c) - else: - regex += re.escape(c) - - if char_class: - raise ValueError("unmatched opening '['") - if escaped: - raise ValueError("unexpected trailing '\\'") - - if case_sensitive: - self.regex = re.compile('^' + regex + '$') - else: - self.regex = re.compile('^' + regex + '$', flags = re.IGNORECASE) - self.pattern = pattern +SCRIPT_DESC = 'Flexible automatic (or manual) buffer sorting based on eval expressions.' - def match(self, input): - ''' Match the pattern against a string. ''' - return self.regex.match(input) +config = None +hooks = [] +signal_delay_timer = None +sort_limit_timer = None +sort_queued = False -class FriendlyList(object): - ''' A list with human readable errors. ''' - def __init__(self): - self.__data = [] +# Make sure that unicode, bytes and str are always available in python2 and 3. +# For python 2, str == bytes +# For python 3, str == unicode +if sys.version_info[0] >= 3: + unicode = str - def raw(self): - return self.__data - - def append(self, value): - ''' Add a rule to the list. ''' - self.__data.append(value) - - def insert(self, index, value): - ''' Add a rule to the list. ''' - if not 0 <= index <= len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}], got {1}.'.format(len(self), index)) - self.__data.insert(index, value) - - def pop(self, index): - ''' Remove a rule from the list and return it. ''' - if not 0 <= index < len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}), got {1}.'.format(len(self), index)) - return self.__data.pop(index) - - def move(self, index_a, index_b): - ''' Move a rule to a new position in the list. ''' - self.insert(index_b, self.pop(index_a)) - - def swap(self, index_a, index_b): - ''' Swap two elements in the list. ''' - self[index_a], self[index_b] = self[index_b], self[index_a] - - def __len__(self): - return len(self.__data) - - def __getitem__(self, index): - if not 0 <= index < len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}), got {1}.'.format(len(self), index)) - return self.__data[index] - - def __setitem__(self, index, value): - if not 0 <= index < len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}), got {1}.'.format(len(self), index)) - self.__data[index] = value - - def __iter__(self): - return iter(self.__data) - - -class RuleList(FriendlyList): - ''' A list of rules to test buffer names against. ''' - rule_regex = re.compile(r'^(.*)=\s*([+-]?[^=]*)$') - - def __init__(self, rules): - ''' Construct a RuleList from a list of rules. ''' - super(RuleList, self).__init__() - for rule in rules: self.append(rule) - - def get_score(self, name): - ''' Get the sort score of a partial name according to a rule list. ''' - for rule in self: - if rule[0].match(name): return rule[1] - return 999999999 - - def encode(self): - ''' Encode the rules for storage. ''' - return json.dumps(list(map(lambda x: (x[0].pattern, x[1]), self))) - - @staticmethod - def decode(blob, case_sensitive): - ''' Parse rules from a string blob. ''' - result = [] - - try: - decoded = json.loads(blob) - except ValueError: - log('Invalid rules: expected JSON encoded list of pairs, got "{0}".'.format(blob)) - return [], 0 - - for rule in decoded: - # Rules must be a pattern,score pair. - if len(rule) != 2: - log('Invalid rule: expected (pattern, score), got "{0}". Rule ignored.'.format(rule)) - continue - - # Rules must have a valid pattern. - try: - pattern = Pattern(rule[0], case_sensitive) - except ValueError as e: - log('Invalid pattern: {0} in "{1}". Rule ignored.'.format(e, rule[0])) - continue +def ensure_str(input): + ''' + Make sure the given type if the correct string type for the current python version. + That means bytes for python2 and unicode for python3. + ''' + if not isinstance(input, str): + if isinstance(input, bytes): + return input.encode('utf-8') + if isinstance(input, unicode): + return input.decode('utf-8') + return input - # Rules must have a valid score. - try: - score = int(rule[1]) - except ValueError as e: - log('Invalid score: expected an integer, got "{0}". Rule ignored.'.format(score)) - continue - result.append((pattern, score)) +if hasattr(time, 'perf_counter'): + perf_counter = time.perf_counter +else: + perf_counter = time.clock - return RuleList(result) +def casefold(string): + if hasattr(string, 'casefold'): return string.casefold() + # Fall back to lowercasing for python2. + return string.lower() - @staticmethod - def parse_rule(arg, case_sensitive): - ''' Parse a rule argument. ''' - arg = arg.strip() - match = RuleList.rule_regex.match(arg) - if not match: - raise HumanReadableError('Invalid rule: expected " = ", got "{0}".'.format(arg)) +def list_swap(values, a, b): + values[a], values[b] = values[b], values[a] - pattern = match.group(1).strip() - try: - pattern = Pattern(pattern, case_sensitive) - except ValueError as e: - raise HumanReadableError('Invalid pattern: {0} in "{1}".'.format(e, pattern)) +def list_move(values, old_index, new_index): + values.insert(new_index, values.pop(old_index)) - score = parse_int(match.group(2), 'score') - return (pattern, score) +def list_find(collection, value): + for i, elem in enumerate(collection): + if elem == value: return i + return None +class HumanReadableError(Exception): + pass -def decode_replacements(blob): - ''' Decode a replacement list encoded as JSON. ''' - result = FriendlyList() +def parse_int(arg, arg_name = 'argument'): + ''' Parse an integer and provide a more human readable error. ''' + arg = arg.strip() try: - decoded = json.loads(blob) + return int(arg) except ValueError: - log('Invalid replacement list: expected JSON encoded list of pairs, got "{0}".'.format(blob)) - return [], 0 + raise HumanReadableError('Invalid {0}: expected integer, got "{1}".'.format(arg_name, arg)) - for replacement in decoded: - # Replacements must be a (string, string) pair. - if len(replacement) != 2: - log('Invalid replacement pattern: expected (pattern, replacement), got "{0}". Replacement ignored.'.format(rule)) - continue - result.append(replacement) +def decode_rules(blob): + parsed = json.loads(blob) + if not isinstance(parsed, list): + log('Malformed rules, expected a JSON encoded list of strings, but got a {0}. No rules have been loaded. Please fix the setting manually.'.format(type(parsed))) + return [] - return result + for i, entry in enumerate(parsed): + if not isinstance(entry, (str, unicode)): + log('Rule #{0} is not a string but a {1}. No rules have been loaded. Please fix the setting manually.'.format(i, type(entry))) + return [] + return parsed -def encode_replacements(replacements): - ''' Encode a list of replacement patterns as JSON. ''' - return json.dumps(replacements.raw()) +def decode_helpers(blob): + parsed = json.loads(blob) + if not isinstance(parsed, dict): + log('Malformed helpers, expected a JSON encoded dictionary but got a {0}. No helpers have been loaded. Please fix the setting manually.'.format(type(parsed))) + return {} + for key, value in parsed.items(): + if not isinstance(value, (str, unicode)): + log('Helper "{0}" is not a string but a {1}. No helpers have been loaded. Please fix setting manually.'.format(key, type(value))) + return {} + return parsed class Config: ''' The autosort configuration. ''' default_rules = json.dumps([ - ('core', 0), - ('irc', 2), - ('*', 1), - - ('irc.irc_raw', 0), - ('irc.server', 1), + '${core_first}', + '${info:autosort_order,${info:autosort_escape,${script_or_plugin}},core,*,irc,bitlbee,matrix,slack}', + '${script_or_plugin}', + '${irc_raw_first}', + '${server}', + '${info:autosort_order,${type},server,*,channel,private}', + '${hashless_name}', + '${buffer.full_name}', ]) - default_replacements = '[]' - default_signals = 'buffer_opened buffer_merged buffer_unmerged buffer_renamed' + default_helpers = json.dumps({ + 'core_first': '${if:${buffer.full_name}!=core.weechat}', + 'irc_raw_first': '${if:${buffer.full_name}!=irc.irc_raw}', + 'irc_raw_last': '${if:${buffer.full_name}==irc.irc_raw}', + 'hashless_name': '${info:autosort_replace,#,,${info:autosort_escape,${buffer.name}}}', + 'script_or_plugin': '${if:${script_name}?${script_name}:${plugin}}', + }) + + default_signal_delay = 5 + default_sort_limit = 100 + + default_signals = 'buffer_opened buffer_merged buffer_unmerged buffer_renamed' def __init__(self, filename): ''' Initialize the configuration. ''' @@ -292,26 +210,32 @@ def __init__(self, filename): self.filename = filename self.config_file = weechat.config_new(self.filename, '', '') self.sorting_section = None + self.v3_section = None self.case_sensitive = False - self.group_irc = True self.rules = [] - self.replacements = [] + self.helpers = {} self.signals = [] + self.signal_delay = Config.default_signal_delay, + self.sort_limit = Config.default_sort_limit, self.sort_on_config = True + self.debug_log = False self.__case_sensitive = None - self.__group_irc = None self.__rules = None - self.__replacements = None + self.__helpers = None self.__signals = None + self.__signal_delay = None + self.__sort_limit = None self.__sort_on_config = None + self.__debug_log = None if not self.config_file: log('Failed to initialize configuration file "{0}".'.format(self.filename)) return self.sorting_section = weechat.config_new_section(self.config_file, 'sorting', False, False, '', '', '', '', '', '', '', '', '', '') + self.v3_section = weechat.config_new_section(self.config_file, 'v3', False, False, '', '', '', '', '', '', '', '', '', '') if not self.sorting_section: log('Failed to initialize section "sorting" of configuration file.') @@ -326,39 +250,62 @@ def __init__(self, filename): '', '', '', '', '', '' ) - self.__group_irc = weechat.config_new_option( + weechat.config_new_option( self.config_file, self.sorting_section, - 'group_irc', 'boolean', - 'If this option is on, the script pretends that IRC channel/private buffers are renamed to "irc.server.{network}.{channel}" rather than "irc.{network}.{channel}".' + - 'This ensures that these buffers are grouped with their respective server buffer.', - '', 0, 0, 'on', 'on', 0, + 'rules', 'string', + 'Sort rules used by autosort v2.x and below. Not used by autosort anymore.', + '', 0, 0, '', '', 0, '', '', '', '', '', '' ) - self.__rules = weechat.config_new_option( + weechat.config_new_option( self.config_file, self.sorting_section, + 'replacements', 'string', + 'Replacement patterns used by autosort v2.x and below. Not used by autosort anymore.', + '', 0, 0, '', '', 0, + '', '', '', '', '', '' + ) + + self.__rules = weechat.config_new_option( + self.config_file, self.v3_section, 'rules', 'string', 'An ordered list of sorting rules encoded as JSON. See /help autosort for commands to manipulate these rules.', '', 0, 0, Config.default_rules, Config.default_rules, 0, '', '', '', '', '', '' ) - self.__replacements = weechat.config_new_option( - self.config_file, self.sorting_section, - 'replacements', 'string', - 'An ordered list of replacement patterns to use on buffer name components, encoded as JSON. See /help autosort for commands to manipulate these replacements.', - '', 0, 0, Config.default_replacements, Config.default_replacements, 0, + self.__helpers = weechat.config_new_option( + self.config_file, self.v3_section, + 'helpers', 'string', + 'A dictionary helper variables to use in the sorting rules, encoded as JSON. See /help autosort for commands to manipulate these helpers.', + '', 0, 0, Config.default_helpers, Config.default_helpers, 0, '', '', '', '', '', '' ) self.__signals = weechat.config_new_option( self.config_file, self.sorting_section, 'signals', 'string', - 'The signals that will cause autosort to resort your buffer list. Seperate signals with spaces.', + 'A space separated list of signals that will cause autosort to resort your buffer list.', '', 0, 0, Config.default_signals, Config.default_signals, 0, '', '', '', '', '', '' ) + self.__signal_delay = weechat.config_new_option( + self.config_file, self.sorting_section, + 'signal_delay', 'integer', + 'Delay in milliseconds to wait after a signal before sorting the buffer list. This prevents triggering many times if multiple signals arrive in a short time. It can also be needed to wait for buffer localvars to be available.', + '', 0, 1000, str(Config.default_signal_delay), str(Config.default_signal_delay), 0, + '', '', '', '', '', '' + ) + + self.__sort_limit = weechat.config_new_option( + self.config_file, self.sorting_section, + 'sort_limit', 'integer', + 'Minimum delay in milliseconds to wait after sorting before signals can trigger a sort again. This is effectively a rate limit on sorting. Keeping signal_delay low while setting this higher can reduce excessive sorting without a long initial delay.', + '', 0, 1000, str(Config.default_sort_limit), str(Config.default_sort_limit), 0, + '', '', '', '', '', '' + ) + self.__sort_on_config = weechat.config_new_option( self.config_file, self.sorting_section, 'sort_on_config_change', 'boolean', @@ -367,6 +314,14 @@ def __init__(self, filename): '', '', '', '', '', '' ) + self.__debug_log = weechat.config_new_option( + self.config_file, self.sorting_section, + 'debug_log', 'boolean', + 'If enabled, print more debug messages. Not recommended for normal usage.', + '', 0, 0, 'off', 'off', 0, + '', '', '', '', '', '' + ) + if weechat.config_read(self.config_file) != weechat.WEECHAT_RC_OK: log('Failed to load configuration file.') @@ -379,105 +334,105 @@ def reload(self): ''' Load configuration variables. ''' self.case_sensitive = weechat.config_boolean(self.__case_sensitive) - self.group_irc = weechat.config_boolean(self.__group_irc) - rules_blob = weechat.config_string(self.__rules) - replacements_blob = weechat.config_string(self.__replacements) - signals_blob = weechat.config_string(self.__signals) + rules_blob = weechat.config_string(self.__rules) + helpers_blob = weechat.config_string(self.__helpers) + signals_blob = weechat.config_string(self.__signals) - self.rules = RuleList.decode(rules_blob, self.case_sensitive) - self.replacements = decode_replacements(replacements_blob) + self.rules = decode_rules(rules_blob) + self.helpers = decode_helpers(helpers_blob) self.signals = signals_blob.split() + self.signal_delay = weechat.config_integer(self.__signal_delay) + self.sort_limit = weechat.config_integer(self.__sort_limit) self.sort_on_config = weechat.config_boolean(self.__sort_on_config) + self.debug_log = weechat.config_boolean(self.__debug_log) def save_rules(self, run_callback = True): ''' Save the current rules to the configuration. ''' - weechat.config_option_set(self.__rules, RuleList.encode(self.rules), run_callback) + weechat.config_option_set(self.__rules, json.dumps(self.rules), run_callback) - def save_replacements(self, run_callback = True): - ''' Save the current replacement patterns to the configuration. ''' - weechat.config_option_set(self.__replacements, encode_replacements(self.replacements), run_callback) + def save_helpers(self, run_callback = True): + ''' Save the current helpers to the configuration. ''' + weechat.config_option_set(self.__helpers, json.dumps(self.helpers), run_callback) def pad(sequence, length, padding = None): ''' Pad a list until is has a certain length. ''' return sequence + [padding] * max(0, (length - len(sequence))) - def log(message, buffer = 'NULL'): weechat.prnt(buffer, 'autosort: {0}'.format(message)) +def debug(message, buffer = 'NULL'): + if config.debug_log: + weechat.prnt(buffer, 'autosort: debug: {0}'.format(message)) def get_buffers(): ''' Get a list of all the buffers in weechat. ''' - buffers = [] - - buffer_list = weechat.infolist_get('buffer', '', '') - - while weechat.infolist_next(buffer_list): - name = weechat.infolist_string (buffer_list, 'full_name') - number = weechat.infolist_integer(buffer_list, 'number') - - # Buffer is merged with one we already have in the list, skip it. - if number <= len(buffers): - continue - buffers.append((name, number - 1)) - - weechat.infolist_free(buffer_list) - return buffers - - -def preprocess(buffer, config): + hdata = weechat.hdata_get('buffer') + buffer = weechat.hdata_get_list(hdata, "gui_buffers"); + + result = [] + while buffer: + number = weechat.hdata_integer(hdata, buffer, 'number') + result.append((number, buffer)) + buffer = weechat.hdata_pointer(hdata, buffer, 'next_buffer') + return hdata, result + +class MergedBuffers(list): + """ A list of merged buffers, possibly of size 1. """ + def __init__(self, number): + super(MergedBuffers, self).__init__() + self.number = number + +def merge_buffer_list(buffers): ''' - Preprocess a buffers names. + Group merged buffers together. + The output is a list of MergedBuffers. ''' - - # Make sure the name is a unicode string. - # On python3 this is a NOP since the string type is already decoded as UTF-8. - if isinstance(buffer, bytes): - buffer = buffer.decode('utf-8') - - if not config.case_sensitive: - buffer = buffer.lower() - - for replacement in config.replacements: - buffer = buffer.replace(replacement[0], replacement[1]) - - buffer = buffer.split('.') - if config.group_irc and len(buffer) >= 2 and buffer[0] == 'irc' and buffer[1] not in ('server', 'irc_raw'): - buffer.insert(1, 'server') - - return buffer - - -def buffer_sort_key(rules): - ''' Create a sort key function for a buffer list from a rule list. ''' + if not buffers: return [] + result = {} + for number, buffer in buffers: + if number not in result: result[number] = MergedBuffers(number) + result[number].append(buffer) + return result.values() + +def sort_buffers(hdata, buffers, rules, helpers, case_sensitive): + for merged in buffers: + for buffer in merged: + name = weechat.hdata_string(hdata, buffer, 'name') + + return sorted(buffers, key=merged_sort_key(rules, helpers, case_sensitive)) + +def buffer_sort_key(rules, helpers, case_sensitive): + ''' Create a sort key function for a list of lists of merged buffers. ''' def key(buffer): - result = [] - name = '' - for word in preprocess(buffer[0], config): - name += ('.' if name else '') + word - result.append((rules.get_score(name), word)) + extra_vars = {} + for helper_name, helper in sorted(helpers.items()): + expanded = weechat.string_eval_expression(helper, {"buffer": buffer}, {}, {}) + extra_vars[helper_name] = expanded if case_sensitive else casefold(expanded) + result = [] + for rule in rules: + expanded = weechat.string_eval_expression(rule, {"buffer": buffer}, extra_vars, {}) + result.append(expanded if case_sensitive else casefold(expanded)) return result return key +def merged_sort_key(rules, helpers, case_sensitive): + buffer_key = buffer_sort_key(rules, helpers, case_sensitive) + def key(merged): + best = None + for buffer in merged: + this = buffer_key(buffer) + if best is None or this < best: best = this + return best + return key -def apply_buffer_order(order): +def apply_buffer_order(buffers): ''' Sort the buffers in weechat according to the given order. ''' - indices = list(order) - reverse = [0] * len(indices) - for i, index in enumerate(indices): - reverse[index] = i - - for i in range(len(indices)): - wanted = indices[i] - if wanted == i: continue - # Weechat buffers are 1-indexed, but our indices aren't. - weechat.command('', '/buffer swap {0} {1}'.format(i + 1, wanted + 1)) - indices[reverse[i]] = wanted - reverse[wanted] = reverse[i] - + for i, buffer in enumerate(buffers): + weechat.buffer_set(buffer[0], "number", str(i + 1)) def split_args(args, expected, optional = 0): ''' Split an argument string in the desired number of arguments. ''' @@ -486,31 +441,63 @@ def split_args(args, expected, optional = 0): raise HumanReadableError('Expected at least {0} arguments, got {1}.'.format(expected, len(split))) return split[:-1] + pad(split[-1].split(' ', optional), optional + 1, '') +def do_sort(verbose = False): + start = perf_counter() + + hdata, buffers = get_buffers() + buffers = merge_buffer_list(buffers) + buffers = sort_buffers(hdata, buffers, config.rules, config.helpers, config.case_sensitive) + apply_buffer_order(buffers) + + elapsed = perf_counter() - start + if verbose: + log("Finished sorting buffers in {0:.4f} seconds.".format(elapsed)) + else: + debug("Finished sorting buffers in {0:.4f} seconds.".format(elapsed)) def command_sort(buffer, command, args): ''' Sort the buffers and print a confirmation. ''' - on_buffers_changed() - log("Finished sorting buffers.", buffer) + do_sort(True) return weechat.WEECHAT_RC_OK +def command_debug(buffer, command, args): + hdata, buffers = get_buffers() + buffers = merge_buffer_list(buffers) + + # Show evaluation results. + log('Individual evaluation results:') + start = perf_counter() + key = buffer_sort_key(config.rules, config.helpers, config.case_sensitive) + results = [] + for merged in buffers: + for buffer in merged: + fullname = weechat.hdata_string(hdata, buffer, 'full_name') + results.append((fullname, key(buffer))) + elapsed = perf_counter() - start + + for fullname, result in results: + fullname = ensure_str(fullname) + result = [ensure_str(x) for x in result] + log('{0}: {1}'.format(fullname, result)) + log('Computing evaluation results took {0:.4f} seconds.'.format(elapsed)) + + return weechat.WEECHAT_RC_OK def command_rule_list(buffer, command, args): ''' Show the list of sorting rules. ''' output = 'Sorting rules:\n' for i, rule in enumerate(config.rules): - output += ' {0}: {1} = {2}\n'.format(i, rule[0].pattern, rule[1]) + output += ' {0}: {1}\n'.format(i, rule) if not len(config.rules): output += ' No sorting rules configured.\n' - log(output, buffer) + log(output ) return weechat.WEECHAT_RC_OK def command_rule_add(buffer, command, args): ''' Add a rule to the rule list. ''' - rule = RuleList.parse_rule(args, config.case_sensitive) - - config.rules.append(rule) + config.rules.append(args) config.save_rules() command_rule_list(buffer, command, '') @@ -521,7 +508,6 @@ def command_rule_insert(buffer, command, args): ''' Insert a rule at the desired position in the rule list. ''' index, rule = split_args(args, 2) index = parse_int(index, 'index') - rule = RuleList.parse_rule(rule, config.case_sensitive) config.rules.insert(index, rule) config.save_rules() @@ -533,7 +519,6 @@ def command_rule_update(buffer, command, args): ''' Update a rule in the rule list. ''' index, rule = split_args(args, 2) index = parse_int(index, 'index') - rule = RuleList.parse_rule(rule, config.case_sensitive) config.rules[index] = rule config.save_rules() @@ -558,7 +543,7 @@ def command_rule_move(buffer, command, args): index_a = parse_int(index_a, 'index') index_b = parse_int(index_b, 'index') - config.rules.move(index_a, index_b) + list_move(config.rules, index_a, index_b) config.save_rules() command_rule_list(buffer, command, '') return weechat.WEECHAT_RC_OK @@ -570,96 +555,75 @@ def command_rule_swap(buffer, command, args): index_a = parse_int(index_a, 'index') index_b = parse_int(index_b, 'index') - config.rules.swap(index_a, index_b) + list_swap(config.rules, index_a, index_b) config.save_rules() command_rule_list(buffer, command, '') return weechat.WEECHAT_RC_OK -def command_replacement_list(buffer, command, args): - ''' Show the list of sorting rules. ''' - output = 'Replacement patterns:\n' - for i, pattern in enumerate(config.replacements): - output += ' {0}: {1} -> {2}\n'.format(i, pattern[0], pattern[1]) - if not len(config.replacements): - output += ' No replacement patterns configured.' - log(output, buffer) - - return weechat.WEECHAT_RC_OK +def command_helper_list(buffer, command, args): + ''' Show the list of helpers. ''' + output = 'Helper variables:\n' + width = max(map(lambda x: len(x) if len(x) <= 30 else 0, config.helpers.keys())) -def command_replacement_add(buffer, command, args): - ''' Add a rule to the rule list. ''' - pattern, replacement = split_args(args, 1, 1) - - config.replacements.append((pattern, replacement)) - config.save_replacements() - command_replacement_list(buffer, command, '') + for name, expression in sorted(config.helpers.items()): + output += ' {0:>{width}}: {1}\n'.format(name, expression, width=width) + if not len(config.helpers): + output += ' No helper variables configured.' + log(output) return weechat.WEECHAT_RC_OK -def command_replacement_insert(buffer, command, args): - ''' Insert a rule at the desired position in the rule list. ''' - index, pattern, replacement = split_args(args, 2, 1) - index = parse_int(index, 'index') - - config.replacements.insert(index, (pattern, replacement)) - config.save_replacements() - command_replacement_list(buffer, command, '') - return weechat.WEECHAT_RC_OK +def command_helper_set(buffer, command, args): + ''' Add/update a helper to the helper list. ''' + name, expression = split_args(args, 2) + config.helpers[name] = expression + config.save_helpers() + command_helper_list(buffer, command, '') -def command_replacement_update(buffer, command, args): - ''' Update a rule in the rule list. ''' - index, pattern, replacement = split_args(args, 2, 1) - index = parse_int(index, 'index') - - config.replacements[index] = (pattern, replacement) - config.save_replacements() - command_replacement_list(buffer, command, '') return weechat.WEECHAT_RC_OK +def command_helper_delete(buffer, command, args): + ''' Delete a helper from the helper list. ''' + name = args.strip() -def command_replacement_delete(buffer, command, args): - ''' Delete a rule from the rule list. ''' - index = args.strip() - index = parse_int(index, 'index') - - config.replacements.pop(index) - config.save_replacements() - command_replacement_list(buffer, command, '') + del config.helpers[name] + config.save_helpers() + command_helper_list(buffer, command, '') return weechat.WEECHAT_RC_OK -def command_replacement_move(buffer, command, args): - ''' Move a rule to a new position. ''' - index_a, index_b = split_args(args, 2) - index_a = parse_int(index_a, 'index') - index_b = parse_int(index_b, 'index') +def command_helper_rename(buffer, command, args): + ''' Rename a helper to a new position. ''' + old_name, new_name = split_args(args, 2) - config.replacements.move(index_a, index_b) - config.save_replacements() - command_replacement_list(buffer, command, '') + try: + config.helpers[new_name] = config.helpers[old_name] + del config.helpers[old_name] + except KeyError: + raise HumanReadableError('No such helper: {0}'.format(old_name)) + config.save_helpers() + command_helper_list(buffer, command, '') return weechat.WEECHAT_RC_OK -def command_replacement_swap(buffer, command, args): - ''' Swap two rules. ''' - index_a, index_b = split_args(args, 2) - index_a = parse_int(index_a, 'index') - index_b = parse_int(index_b, 'index') +def command_helper_swap(buffer, command, args): + ''' Swap two helpers. ''' + a, b = split_args(args, 2) + try: + config.helpers[b], config.helpers[a] = config.helpers[a], config.helpers[b] + except KeyError as e: + raise HumanReadableError('No such helper: {0}'.format(e.args[0])) - config.replacements.swap(index_a, index_b) - config.save_replacements() - command_replacement_list(buffer, command, '') + config.save_helpers() + command_helper_list(buffer, command, '') return weechat.WEECHAT_RC_OK - - - def call_command(buffer, command, args, subcommands): - ''' Call a subccommand from a dictionary. ''' + ''' Call a subcommand from a dictionary. ''' subcommand, tail = pad(args.split(' ', 1), 2, '') subcommand = subcommand.strip() if (subcommand == ''): @@ -676,30 +640,162 @@ def call_command(buffer, command, args, subcommands): log('{0}: command not found'.format(' '.join(command))) return weechat.WEECHAT_RC_ERROR +def on_signal(data, signal, signal_data): + global signal_delay_timer + global sort_queued -def on_buffers_changed(*args, **kwargs): - ''' Called whenever the buffer list changes. ''' - buffers = get_buffers() - buffers.sort(key=buffer_sort_key(config.rules)) - apply_buffer_order([i for _, i in buffers]) + # If the sort limit timeout is started, we're in the hold-off time after sorting, just queue a sort. + if sort_limit_timer is not None: + if sort_queued: + debug('Signal {0} ignored, sort limit timeout is active and sort is already queued.'.format(signal)) + else: + debug('Signal {0} received but sort limit timeout is active, sort is now queued.'.format(signal)) + sort_queued = True + return weechat.WEECHAT_RC_OK + + # If the signal delay timeout is started, a signal was recently received, so ignore this signal. + if signal_delay_timer is not None: + debug('Signal {0} ignored, signal delay timeout active.'.format(signal)) + return weechat.WEECHAT_RC_OK + + # Otherwise, start the signal delay timeout. + debug('Signal {0} received, starting signal delay timeout of {1} ms.'.format(signal, config.signal_delay)) + weechat.hook_timer(config.signal_delay, 0, 1, "on_signal_delay_timeout", "") return weechat.WEECHAT_RC_OK +def on_signal_delay_timeout(pointer, remaining_calls): + """ Called when the signal_delay_timer triggers. """ + global signal_delay_timer + global sort_limit_timer + global sort_queued -def on_config_changed(*args, **kwargs): - ''' Called whenever the configuration changes. ''' - config.reload() + signal_delay_timer = None + + # If the sort limit timeout was started, we're still in the no-sort period, so just queue a sort. + if sort_limit_timer is not None: + debug('Signal delay timeout expired, but sort limit timeout is active, sort is now queued.') + sort_queued = True + return weechat.WEECHAT_RC_OK + + # Time to sort! + debug('Signal delay timeout expired, starting sort.') + do_sort() + + # Start the sort limit timeout if not disabled. + if config.sort_limit > 0: + debug('Starting sort limit timeout of {0} ms.'.format(config.sort_limit)) + sort_limit_timer = weechat.hook_timer(config.sort_limit, 0, 1, "on_sort_limit_timeout", "") + + return weechat.WEECHAT_RC_OK +def on_sort_limit_timeout(pointer, remainin_calls): + """ Called when de sort_limit_timer triggers. """ + global sort_limit_timer + global sort_queued + + # If no signal was received during the timeout, we're done. + if not sort_queued: + debug('Sort limit timeout expired without receiving a signal.') + sort_limit_timer = None + return weechat.WEECHAT_RC_OK + + # Otherwise it's time to sort. + debug('Signal received during sort limit timeout, starting queued sort.') + do_sort() + sort_queued = False + + # Start the sort limit timeout again if not disabled. + if config.sort_limit > 0: + debug('Starting sort limit timeout of {0} ms.'.format(config.sort_limit)) + sort_limit_timer = weechat.hook_timer(config.sort_limit, 0, 1, "on_sort_limit_timeout", "") + + return weechat.WEECHAT_RC_OK + + +def apply_config(): # Unhook all signals and hook the new ones. for hook in hooks: weechat.unhook(hook) for signal in config.signals: - hooks.append(weechat.hook_signal(signal, 'on_buffers_changed', '')) + hooks.append(weechat.hook_signal(signal, 'on_signal', '')) if config.sort_on_config: - on_buffers_changed() + debug('Sorting because configuration changed.') + do_sort() + +def on_config_changed(*args, **kwargs): + ''' Called whenever the configuration changes. ''' + config.reload() + apply_config() return weechat.WEECHAT_RC_OK +def parse_arg(args): + if not args: return '', None + + result = '' + escaped = False + for i, c in enumerate(args): + if not escaped: + if c == '\\': + escaped = True + continue + elif c == ',': + return result, args[i+1:] + result += c + escaped = False + return result, None + +def parse_args(args, max = None): + result = [] + i = 0 + while max is None or i < max: + i += 1 + arg, args = parse_arg(args) + if arg is None: break + result.append(arg) + if args is None: break + return result, args + +def on_info_escape(pointer, name, arguments): + result = '' + for c in arguments: + if c == '\\': + result += '\\\\' + elif c == ',': + result += '\\,' + else: + result +=c + return result + +def on_info_replace(pointer, name, arguments): + arguments, rest = parse_args(arguments, 3) + if rest or len(arguments) < 3: + log('usage: ${{info:{0},old,new,text}}'.format(name)) + return '' + old, new, text = arguments + + return text.replace(old, new) + +def on_info_order(pointer, name, arguments): + arguments, rest = parse_args(arguments) + if len(arguments) < 1: + log('usage: ${{info:{0},value,first,second,third,...}}'.format(name)) + return '' + + value = arguments[0] + keys = arguments[1:] + if not keys: return '0' + + # Find the value in the keys (or '*' if we can't find it) + result = list_find(keys, value) + if result is None: result = list_find(keys, '*') + if result is None: result = len(keys) + + # Pad result with leading zero to make sure string sorting works. + width = int(math.log10(len(keys))) + 1 + return '{0:0{1}}'.format(result, width) + def on_autosort_command(data, buffer, args): ''' Called when the autosort command is invoked. ''' @@ -707,6 +803,7 @@ def on_autosort_command(data, buffer, args): return call_command(buffer, ['/autosort'], args, { ' ': command_sort, 'sort': command_sort, + 'debug': command_debug, 'rules': { ' ': command_rule_list, @@ -718,168 +815,262 @@ def on_autosort_command(data, buffer, args): 'move': command_rule_move, 'swap': command_rule_swap, }, - 'replacements': { - ' ': command_replacement_list, - 'list': command_replacement_list, - 'add': command_replacement_add, - 'insert': command_replacement_insert, - 'update': command_replacement_update, - 'delete': command_replacement_delete, - 'move': command_replacement_move, - 'swap': command_replacement_swap, + 'helpers': { + ' ': command_helper_list, + 'list': command_helper_list, + 'set': command_helper_set, + 'delete': command_helper_delete, + 'rename': command_helper_rename, + 'swap': command_helper_swap, }, - 'sort': on_buffers_changed, }) except HumanReadableError as e: - log(e, buffer) + log(e) return weechat.WEECHAT_RC_ERROR +def add_completions(completion, words): + for word in words: + weechat.completion_list_add(completion, word, 0, weechat.WEECHAT_LIST_POS_END) + +def autosort_complete_rules(words, completion): + if len(words) == 0: + add_completions(completion, ['add', 'delete', 'insert', 'list', 'move', 'swap', 'update']) + if len(words) == 1 and words[0] in ('delete', 'insert', 'move', 'swap', 'update'): + add_completions(completion, map(str, range(len(config.rules)))) + if len(words) == 2 and words[0] in ('move', 'swap'): + add_completions(completion, map(str, range(len(config.rules)))) + if len(words) == 2 and words[0] in ('update'): + try: + add_completions(completion, [config.rules[int(words[1])]]) + except KeyError: pass + except ValueError: pass + else: + add_completions(completion, ['']) + return weechat.WEECHAT_RC_OK + +def autosort_complete_helpers(words, completion): + if len(words) == 0: + add_completions(completion, ['delete', 'list', 'rename', 'set', 'swap']) + elif len(words) == 1 and words[0] in ('delete', 'rename', 'set', 'swap'): + add_completions(completion, sorted(config.helpers.keys())) + elif len(words) == 2 and words[0] == 'swap': + add_completions(completion, sorted(config.helpers.keys())) + elif len(words) == 2 and words[0] == 'rename': + add_completions(completion, sorted(config.helpers.keys())) + elif len(words) == 2 and words[0] == 'set': + try: + add_completions(completion, [config.helpers[words[1]]]) + except KeyError: pass + return weechat.WEECHAT_RC_OK -command_description = r''' -NOTE: For the best effect, you may want to consider setting the option irc.look.server_buffer to independent and buffers.look.indenting to on. +def on_autosort_complete(data, name, buffer, completion): + cmdline = weechat.buffer_get_string(buffer, "input") + cursor = weechat.buffer_get_integer(buffer, "input_pos") + prefix = cmdline[:cursor] + words = prefix.split()[1:] + + # If the current word isn't finished yet, + # ignore it for coming up with completion suggestions. + if prefix[-1] != ' ': words = words[:-1] + + if len(words) == 0: + add_completions(completion, ['debug', 'helpers', 'rules', 'sort']) + elif words[0] == 'rules': + return autosort_complete_rules(words[1:], completion) + elif words[0] == 'helpers': + return autosort_complete_helpers(words[1:], completion) + return weechat.WEECHAT_RC_OK -# Commands +command_description = r'''{*white}# General commands{reset} -## Miscellaneous -/autosort sort +{*white}/autosort {brown}sort{reset} Manually trigger the buffer sorting. +{*white}/autosort {brown}debug{reset} +Show the evaluation results of the sort rules for each buffer. -## Sorting rules -/autosort rules list +{*white}# Sorting rule commands{reset} + +{*white}/autosort{brown} rules list{reset} Print the list of sort rules. -/autosort rules add = +{*white}/autosort {brown}rules add {cyan}{reset} Add a new rule at the end of the list. -/autosort rules insert = +{*white}/autosort {brown}rules insert {cyan} {reset} Insert a new rule at the given index in the list. -/autosort rules update = -Update a rule in the list with a new pattern and score. +{*white}/autosort {brown}rules update {cyan} {reset} +Update a rule in the list with a new expression. -/autosort rules delete +{*white}/autosort {brown}rules delete {cyan} Delete a rule from the list. -/autosort rules move +{*white}/autosort {brown}rules move {cyan} {reset} Move a rule from one position in the list to another. -/autosort rules swap +{*white}/autosort {brown}rules swap {cyan} {reset} Swap two rules in the list -## Replacement patterns +{*white}# Helper variable commands{reset} + +{*white}/autosort {brown}helpers list +Print the list of helper variables. -/autosort replacements list -Print the list of replacement patterns. +{*white}/autosort {brown}helpers set {cyan} +Add or update a helper variable with the given name. -/autosort replacements add -Add a new replacement pattern at the end of the list. +{*white}/autosort {brown}helpers delete {cyan} +Delete a helper variable. -/autosort replacements insert -Insert a new replacement pattern at the given index in the list. +{*white}/autosort {brown}helpers rename {cyan} +Rename a helper variable. -/autosort replacements update -Update a replacement pattern in the list. +{*white}/autosort {brown}helpers swap {cyan} +Swap the expressions of two helper variables in the list. -/autosort replacements delete -Delete a replacement pattern from the list. -/autosort replacements move -Move a replacement pattern from one position in the list to another. +{*white}# Info hooks{reset} +Autosort comes with a number of info hooks to add some extra functionality to regular weechat eval strings. +Info hooks can be used in eval strings in the form of {cyan}${{info:some_hook,arguments}}{reset}. -/autosort replacements swap -Swap two replacement pattern in the list +Commas and backslashes in arguments to autosort info hooks (except for {cyan}${{info:autosort_escape}}{reset}) must be escaped with a backslash. +{*white}${{info:{brown}autosort_replace{white},{cyan}pattern{white},{cyan}replacement{white},{cyan}source{white}}}{reset} +Replace all occurrences of {cyan}pattern{reset} with {cyan}replacement{reset} in the string {cyan}source{reset}. +Can be used to ignore certain strings when sorting by replacing them with an empty string. -# Introduction -Autosort is a weechat script to automatically keep your buffers sorted. -The sort order can be customized by defining your own sort rules, -but the default should be sane enough for most people. -It can also group IRC channel/private buffers under their server buffer if you like. +For example: {cyan}${{info:autosort_replace,cat,dog,the dog is meowing}}{reset} expands to "the cat is meowing". -Autosort first turns buffer names into a list of their components by splitting on them on the period character. -For example, the buffer name "irc.server.freenode" is turned into ['irc', 'server', 'freenode']. -The list of buffers is then lexicographically sorted. +{*white}${{info:{brown}autosort_order{white},{cyan}value{white},{cyan}option0{white},{cyan}option1{white},{cyan}option2{white},{cyan}...{white}}} +Generate a zero-padded number that corresponds to the index of {cyan}value{reset} in the list of options. +If one of the options is the special value {brown}*{reset}, then any value not explicitly mentioned will be sorted at that position. +Otherwise, any value that does not match an option is assigned the highest number available. +Can be used to easily sort buffers based on a manual sequence. -To facilitate custom sort orders, it is possible to assign a score to each component individually before the sorting is done. -Any name component that did not get a score assigned will be sorted after those that did receive a score. -Components are always sorted on their score first and on their name second. -Lower scores are sorted first. +For example: {cyan}${{info:autosort_order,${{server}},freenode,oftc,efnet}}{reset} will sort freenode before oftc, followed by efnet and then any remaining servers. +Alternatively, {cyan}${{info:autosort_order,${{server}},freenode,oftc,*,efnet}}{reset} will sort any unlisted servers after freenode and oftc, but before efnet. -## Automatic or manual sorting -By default, autosort will automatically sort your buffer list whenever a buffer is opened, merged, unmerged or renamed. -This should keep your buffers sorted in almost all situations. -However, you may wish to change the list of signals that cause your buffer list to be sorted. -Simply edit the "autosort.sorting.signals" option to add or remove any signal you like. -If you remove all signals you can still sort your buffers manually with the "/autosort sort" command. -To prevent all automatic sorting, "autosort.sorting.sort_on_config_change" should also be set to off. +{*white}${{info:{brown}autosort_escape{white},{cyan}text{white}}}{reset} +Escape commas and backslashes in {cyan}text{reset} by prepending them with a backslash. +This is mainly useful to pass arbitrary eval strings as arguments to other autosort info hooks. +Otherwise, an eval string that expands to something with a comma would be interpreted as multiple arguments. -## Grouping IRC buffers -In weechat, IRC channel/private buffers are named "irc..<#channel>", -and IRC server buffers are named "irc.server.". -This does not work very well with lexicographical sorting if you want all buffers for one network grouped together. -That is why autosort comes with the "autosort.sorting.group_irc" option, -which secretly pretends IRC channel/private buffers are called "irc.server..<#channel>". -The buffers are not actually renamed, autosort simply pretends they are for sorting purposes. +For example, it can be used to safely pass buffer names to {cyan}${{info:autosort_replace}}{reset} like so: +{cyan}${{info:autosort_replace,##,#,${{info:autosort_escape,${{buffer.name}}}}}}{reset}. -## Replacement patterns -Sometimes you may want to ignore some characters for sorting purposes. -On Freenode for example, you may wish to ignore the difference between channels starting with a double or a single hash sign. -To do so, simply add a replacement pattern that replaces ## with # with the following command: -/autosort replacements add ## # -Replacement patterns do not support wildcards or special characters at the moment. +{*white}# Description +Autosort is a weechat script to automatically keep your buffers sorted. The sort +order can be customized by defining your own sort rules, but the default should +be sane enough for most people. It can also group IRC channel/private buffers +under their server buffer if you like. -## Sort rules -You can assign scores to name components by defining sort rules. -The first rule that matches a component decides the score. -Further rules are not examined. -Sort rules use the following syntax: - = +Autosort uses a stable sorting algorithm, meaning that you can manually move buffers +to change their relative order, if they sort equal with your rule set. -You can use the "/autosort rules" command to show and manipulate the list of sort rules. +{*white}# Sort rules{reset} +Autosort evaluates a list of eval expressions (see {*default}/help eval{reset}) and sorts the +buffers based on evaluated result. Earlier rules will be considered first. Only +if earlier rules produced identical results is the result of the next rule +considered for sorting purposes. +You can debug your sort rules with the `{*default}/autosort debug{reset}` command, which will +print the evaluation results of each rule for each buffer. -Allowed special characters in the glob patterns are: +{*brown}NOTE:{reset} The sort rules for version 3 are not compatible with version 2 or vice +versa. You will have to manually port your old rules to version 3 if you have any. -Pattern | Meaning ---------|-------- -* | Matches a sequence of any characters except for periods. -? | Matches a single character, but not a period. -[a-z] | Matches a single character in the given regex-like character class. -[^ab] | A negated regex-like character class. -\* | A backslash escapes the next characters and removes its special meaning. -\\ | A literal backslash. +{*white}# Helper variables{reset} +You may define helper variables for the main sort rules to keep your rules +readable. They can be used in the main sort rules as variables. For example, +a helper variable named `{cyan}foo{reset}` can be accessed in a main rule with the +string `{cyan}${{foo}}{reset}`. +{*white}# Automatic or manual sorting{reset} +By default, autosort will automatically sort your buffer list whenever a buffer +is opened, merged, unmerged or renamed. This should keep your buffers sorted in +almost all situations. However, you may wish to change the list of signals that +cause your buffer list to be sorted. Simply edit the `{cyan}autosort.sorting.signals{reset}` +option to add or remove any signal you like. -## Example -As an example, consider the following rule list: -0: core = 0 -1: irc = 2 -2: * = 1 +If you remove all signals you can still sort your buffers manually with the +`{*default}/autosort sort{reset}` command. To prevent all automatic sorting, the option +`{cyan}autosort.sorting.sort_on_config_change{reset}` should also be disabled. -3: irc.server.*.#* = 1 -4: irc.server.*.* = 0 +{*white}# Recommended settings +For the best visual effect, consider setting the following options: +{*white}/set {cyan}irc.look.server_buffer{reset} {brown}independent{reset} -Rule 0 ensures the core buffer is always sorted first. -Rule 1 sorts IRC buffers last and rule 2 puts all remaining buffers in between the two. +This setting allows server buffers to be sorted independently, which is +needed to create a hierarchical tree view of the server and channel buffers. -Rule 3 and 4 would make no sense with the group_irc option off. -With the option on though, these rules will sort private buffers before regular channel buffers. -Rule 3 matches channel buffers and assigns them a higher score, -while rule 4 matches the buffers that remain and assigns them a lower score. -The same effect could also be achieved with a single rule: -irc.server.*.[^#]* = 0 +If you are using the {*default}buflist{reset} plugin you can (ab)use Unicode to draw a tree +structure with the following setting (modify to suit your need): +{*white}/set {cyan}buflist.format.indent {brown}"${{color:237}}${{if:${{buffer.next_buffer.local_variables.type}}=~^(channel|private)$?├─:└─}}"{reset} ''' -command_completion = 'sort||rules list|add|insert|update|delete|move|swap||replacements list|add|insert|update|delete|move|swap' +command_completion = '%(plugin_autosort) %(plugin_autosort) %(plugin_autosort) %(plugin_autosort) %(plugin_autosort)' + +info_replace_description = ( + 'Replace all occurrences of `pattern` with `replacement` in the string `source`. ' + 'Can be used to ignore certain strings when sorting by replacing them with an empty string. ' + 'See /help autosort for examples.' +) +info_replace_arguments = 'pattern,replacement,source' + +info_order_description = ( + 'Generate a zero-padded number that corresponds to the index of `value` in the list of options. ' + 'If one of the options is the special value `*`, then any value not explicitly mentioned will be sorted at that position. ' + 'Otherwise, any value that does not match an option is assigned the highest number available. ' + 'Can be used to easily sort buffers based on a manual sequence. ' + 'See /help autosort for examples.' +) +info_order_arguments = 'value,first,second,third,...' + +info_escape_description = ( + 'Escape commas and backslashes in `text` by prepending them with a backslash. ' + 'This is mainly useful to pass arbitrary eval strings as arguments to other autosort info hooks. ' + 'Otherwise, an eval string that expands to something with a comma would be interpreted as multiple arguments.' + 'See /help autosort for examples.' +) +info_escape_arguments = 'text' if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): config = Config('autosort') - weechat.hook_config('autosort.*', 'on_config_changed', '') - weechat.hook_command('autosort', command_description, '', '', command_completion, 'on_autosort_command', 'NULL') - on_config_changed() + colors = { + 'default': weechat.color('default'), + 'reset': weechat.color('reset'), + 'black': weechat.color('black'), + 'red': weechat.color('red'), + 'green': weechat.color('green'), + 'brown': weechat.color('brown'), + 'yellow': weechat.color('yellow'), + 'blue': weechat.color('blue'), + 'magenta': weechat.color('magenta'), + 'cyan': weechat.color('cyan'), + 'white': weechat.color('white'), + '*default': weechat.color('*default'), + '*black': weechat.color('*black'), + '*red': weechat.color('*red'), + '*green': weechat.color('*green'), + '*brown': weechat.color('*brown'), + '*yellow': weechat.color('*yellow'), + '*blue': weechat.color('*blue'), + '*magenta': weechat.color('*magenta'), + '*cyan': weechat.color('*cyan'), + '*white': weechat.color('*white'), + } + + weechat.hook_config('autosort.*', 'on_config_changed', '') + weechat.hook_completion('plugin_autosort', '', 'on_autosort_complete', '') + weechat.hook_command('autosort', command_description.format(**colors), '', '', command_completion, 'on_autosort_command', '') + weechat.hook_info('autosort_escape', info_escape_description, info_escape_arguments, 'on_info_escape', '') + weechat.hook_info('autosort_replace', info_replace_description, info_replace_arguments, 'on_info_replace', '') + weechat.hook_info('autosort_order', info_order_description, info_order_arguments, 'on_info_order', '') + + apply_config() diff --git a/python/away_action.py b/python/away_action.py index 51f7461b..eae61b89 100644 --- a/python/away_action.py +++ b/python/away_action.py @@ -40,6 +40,10 @@ # # # History: +# 2020-01-31: +# version 0.7: add include_text option - contributed by dargad +# 2019-10-02: +# version 0.6: make compatible with python 3 # 2014-05-10: # version 0.5: change hook_print callback argument type of # displayed/highlight (WeeChat >= 1.0) @@ -54,9 +58,11 @@ # ### +from __future__ import print_function + SCRIPT_NAME = "away_action" SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "0.5" +SCRIPT_VERSION = "0.7" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Run command on highlight and privmsg when away" @@ -68,6 +74,7 @@ 'command' : '/mute msg ', # Command to be ran, nick and message will be inserted at the end 'force_enabled' : 'off', 'include_channel': 'off', # Option to include channel in insert after command. +'include_text' : 'on', # Option to include message text in insert after command. } ignore_nick, ignore_text, ignore_channel = (), (), () @@ -79,8 +86,8 @@ import_ok = True from fnmatch import fnmatch except: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") import_ok = False class Ignores(object): @@ -145,11 +152,24 @@ def away_cb(data, buffer, time, tags, display, hilight, prefix, msg): w.prnt('', '%s: Error: %s' %(SCRIPT_NAME, 'command must start with /')) return WEECHAT_RC_OK + format = "{command}" + data = { + 'command': command, + 'nick': prefix, + 'channel': channel, + 'message': msg + } + if 'channel' in locals() and \ w.config_get_plugin('include_channel') == 'on': - w.command('', '%s @%s <%s> %s' %(command, channel, prefix, msg)) - else: - w.command('', '%s <%s> %s' %(command, prefix, msg)) + format += " {channel}" + + format += " {nick}" + + if w.config_get_plugin('include_text') == 'on': + format += " {message}" + + w.command('', format.format(**data)) return WEECHAT_RC_OK def ignore_update(*args): @@ -169,7 +189,7 @@ def info_hook_cb(data, info_name, arguments): '', ''): - for opt, val in settings.iteritems(): + for opt, val in settings.items(): if not weechat.config_is_set_plugin(opt): weechat.config_set_plugin(opt, val) diff --git a/python/axolotl.py b/python/axolotl.py index 2c4bc1d5..e9696613 100644 --- a/python/axolotl.py +++ b/python/axolotl.py @@ -5,7 +5,7 @@ # =============================================================== SCRIPT_NAME = "axolotl" SCRIPT_AUTHOR = "David R. Andersen " -SCRIPT_VERSION = "0.1.0" +SCRIPT_VERSION = "0.1.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "encrypt/decrypt PRIVMSGs using axolotl ratchet and GPG" @@ -169,19 +169,20 @@ def encryption_statusbar(data, item, window): # register plugin if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, \ SCRIPT_LICENSE, SCRIPT_DESC, "", "UTF-8"): - weechat_dir = weechat.info_get("weechat_dir","") - key_dir = weechat.config_get_plugin('key_dir') version = weechat.info_get("version_number", "") or 0 if int(version) < 0x00030000: - weechat.prnt("", "%s%s: WeeChat 0.3.0 is required for this script." - % (weechat.prefix("error"), SCRIPT_NAME)) + weechat.prnt("", "%s%s: WeeChat 0.3.0 is required for this script." + % (weechat.prefix("error"), SCRIPT_NAME)) else: - weechat.bar_item_new('axolotl', 'encryption_statusbar', '') - for option, default_value in script_options.iteritems(): - if not weechat.config_is_set_plugin(option): - weechat.config_set_plugin(option, default_value) - - # register the modifiers - weechat.hook_modifier("irc_in_privmsg", "decrypt", "") - weechat.hook_modifier("irc_out_privmsg", "encrypt", "") - weechat.hook_signal("buffer_switch","update_encryption_status","") + weechat_dir = weechat.info_get("weechat_data_dir", "") \ + or weechat.info_get("weechat_dir", "") + key_dir = weechat.config_get_plugin('key_dir') + weechat.bar_item_new('axolotl', 'encryption_statusbar', '') + for option, default_value in script_options.iteritems(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, default_value) + + # register the modifiers + weechat.hook_modifier("irc_in_privmsg", "decrypt", "") + weechat.hook_modifier("irc_out_privmsg", "encrypt", "") + weechat.hook_signal("buffer_switch","update_encryption_status","") diff --git a/python/bandwidth.py b/python/bandwidth.py index 5b3e1a2f..cfc795f5 100644 --- a/python/bandwidth.py +++ b/python/bandwidth.py @@ -25,6 +25,8 @@ # # History: # +# 2020-09-05, mumixam +# version 1.1: add python3 support while still supporting python2 # 2011-12-02, quazgaa # version 1.0: Complete rewrite. Make script more featureful, robust, and accurate. # Thanks to FlashCode and ze for helping debug. @@ -41,27 +43,31 @@ try: import weechat except: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" - raise SystemExit, 0 + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") + raise SystemExit(0) try: from time import time except: - print "Error importing time module." - raise SystemExit, 0 + print("Error importing time module.") + raise SystemExit(0) +try: + unichr +except NameError: + unichr = chr # defines SCRIPT_NAME = "bandwidth" SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "1.0" +SCRIPT_VERSION = "1.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Displays network interface bandwidth (KiB/s and MiB/s) on a bar" SCRIPT_SETTINGS = { "device" : ("eth0", "Network interface(s) to monitor, in order, separated by ';'"), "refresh_rate" : ("5", "Refresh rate in seconds"), - "format" : (("%N(" + unichr(8595) + "%DV%DU/s " + unichr(8593) + "%UV%UU/s)").encode('utf-8'), + "format" : (("%N(" + unichr(8595) + "%DV%DU/s " + unichr(8593) + "%UV%UU/s)"), "Output formatting: %N = network interface, %DV = downstream value, %DU = downstream units (K or M), %UV = upstream value, %UU = upstream units (K or M). Note: default setting uses UTF-8"), "separator" : (" ", "String displayed between output for multiple devices"), } @@ -85,9 +91,16 @@ def main(): weechat.config_unset_plugin('display_unit') # set default settings - for option in SCRIPT_SETTINGS.iterkeys(): + for option in SCRIPT_SETTINGS.keys(): if not weechat.config_is_set_plugin(option): - weechat.config_set_plugin(option, SCRIPT_SETTINGS[option][0]) + value = SCRIPT_SETTINGS[option][0] + if isinstance(value, str): + pass + elif isinstance(value, bytes): + pass + elif isinstance(value, unicode): + value = value.encode('utf8') + weechat.config_set_plugin(option, value) if version >= 0x00030500: weechat.config_set_desc_plugin(option, SCRIPT_SETTINGS[option][1]) @@ -156,7 +169,7 @@ def bandwidth_item_cb(data, buffer, args): device_exist = False # get the downstream and upstream byte counts - for i in xrange(num_devices): + for i in range(num_devices): for line in lines: if (device[i] + ':') in line: field = line.split(':')[1].strip().split() @@ -175,7 +188,7 @@ def bandwidth_item_cb(data, buffer, args): if num_last_devices != num_devices: new_device_list = True else: - for i in xrange(num_devices): + for i in range(num_devices): if device[i] != last_device[i]: new_device_list = True break @@ -189,7 +202,7 @@ def bandwidth_item_cb(data, buffer, args): # set them afresh (also if script first starting), if not last_device: if num_devices: - for i in xrange(num_devices): + for i in range(num_devices): last_device.append(device[i]) last_down_bytes.append(current_down_bytes[i]) last_up_bytes.append(current_up_bytes[i]) @@ -204,7 +217,7 @@ def bandwidth_item_cb(data, buffer, args): time_elapsed = current_time - last_time last_time = current_time - for i in xrange(num_devices): + for i in range(num_devices): down_rate.append((current_down_bytes[i] - last_down_bytes[i]) / time_elapsed / 1024) up_rate.append((current_up_bytes[i] - last_up_bytes[i]) / time_elapsed / 1024) last_down_bytes[i] = current_down_bytes[i] @@ -215,7 +228,7 @@ def bandwidth_item_cb(data, buffer, args): output = '' # determine downstream and upstream units; format the output - for i in xrange(num_devices): + for i in range(num_devices): if '%DU' in output_item[i]: if down_rate[i] >= 1024: down_rate[i] = round((down_rate[i]/1024), 1) diff --git a/python/beinc.py b/python/beinc.py index 7bb74b95..cdbfe1f3 100644 --- a/python/beinc.py +++ b/python/beinc.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- - -# Blackmore's Enhanced IRC-Notification Collection (BEINC) v1.1 -# Copyright (C) 2013-2015 Simeon Simeonov +# Blackmore's Enhanced IRC-Notification Collection (BEINC) +# Copyright (C) 2013-2024 Simeon Simeonov # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -15,343 +13,363 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - +"""BEINC client for Weechat""" import datetime -import httplib +import io import json import os -import random -import socket import ssl -import sys -import urllib -import urllib2 +import urllib.parse +import urllib.request import weechat - __author__ = 'Simeon Simeonov' -__version__ = '1.1' +__version__ = '4.4' __license__ = 'GPL3' enabled = True -global_values = dict() +global_values = {} # few constants # BEINC_POLICY_NONE = 0 BEINC_POLICY_ALL = 1 BEINC_POLICY_LIST_ONLY = 2 -BEINC_CURRENT_CONFIG_VERSION = 1 - - -class ValidHTTPSConnection(httplib.HTTPConnection): - """ - Implements a simple CERT verification functionality - """ - - default_port = httplib.HTTPS_PORT +BEINC_CURRENT_CONFIG_VERSION = 2 - def __init__(self, *args, **kwargs): - httplib.HTTPConnection.__init__(self, *args, **kwargs) - def connect(self): - sock = socket.create_connection((self.host, self.port), - self.timeout, self.source_address) - if self._tunnel_host: - self.sock = sock - self._tunnel() - self.sock = ssl.wrap_socket(sock, - ca_certs=global_beinc_cert_file, - cert_reqs=ssl.CERT_REQUIRED) - - -class ValidHTTPSHandler(urllib2.HTTPSHandler): - """ - Implements a simple CERT verification functionality - """ - - def https_open(self, req): - return self.do_open(ValidHTTPSConnection, req) - - -class WeechatTarget(object): +class WeechatTarget: """ The target (destination) class + Each remote destination is represented as a WeechatTarget object """ def __init__(self, target_dict): """ - target_dict: the config-dictionary node that represents this instance - """ - self.__name = target_dict.get( - 'name', - ''.join([chr(random.randrange(97, 123)) for x in range(4)])) - self.__url = target_dict.get('target_url') - self.__password = target_dict.get('target_password') - self.__pm_title_template = target_dict.get('pm_title_template', - '%s @ %S') - self.__pm_message_template = target_dict.get('pm_message_template', - '%m') - self.__cm_title_template = target_dict.get('cm_title_template', - '%c @ %S') - self.__cm_message_template = target_dict.get('cm_message_template', - '%s -> %m') - self.__nm_title_template = target_dict.get('nm_title_template', - '%c @ %S') - self.__nm_message_template = target_dict.get('nm_message_template', - '%s -> %m') - self.__chans = set(target_dict.get('channel_list', list())) - self.__nicks = set(target_dict.get('nick_list', list())) - self.__chan_messages_policy = int(target_dict.get( - 'channel_messages_policy', - BEINC_POLICY_LIST_ONLY)) - self.__priv_messages_policy = int(target_dict.get( - 'private_messages_policy', - BEINC_POLICY_ALL)) - self.__notifications_policy = int(target_dict.get( - 'notifications_policy', - BEINC_POLICY_ALL)) - self.__cert_file = target_dict.get('target_cert_file') - self.__timestamp_format = target_dict.get('target_timestamp_format', - '%H:%M:%S') - self.__debug = bool(target_dict.get('debug', False)) - self.__enabled = bool(target_dict.get('enabled', True)) - self.__socket_timeout = int(target_dict.get('socket_timeout', 3)) + :param target_dict: The config-dict node that represents this instance + :type target_dict: dict + """ + self._name = target_dict.get('name', '') + if self._name == '': + raise Exception('"name" not defined for target') + self._url = target_dict.get('target_url', '') + if self._url == '': + raise Exception('"target_url" not defined for target') + self._password = target_dict.get('target_password', '') + self._pm_title_template = target_dict.get( + 'pm_title_template', '%s @ %S' + ) + self._pm_message_template = target_dict.get( + 'pm_message_template', '%m' + ) + self._cm_title_template = target_dict.get( + 'cm_title_template', '%c @ %S' + ) + self._cm_message_template = target_dict.get( + 'cm_message_template', '%s -> %m' + ) + self._nm_title_template = target_dict.get( + 'nm_title_template', '%c @ %S' + ) + self._nm_message_template = target_dict.get( + 'nm_message_template', '%s -> %m' + ) + self._chans = set(target_dict.get('channel_list', [])) + self._nicks = set(target_dict.get('nick_list', [])) + self._chan_messages_policy = int( + target_dict.get('channel_messages_policy', BEINC_POLICY_LIST_ONLY) + ) + self._priv_messages_policy = int( + target_dict.get('private_messages_policy', BEINC_POLICY_ALL) + ) + self._notifications_policy = int( + target_dict.get('notifications_policy', BEINC_POLICY_ALL) + ) + self._cert_file = target_dict.get('target_cert_file') + self._timestamp_format = target_dict.get( + 'target_timestamp_format', '%H:%M:%S' + ) + self._debug = bool(target_dict.get('debug', False)) + self._enabled = bool(target_dict.get('enabled', True)) + self._socket_timeout = int(target_dict.get('socket_timeout', 3)) + self._ssl_ciphers = target_dict.get('ssl_ciphers', '') + self._disable_hostname_check = bool( + target_dict.get('disable-hostname-check', False) + ) + self._ssl_version = target_dict.get('ssl_version', 'auto') + self._last_message = None # datetime.datetime instance + self._context = None + self._context_setup() @property def name(self): - """ - Target name (read-only property) - """ - return self.__name + """Target name (read-only property)""" + return self._name @property def chans(self): - """ - Target channel list (read-only property) - """ - return self.__chans + """Target channel list (read-only property)""" + return self._chans @property def nicks(self): - """ - Target nick list (read-only property) - """ - return self.__nicks + """Target nick list (read-only property)""" + return self._nicks @property def channel_messages_policy(self): - """ - The target's channel messages policy (read-only property) - """ - return self.__chan_messages_policy + """The target's channel messages policy (read-only property)""" + return self._chan_messages_policy @property def private_messages_policy(self): - """ - The target's private messages policy (read-only property) - """ - return self.__priv_messages_policy + """The target's private messages policy (read-only property)""" + return self._priv_messages_policy @property def notifications_policy(self): - """ - The target's notifications policy (read-only property) - """ - return self.__notifications_policy + """The target's notifications policy (read-only property)""" + return self._notifications_policy @property def enabled(self): - """ - The target's enabled status (bool property) - """ - return self.__enabled + """The target's enabled status (bool property)""" + return self._enabled @enabled.setter def enabled(self, value): - """ - The target's enabled status (bool property) - """ - self.__enabled = value + """The target's enabled status (bool property)""" + self._enabled = value def __repr__(self): - """ - """ - return 'name: {0}\nurl: {1}\nchannel_list: {2}\nnick_list: {3}\n'\ - 'channel_messages_policy: {4}\nprivate_messages_policy: {5}\n'\ - 'notifications_policy: {6}\nenabled: {7}\ndebug: {8}\n\n'.format( - self.__name, - self.__url, - ', '.join(self.__chans), - ', '.join(self.__nicks), - self.__chan_messages_policy, - self.__priv_messages_policy, - self.__notifications_policy, - 'yes' if self.__enabled else 'no', - 'yes' if self.__debug else 'no') + """repr() implementation""" + last_message = 'never' + if self._last_message is not None: + last_message = self._last_message.strftime('%Y-%m-%d %H:%M:%S') + return ( + f'name: {self._name}\nurl: {self._url}\n' + f"enabled: {'yes' if self._enabled else 'no'}\n" + f"channel_list: {', '.join(self._chans)}\n" + f"nick_list: {', '.join(self._nicks)}\n" + f'channel_messages_policy: {self._chan_messages_policy}\n' + f'private_messages_policy: {self._priv_messages_policy}\n' + f'notifications_policy: {self._notifications_policy}\n' + f'last message: {last_message}\n' + f'socket timeout: {self._socket_timeout}\n' + f'ssl-version: {self._ssl_version}\n' + f"ciphers: {self._ssl_ciphers or 'auto'}\n" + "disable hostname check: " + f"{'yes' if self._disable_hostname_check else 'no'}\n" + f"debug: {'yes' if self._debug else 'no'}\n\n" + ) def send_private_message_notification(self, values): """ - sends a private message notification to the represented target + Sends a private message notification to the represented target - values: dict pupulated by the irc msg-handler + :param value: Dict pupulated by the irc msg-handler + :type value: dict """ try: - title_str = self.__fetch_formatted_str(self.__pm_title_template, - values) - message_str = self.__fetch_formatted_str( - self.__pm_message_template, - values) - post_values = {'title': title_str, - 'message': message_str, - 'password': self.__password} - data = urllib.urlencode(post_values) - if not self.__send_beinc_message(data) and self.__debug: + title = self._fetch_formatted_str(self._pm_title_template, values) + message = self._fetch_formatted_str( + self._pm_message_template, values + ) + if not self._send_beinc_message(title, message) and self._debug: beinc_prnt( - 'BEINC DEBUG: send_private_message_notification-ERROR ' - 'for "{0}": __send_beinc_message -> False'.format( - self.__name)) - except Exception as e: - if self.__debug: + f'BEINC DEBUG: send_private_message_notification-ERROR ' + f'for "{self._name}": _send_beinc_message -> False' + ) + except Exception as exp: + if self._debug: beinc_prnt( - 'BEINC DEBUG: send_private_message_notification-ERROR ' - 'for "{0}": {1}'.format(self.__name, e)) + f'BEINC DEBUG: send_private_message_notification-ERROR ' + f'for "{self._name}": {exp}' + ) def send_channel_message_notification(self, values): """ - sends a channel message notification to the represented target + Sends a channel message notification to the represented target - values: dict pupulated by the irc msg-handler + :param value: Dict pupulated by the irc msg-handler + :type value: dict """ try: - title_str = self.__fetch_formatted_str(self.__cm_title_template, - values) - message_str = self.__fetch_formatted_str( - self.__cm_message_template, - values) - post_values = {'title': title_str, - 'message': message_str, - 'password': self.__password} - data = urllib.urlencode(post_values) - if not self.__send_beinc_message(data) and self.__debug: + title = self._fetch_formatted_str(self._cm_title_template, values) + message = self._fetch_formatted_str( + self._cm_message_template, values + ) + if not self._send_beinc_message(title, message) and self._debug: beinc_prnt( - 'BEINC DEBUG: send_channel_message_notification-ERROR ' - 'for "{0}": __send_beinc_message -> False'.format( - self.__name)) - except Exception as e: - if self.__debug: + f'BEINC DEBUG: send_channel_message_notification-ERROR ' + f'for "{self._name}": _send_beinc_message -> False' + ) + except Exception as exp: + if self._debug: beinc_prnt( - 'BEINC DEBUG: send_channel_message_notification-ERROR ' - 'for "{0}": {1}'.format(self.__name, e)) + f'BEINC DEBUG: send_channel_message_notification-ERROR ' + f'for "{self._name}": {exp}' + ) def send_notify_message_notification(self, values): """ - sends a notify message notification to the represented target + Sends a notify message notification to the represented target - values: dict pupulated by the irc msg-handler + :param value: Dict pupulated by the irc msg-handler + :type value: dict """ try: - title_str = self.__fetch_formatted_str(self.__nm_title_template, - values) - message_str = self.__fetch_formatted_str( - self.__nm_message_template, - values) - post_values = {'title': title_str, - 'message': message_str, - 'password': self.__password} - data = urllib.urlencode(post_values) - if not self.__send_beinc_message(data) and self.__debug: + title = self._fetch_formatted_str(self._nm_title_template, values) + message = self._fetch_formatted_str( + self._nm_message_template, values + ) + if not self._send_beinc_message(title, message) and self._debug: beinc_prnt( - 'BEINC DEBUG: send_notify_message_notification-ERROR ' - 'for "{0}": __send_beinc_message -> False'.format( - self.__name)) - except Exception as e: - if self.__debug: + f'BEINC DEBUG: send_notify_message_notification-ERROR ' + f'for "{self._name}": _send_beinc_message -> False' + ) + except Exception as exp: + if self._debug: beinc_prnt( - 'BEINC DEBUG: send_notify_message_notification-ERROR ' - 'for "{0}": {1}'.format(self.__name, e)) + f'BEINC DEBUG: send_notify_message_notification-ERROR ' + f'for "{self._name}": {exp}' + ) def send_broadcast_notification(self, message): """ - sends a 'pure' broadcast / test message notification + Sends a 'pure' broadcast / test message notification to the represented target - message: a single message string + :param message: A single message string + :type message: str """ try: - post_values = {'title': 'BEINC broadcast', - 'message': message, - 'password': self.__password} - data = urllib.urlencode(post_values) - if not self.__send_beinc_message(data) and self.__debug: + title = 'BEINC broadcast' + if not self._send_beinc_message(title, message) and self._debug: beinc_prnt( - 'BEINC DEBUG: send_broadcast_notification-ERROR ' - 'for "{0}": __send_beinc_message -> False'.format( - self.__name)) - except Exception as e: - if self.__debug: + f'BEINC DEBUG: send_broadcast_notification-ERROR ' + f'for "{self._name}": _send_beinc_message -> False' + ) + except Exception as exp: + if self._debug: beinc_prnt( - 'BEINC DEBUG: send_broadcast_notification-ERROR ' - 'for "{0}": {1}'.format(self.__name, e)) + f'BEINC DEBUG: send_broadcast_notification-ERROR ' + f'for "{self._name}": {exp}' + ) + + def _context_setup(self): + """Sets up the SSL context""" + if self._context is not None: + return True + try: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + if self._cert_file: + context.verify_mode = ssl.CERT_REQUIRED + context.load_verify_locations( + cafile=os.path.expanduser(self._cert_file) + ) + context.check_hostname = bool(not self._disable_hostname_check) + else: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + if self._ssl_ciphers and self._ssl_ciphers != 'auto': + context.set_ciphers(self._ssl_ciphers) + self._context = context + return True + except ssl.SSLError as err: + if self._debug: + beinc_prnt(f'BEINC DEBUG: SSL/TLS error: {err}\n') + except Exception as exp: + if self._debug: + beinc_prnt(f'BEINC DEBUG: Generic context error: {exp}\n') + self._context = None + return False - def __fetch_formatted_str(self, template, values): + def _fetch_formatted_str(self, template, values): """ - returns a formatted string by replacing the defined + Returns a formatted string by replacing the defined macros in 'template' the the corresponding values from 'values' - values: dict - template: str - """ - template = unicode(template) - timestamp = datetime.datetime.now().strftime(self.__timestamp_format) - replacements = {u'%S': values['server'].decode('utf-8'), - u'%s': values['source_nick'].decode('utf-8'), - u'%c': values['channel'].decode('utf-8'), - u'%m': values['message'].decode('utf-8'), - u'%t': timestamp.decode('utf-8'), - u'%p': u'BEINC', - u'%n': values['own_nick'].decode('utf-8')} + :param template: The template to use + :type template: str + + :param values: The values dict + :type values: dict + + :return: The formatted string + :rtype: str + """ + timestamp = datetime.datetime.now().strftime(self._timestamp_format) + replacements = { + '%S': values['server'], + '%s': values['source_nick'], + '%c': values['channel'], + '%m': values['message'], + '%t': timestamp, + '%p': 'BEINC', + '%n': values['own_nick'], + } for key, value in replacements.items(): template = template.replace(key, value) - return template.encode('utf-8') + return template - def __send_beinc_message(self, data): + def _send_beinc_message(self, title, message): """ - the function implements the BEINC "protocol" by generating a simple - HTTP request + The method implements the BEINC "protocol" by generating a simple + POST request + + :param title: The title + :type title: str + + :param message: The message + :type message: str + + :return: The status + :rtype: bool """ try: - req = urllib2.Request(self.__url, data) - if self.__cert_file: - global global_beinc_cert_file - global_beinc_cert_file = self.__cert_file - opener = urllib2.build_opener(ValidHTTPSHandler) - response = opener.open(req, timeout=self.__socket_timeout) - else: - response = urllib2.urlopen(req, self.__socket_timeout) - res_code = response.code - response.close() - if res_code == 200: - return True - except urllib2.HTTPError as e: - if self.__debug: + if self._context is None and not self._context_setup(): + return False + response = urllib.request.urlopen( + self._url, + data=urllib.parse.urlencode( + ( + ('resource_name', self._name), + ('password', self._password), + ('title', title), + ('message', message), + ) + ).encode('utf-8'), + timeout=self._socket_timeout, + context=self._context, + ) + response_dict = json.loads(response.read().decode('utf-8')) + if response.code != 200: + raise OSError(response_dict.get('message', '')) + if self._debug: beinc_prnt( - 'BEINC DEBUG: send_beinc_message-ERROR for "{0}": {1} ->' - ' ({2} - {3})'.format(self.__name, - e.url, - e.code, - e.reason)) - # all other exception should be handled by the caller + "BEINC DEBUG: Server responded: " + f"{response_dict.get('message')}" + ) + self._last_message = datetime.datetime.now() + return True + except ssl.SSLError as err: + if self._debug: + beinc_prnt(f'BEINC DEBUG: SSL/TLS error: {err}\n') + except OSError as err: + if self._debug: + beinc_prnt(f'BEINC DEBUG: Connection error: {err}\n') + except Exception as exp: + if self._debug: + beinc_prnt(f'BEINC DEBUG: Unable to send message: {exp}\n') return False def beinc_prnt(message_str): - """ - wrapper around weechat.prnt - """ + """wrapper around weechat.prnt""" if global_values['use_current_buffer']: weechat.prnt(weechat.current_buffer(), message_str) else: @@ -359,9 +377,7 @@ def beinc_prnt(message_str): def beinc_cmd_broadcast_handler(cmd_tokens): - """ - handles: '/beinc broadcast' command actions - """ + """handles: '/beinc broadcast' command actions""" if not cmd_tokens: beinc_prnt('beinc broadcast ') return weechat.WEECHAT_RC_OK @@ -372,13 +388,14 @@ def beinc_cmd_broadcast_handler(cmd_tokens): def beinc_cmd_target_handler(cmd_tokens): - """ - handles: '/beinc target' command actions - """ + """handles: '/beinc target' command actions""" if not cmd_tokens or cmd_tokens[0] not in ['list', 'enable', 'disable']: beinc_prnt('beinc target [ list | enable | disable ]') return weechat.WEECHAT_RC_OK if cmd_tokens[0] == 'list': + beinc_prnt('--- Globals ---') + for key, value in global_values.items(): + beinc_prnt(f'{key} -> {value}') beinc_prnt('--- Targets ---') for target in target_list: beinc_prnt(str(target)) @@ -391,10 +408,10 @@ def beinc_cmd_target_handler(cmd_tokens): for target in target_list: if target.name == name: target.enabled = True - beinc_prnt('target "{0}" enabled'.format(name)) + beinc_prnt(f'target "{name}" enabled') break else: - beinc_prnt('no matching target for "{0}"'.format(name)) + beinc_prnt(f'no matching target for "{name}"') elif cmd_tokens[0] == 'disable': if not cmd_tokens[1:]: beinc_prnt('missing a name-argument') @@ -403,17 +420,15 @@ def beinc_cmd_target_handler(cmd_tokens): for target in target_list: if target.name == name: target.enabled = False - beinc_prnt('target "{0}" disabled'.format(name)) + beinc_prnt(f'target "{name}" disabled') break else: - beinc_prnt('no matching target for "{0}"'.format(name)) + beinc_prnt(f'no matching target for "{name}"') return weechat.WEECHAT_RC_OK def beinc_command(data, buffer_obj, args): - """ - Callback function handling the Weechat's /beinc command - """ + """Callback function handling the Weechat's /beinc command""" global enabled cmd_tokens = args.split() if not cmd_tokens: @@ -432,91 +447,69 @@ def beinc_command(data, buffer_obj, args): elif cmd_tokens[0] == 'target': return beinc_cmd_target_handler(cmd_tokens[1:]) else: - beinc_prnt('syntax: /beinc < on | off | reload |' - ' broadcast | target >') + beinc_prnt( + 'syntax: /beinc < on | off | reload |' + ' broadcast | target >' + ) return weechat.WEECHAT_RC_OK def beinc_privmsg_handler(data, signal, signal_data): - """ - Callback function the *PRIVMSG* IRC messages hooked by Weechat - """ + """Callback function the *PRIVMSG* IRC messages hooked by Weechat""" if not enabled: return weechat.WEECHAT_RC_OK - prvmsg_dict = weechat.info_get_hashtable('irc_message_parse', - {'message': signal_data}) + prvmsg_dict = weechat.info_get_hashtable( + 'irc_message_parse', {'message': signal_data} + ) # packing the privmsg handler values - ph_values = dict() + ph_values = {} ph_values['server'] = signal.split(',')[0] ph_values['own_nick'] = weechat.info_get('irc_nick', ph_values['server']) ph_values['channel'] = prvmsg_dict['arguments'].split(':')[0].strip() ph_values['source_nick'] = prvmsg_dict['nick'] ph_values['message'] = ':'.join( - prvmsg_dict['arguments'].split(':')[1:]).strip() + prvmsg_dict['arguments'].split(':')[1:] + ).strip() if ph_values['channel'] == ph_values['own_nick']: # priv messages are handled here if not global_values['global_private_messages_policy']: return weechat.WEECHAT_RC_OK - p_messages_policy = global_values['global_private_messages_policy'] - if p_messages_policy == BEINC_POLICY_LIST_ONLY and \ - '{0}.{1}'.format( - ph_values['server'], - ph_values['source_nick'].lower() - ) not in global_values['global_nicks']: - return weechat.WEECHAT_RC_OK for target in target_list: if not target.enabled: continue p_messages_policy = target.private_messages_policy if p_messages_policy == BEINC_POLICY_ALL or ( - p_messages_policy == BEINC_POLICY_LIST_ONLY and - '{0}.{1}'.format( - ph_values['server'], - ph_values['source_nick'].lower()) in target.nicks): + p_messages_policy == BEINC_POLICY_LIST_ONLY + and f"{ph_values['server']}.{ph_values['source_nick'].lower()}" + in target.nicks + ): target.send_private_message_notification(ph_values) elif ph_values['own_nick'].lower() in ph_values['message'].lower(): # notify messages are handled here if not global_values['global_notifications_policy']: return weechat.WEECHAT_RC_OK - notifications_policy = global_values['global_notifications_policy'] - if notifications_policy == BEINC_POLICY_LIST_ONLY and ( - '{0}.{1}'.format( - ph_values['server'], - ph_values['channel'].lower() - ) not in global_values['global_chans'] - ): - return weechat.WEECHAT_RC_OK for target in target_list: if not target.enabled: continue if target.notifications_policy == BEINC_POLICY_ALL or ( - target.notifications_policy == BEINC_POLICY_LIST_ONLY and - '{0}.{1}'.format( - ph_values['server'], - ph_values['channel'].lower()) in target.chans + target.notifications_policy == BEINC_POLICY_LIST_ONLY + and f"{ph_values['server']}.{ph_values['channel'].lower()}" + in target.chans ): target.send_notify_message_notification(ph_values) elif global_values['global_channel_messages_policy']: # chan messages are handled here if not global_values['global_notifications_policy']: return weechat.WEECHAT_RC_OK - c_messages_policy = global_values['global_channel_messages_policy'] - if c_messages_policy == BEINC_POLICY_LIST_ONLY and ( - '{0}.{1}'.format( - ph_values['server'], - ph_values['channel'].lower() - ) not in global_values['global_chans'] - ): - return weechat.WEECHAT_RC_OK for target in target_list: if not target.enabled: continue c_messages_policy = target.channel_messages_policy if c_messages_policy == BEINC_POLICY_ALL or ( - c_messages_policy == BEINC_POLICY_LIST_ONLY and - '{0}.{1}'.format( - ph_values['server'], - ph_values['channel'].lower()) in target.chans): + c_messages_policy == BEINC_POLICY_LIST_ONLY + and f"{ph_values['server']}.{ph_values['channel'].lower()}" + in target.chans + ): target.send_channel_message_notification(ph_values) return weechat.WEECHAT_RC_OK @@ -533,10 +526,8 @@ def beinc_init(): global global_values # global chans/nicks sets are used to speed up the filtering - global_values = dict() - global_values['global_chans'] = set() - global_values['global_nicks'] = set() - target_list = list() + global_values = {} + target_list = [] custom_error = '' global_values['global_channel_messages_policy'] = False global_values['global_private_messages_policy'] = False @@ -545,33 +536,34 @@ def beinc_init(): try: beinc_config_file_str = os.path.join( - weechat.info_get('weechat_dir', ''), - 'beinc_weechat.json') - beinc_prnt('Parsing {0}...'.format(beinc_config_file_str)) + weechat.info_get('weechat_dir', ''), 'beinc_weechat.json' + ) + beinc_prnt(f'Parsing {beinc_config_file_str}...') custom_error = 'load error' - with open(beinc_config_file_str, 'r') as fp: - config_dict = json.load(fp, encoding='utf-8') + with io.open(beinc_config_file_str, 'r', encoding='utf-8') as fp: + config_dict = json.load(fp) custom_error = 'target parse error' global_values['use_current_buffer'] = bool( - config_dict['irc_client'].get( - 'use_current_buffer', False)) - if config_dict.get('config_version', - 0) != BEINC_CURRENT_CONFIG_VERSION: - beinc_prnt('WARNING: The version of the config-file: {0} ({1}) ' - 'does not correspond to the latest version supported ' - 'by this program ({2})\nCheck beinc_config_sample.json ' - 'for the newest features!'.format( - beinc_config_file_str, - config_dict.get('config_version', 0), - BEINC_CURRENT_CONFIG_VERSION)) + config_dict['irc_client'].get('use_current_buffer', False) + ) + if ( + config_dict.get('config_version', 0) + != BEINC_CURRENT_CONFIG_VERSION + ): + beinc_prnt( + "WARNING: The version of the config-file: " + f"{beinc_config_file_str} " + f"({config_dict.get('config_version', 0)}) " + "does not correspond to the latest version supported " + f"by this program ({BEINC_CURRENT_CONFIG_VERSION})\n" + "Check beinc_config_sample.json for the newest features!" + ) for target in config_dict['irc_client']['targets']: try: new_target = WeechatTarget(target) - except Exception as e: - beinc_prnt('Unable to add target: {0}'.format(e)) + except Exception as exp: + beinc_prnt(f'Unable to add target: {exp}') continue - global_values['global_chans'].update(new_target.chans) - global_values['global_nicks'].update(new_target.nicks) if new_target.channel_messages_policy: global_values['global_channel_messages_policy'] = True if new_target.private_messages_policy: @@ -579,12 +571,13 @@ def beinc_init(): if new_target.notifications_policy: global_values['global_notifications_policy'] = True target_list.append(new_target) - beinc_prnt('BEINC target "{0}" added'.format(new_target.name)) + beinc_prnt(f'BEINC target "{new_target.name}" added') beinc_prnt('Done!') - except Exception as e: - beinc_prnt('ERROR: unable to parse {0}: {1} - {2}\n' - 'BEINC is now disabled'.format( - beinc_config_file_str, custom_error, e)) + except Exception as exp: + beinc_prnt( + f'ERROR: unable to parse {beinc_config_file_str}: ' + f'{custom_error} - {exp}\nBEINC is now disabled' + ) enabled = False # do not return error / exit the script # in order to give a smoother opportunity to fix a 'broken' config @@ -594,22 +587,29 @@ def beinc_init(): weechat.register( 'beinc_weechat', - 'Simeon Simeonov', - '1.1', - 'GPL3', - 'Blackmore\'s Extended IRC Notification Collection (Weechat Client)', + __author__, + __version__, + __license__, + "Blackmore's Extended IRC Notification Collection (Weechat Client)", + '', '', - '') +) version = weechat.info_get('version_number', '') or 0 if int(version) < 0x00040000: weechat.prnt('', 'WeeChat version >= 0.4.0 is required to run beinc') else: - weechat.hook_command('beinc', - 'beinc on off toggle', '', - 'description...', - 'None', - 'beinc_command', - '') + weechat.hook_command( + 'beinc', + 'BEINC command', + '< broadcast | on | off | reload | target >', + ( + 'Available target actions:\n' + 'disable \nenable \nlist' + ), + 'None', + 'beinc_command', + '', + ) weechat.hook_signal('*,irc_in2_privmsg', 'beinc_privmsg_handler', '') beinc_init() weechat.prnt('', 'beinc initiated!') diff --git a/python/bitlbee_completion.py b/python/bitlbee_completion.py index be6f80e2..84230620 100644 --- a/python/bitlbee_completion.py +++ b/python/bitlbee_completion.py @@ -4,6 +4,8 @@ # # History: # +# 2023-10-04, Andrea Beciani : +# version 0.3: set default template, fix command executed, fix function names # 2015-11-02, Mickaël Thomas : # version 0.2: strip color attributes for topic detection # 2015-03-22, Roger Duran : @@ -13,9 +15,10 @@ SCRIPT_NAME = "bitlbee_completion" SCRIPT_AUTHOR = "Roger Duran " -SCRIPT_VERSION = "0.2" +SCRIPT_VERSION = "0.3" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Add tab completion to bitlbee commands" +TEMPLATE_NAME = "bitlbee_completion" OPTS = { "server": None, @@ -33,7 +36,7 @@ def request_completion(): Request the completion to the bitlbee server and wait for response """ server = OPTS["server"] - weechat.command(server, "/quote -server %s COMPLETIONS" % server) + weechat.command("", "/quote -server %s COMPLETIONS" % server) def modifier_cb(data, modifier, modifier_data, string): @@ -49,7 +52,7 @@ def modifier_cb(data, modifier, modifier_data, string): return "" -def bitlbee_completion(data, completion_item, buffer, completion): +def completion_cb(data, completion_item, buffer, completion): """ Complete bitlbee commands only in the bitlbee buffer """ @@ -66,6 +69,15 @@ def bitlbee_completion(data, completion_item, buffer, completion): weechat.WEECHAT_LIST_POS_SORT) return weechat.WEECHAT_RC_OK +def check_config(): + option = weechat.config_get("weechat.completion.default_template") + default_template = weechat.config_string(option) + if TEMPLATE_NAME not in default_template: + rc = weechat.config_option_set(option, default_template + "|%(" + TEMPLATE_NAME + ")", 1) + if rc == weechat.WEECHAT_CONFIG_OPTION_SET_OK_SAME_VALUE: + weechat.prnt("", SCRIPT_NAME + " - warning! - weechat.completion.default_template same value") + elif rc == weechat.WEECHAT_CONFIG_OPTION_SET_ERROR: + weechat.prnt("", SCRIPT_NAME + " - error! - writing weechat.completion.default_template option") def find_buffer(): """ @@ -88,11 +100,12 @@ def set_options(name): OPTS["channel"] = channel -def print_332(data, buffer, time, tags, displayed, highlight, prefix, message): +def print_cb(data, buffer, time, tags, displayed, highlight, prefix, message): """ Find the buffer when a new one is open """ - if weechat.string_remove_color(message, "") == TOPIC: + current_topic = weechat.string_remove_color(message, "").split('"')[1] + if current_topic == TOPIC: name = weechat.buffer_get_string(buffer, "name") set_options(name) request_completion() @@ -100,12 +113,13 @@ def print_332(data, buffer, time, tags, displayed, highlight, prefix, message): def main(): + check_config() weechat.hook_modifier("irc_in_notice", "modifier_cb", "") - weechat.hook_completion("bitlbee", "bitlbee completion", - "bitlbee_completion", "") + weechat.hook_completion(TEMPLATE_NAME, "TAB completion to bitlbee", + "completion_cb", "") - weechat.hook_print('', 'irc_332', '', 1, 'print_332', '') - weechat.hook_print('', 'irc_topic', '', 1, 'print_332', '') + weechat.hook_print('', 'irc_332', '', 1, 'print_cb', '') + weechat.hook_print('', 'irc_topic', '', 1, 'print_cb', '') find_buffer() if __name__ == "__main__": diff --git a/python/bitlbee_typing_notice.py b/python/bitlbee_typing_notice.py index b2a1cc00..c69ffd08 100644 --- a/python/bitlbee_typing_notice.py +++ b/python/bitlbee_typing_notice.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (c) 2010 by Alexander Schremmer -# Copyright (c) 2013 by Corey Halpin +# Copyright (c) 2013,2020 by Corey Halpin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,6 +21,12 @@ # (this script requires WeeChat 0.3.6 or newer) # # History: +# 2020-05-10, Sébastien Helleu +# version 0.7: remove useless list() around .items() +# 2020-04-15, Corey Halpin +# version 0.6: +# * Update for python3, fixing issues pointed out by 2to3. +# * Fix complaints from pycodestyle. # 2014-02-15, Corey Halpin # version 0.5: # * Improve documentation for the 'server' setting @@ -34,8 +40,8 @@ # * Send typing = 0 at message completion in private buffers # * Make server, channel, and timeout configurable w/o editing plugin code. # 2010-05-20, Alexander Schremmer -# version 0.2: also handle users that do not send a TYPING 0 msg before quitting -# removed InfoList code +# version 0.2: also handle users that do not send a TYPING 0 msg before +# quitting removed InfoList code # 2010-05-16, Alexander Schremmer # version 0.1: initial release @@ -63,29 +69,36 @@ import weechat as w import re -SCRIPT_NAME = "bitlbee_typing_notice" -SCRIPT_AUTHOR = "Alexander Schremmer " -SCRIPT_VERSION = "0.5" +SCRIPT_NAME = "bitlbee_typing_notice" +SCRIPT_AUTHOR = "Alexander Schremmer " +SCRIPT_VERSION = "0.7" SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Shows when somebody is typing on bitlbee and sends the notice as well" +SCRIPT_DESC = "Sends and displays bitlbee typing notices." -typing = {} # Record nicks who sent typing notices. key = subject nick, val = typing level. -sending_typing = {} # Nicks to whom we're sending typing notices. -# key = target nick, val = sequence number used to determine when the typing notice -# should be removed. +# Record nicks who sent typing notices. +# key = subject nick, val = typing level. +typing = {} + + +# Nicks to whom we're sending typing notices. +# key = target nick, val = sequence number used to determine when the typing +# notice should be removed. +sending_typing = {} + def channel_has_nick(server, channel, nick): buffer = w.buffer_search("", "%s.%s" % (server, channel)) return bool(w.nicklist_search_nick(buffer, "", nick)) + # Callback which checks for ctcp typing notices sent to us. # Updates typing data, hides the ctcp notices. def ctcp_cb(data, modifier, modifier_data, string): if modifier_data != w.config_get_plugin("server"): return string msg_hash = w.info_get_hashtable( - "irc_message_parse", {"message": string} ) + "irc_message_parse", {"message": string}) if msg_hash["command"] != "PRIVMSG": return string match = re.search('\001TYPING ([0-9])\001', msg_hash["arguments"]) @@ -104,12 +117,13 @@ def ctcp_cb(data, modifier, modifier_data, string): def stop_typing(data, signal, signal_data): msg_hash = w.info_get_hashtable( - "irc_message_parse", {"message": signal_data } ) + "irc_message_parse", {"message": signal_data}) if msg_hash["nick"] in typing: del typing[msg_hash["nick"]] w.bar_item_update("bitlbee_typing_notice") return w.WEECHAT_RC_OK + def typed_char(data, signal, signal_data): buffer = w.current_buffer() input_s = w.buffer_get_string(buffer, 'input') @@ -120,8 +134,8 @@ def typed_char(data, signal, signal_data): if server != w.config_get_plugin("server") or input_s.startswith("/"): return w.WEECHAT_RC_OK if buffer_type == "private": - if len(input_s)==0: - send_typing(channel, 0) # Line sent or deleted -- no longer typing + if len(input_s) == 0: + send_typing(channel, 0) # Line sent or deleted -- no longer typing else: send_typing(channel, 1) elif channel == w.config_get_plugin("channel"): @@ -136,7 +150,7 @@ def typed_char(data, signal, signal_data): def typing_disable_timer(data, remaining_calls): nick, cookie = data.rsplit(":", 1) cookie = int(cookie) - if nick in sending_typing and sending_typing[nick]==cookie: + if nick in sending_typing and sending_typing[nick] == cookie: send_typing_ctcp(nick, 0) del sending_typing[nick] return w.WEECHAT_RC_OK @@ -146,9 +160,11 @@ def send_typing_ctcp(nick, level): if not channel_has_nick(w.config_get_plugin("server"), w.config_get_plugin("channel"), nick): return - buffer = w.buffer_search("irc", "%s.%s" % - (w.config_get_plugin("server"), - w.config_get_plugin("channel")) ) + + server = w.config_get_plugin("server") + channel = w.config_get_plugin("channel") + buffer = w.buffer_search( + "irc", "%s.%s" % (server, channel)) w.command(buffer, "/mute -all /ctcp %s TYPING %i" % (nick, level)) @@ -156,13 +172,14 @@ def send_typing(nick, level): if level == 0 and nick in sending_typing: send_typing_ctcp(nick, 0) del sending_typing[nick] - elif level > 0 : + elif level > 0: if nick not in sending_typing: send_typing_ctcp(nick, level) cookie = sending_typing.get(nick, 0) + 1 sending_typing[nick] = cookie - w.hook_timer( int(1000 * float(w.config_get_plugin('timeout'))), 0, 1, - "typing_disable_timer", "%s:%i" % (nick, cookie)) + timeout = int(1000 * float(w.config_get_plugin('timeout'))) + w.hook_timer( + timeout, 0, 1, "typing_disable_timer", "%s:%i" % (nick, cookie)) def typing_notice_item_cb(data, buffer, args): @@ -180,7 +197,7 @@ def typing_notice_item_cb(data, buffer, args): # Main if __name__ == "__main__": if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, "", ""): + SCRIPT_DESC, "", ""): if not w.config_get_plugin('channel'): w.config_set_plugin('channel', "&bitlbee") @@ -189,8 +206,10 @@ def typing_notice_item_cb(data, buffer, args): if not w.config_get_plugin('timeout'): w.config_set_plugin('timeout', "4") + server = w.config_get_plugin("server") + w.hook_signal("input_text_changed", "typed_char", "") - w.hook_signal(w.config_get_plugin("server")+",irc_in_quit", "stop_typing", "") - w.hook_signal(w.config_get_plugin("server")+",irc_in_privmsg", "stop_typing", "") + w.hook_signal(server+",irc_in_quit", "stop_typing", "") + w.hook_signal(server+",irc_in_privmsg", "stop_typing", "") w.bar_item_new('bitlbee_typing_notice', 'typing_notice_item_cb', '') w.hook_modifier("irc_in_privmsg", "ctcp_cb", "") diff --git a/python/btc_ticker.py b/python/btc_ticker.py deleted file mode 100644 index aae8279c..00000000 --- a/python/btc_ticker.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 - -# Copyright (c) 2014, Eugene Ciurana (pr3d4t0r) -# All rights reserved. -# -# Version 1.1.1 -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, this -# list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# * Neither the name of the {organization} nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# Main repository, version history: https://github.com/pr3d4t0r/weechat-btc-ticker - - -from time import gmtime, strftime - -import json - -import weechat - - -# *** Symbolic constants *** - -BTCE_API_TIME_OUT = 15000 # ms -BTCE_API_URI = u'url:https://btc-e.com/api/2/%s_%s/ticker' - -DEFAULT_CRYPTO_CURRENCY = u'btc' -DEFAULT_FIAT_CURRENCY = u'usd' - -VALID_CRYPTO_CURRENCIES = [ DEFAULT_CRYPTO_CURRENCY, u'ltc' ] -VALID_FIAT_CURRENCIES = [ DEFAULT_FIAT_CURRENCY, u'eur', u'rur' ] - -COMMAND_NICK = u'tick' - - -# *** Functions *** - -def extractRelevantInfoFrom(rawTicker): - payload = json.loads(rawTicker) - result = dict() - - result[u'avg'] = payload[u'ticker'][u'avg'] - result[u'buy'] = payload[u'ticker'][u'buy'] - result[u'high'] = payload[u'ticker'][u'high'] - result[u'last'] = payload[u'ticker'][u'last'] - result[u'low'] = payload[u'ticker'][u'low'] - result[u'sell'] = payload[u'ticker'][u'sell'] - result[u'updated'] = unicode(payload[u'ticker'][u'updated']) - - result[u'time'] = strftime(u'%Y-%b-%d %H:%M:%S Z', gmtime(payload[u'ticker'][u'updated'])) - - return result - - -def display(buffer, ticker, currencyLabel, fiatCurrencyLabel): - output = (u'%s:%s sell = %4.2f, buy = %4.2f, last = %4.2f; high = %4.2f, low = %4.2f, avg = %4.2f || via BTC-e on %s' % \ - (currencyLabel, fiatCurrencyLabel, \ - ticker[u'sell'], ticker[u'buy'], ticker[u'last'], \ - ticker[u'high'], ticker[u'low'], ticker[u'avg'], \ - ticker[u'time'])) - - weechat.command(buffer, u'/say %s' % output) - - -def displayCurrentTicker(buffer, rawTicker, cryptoCurrency, fiatCurrency): - if rawTicker is not None: - ticker = extractRelevantInfoFrom(rawTicker) - display(buffer, ticker, cryptoCurrency.upper(), fiatCurrency.upper()) - else: - weechat.prnt(buffer, u'%s\t*** UNABLE TO READ DATA FROM: %s ***' % (COMMAND_NICK, serviceURI)) - - -def tickerPayloadHandler(tickerData, service, returnCode, out, err): - if returnCode == weechat.WEECHAT_HOOK_PROCESS_ERROR: - weechat.prnt(u"", u"%s\tError with service call '%s'" % (COMMAND_NICK, service)) - return weechat.WEECHAT_RC_OK - - tickerInfo = tickerData.split(u' ') - displayCurrentTicker('', out, tickerInfo[0], tickerInfo[1]) - - return weechat.WEECHAT_RC_OK - - -def fetchJSONTickerFor(cryptoCurrency, fiatCurrency): - serviceURI = BTCE_API_URI % (cryptoCurrency, fiatCurrency) - tickerData = cryptoCurrency+u' '+fiatCurrency - - weechat.hook_process(serviceURI, BTCE_API_TIME_OUT, u'tickerPayloadHandler', tickerData) - - -def displayCryptoCurrencyTicker(data, buffer, arguments): - cryptoCurrency = DEFAULT_CRYPTO_CURRENCY - fiatCurrency = DEFAULT_FIAT_CURRENCY - - if len(arguments) > 0: - tickerArguments = arguments.split(u' ') # no argparse module; these aren't CLI, but WeeChat's arguments - - if len(tickerArguments) >= 1: - if tickerArguments[0].lower() in VALID_CRYPTO_CURRENCIES: - cryptoCurrency = tickerArguments[0].lower() - else: - weechat.prnt(buffer, u'%s\tInvalid crypto currency; using default %s' % (COMMAND_NICK, DEFAULT_CRYPTO_CURRENCY)) - - if len(tickerArguments) == 2: - if tickerArguments[1].lower() in VALID_FIAT_CURRENCIES: - fiatCurrency = tickerArguments[1].lower() - else: - weechat.prnt(buffer, u'%s\tInvalid fiat currency; using default %s' % (COMMAND_NICK, DEFAULT_FIAT_CURRENCY)) - - fetchJSONTickerFor(cryptoCurrency, fiatCurrency) - - return weechat.WEECHAT_RC_OK - - -# *** main *** - -weechat.register(u'btc_ticker', u'pr3d4t0r', u'1.1.1', u'BSD', u'Display a crypto currency spot price ticker (BTC, LTC) in the active buffer', u'', u'UTF-8') - -weechat.hook_command(COMMAND_NICK, u'Display Bitcoin or other crypto currency spot exchange value in a fiat currency like USD or EUR',\ - u'[btc|ltc|nmc [usd|eur|rur] ]', u' btc = Bitcoin\n ltc = Litecoin\n nmc = Namecoin\n usd = US dollar\n eur = euro\n rur = Russian ruble', u'', u'displayCryptoCurrencyTicker', u'') diff --git a/python/buffer_autoclose.py b/python/buffer_autoclose.py index 303f5b37..a47fae1e 100644 --- a/python/buffer_autoclose.py +++ b/python/buffer_autoclose.py @@ -20,6 +20,11 @@ # (this script requires WeeChat 0.3.0 or newer) # # History: +# 2024-05-04, Miklos Vajna +# version 0.6: Allow autoclosing explicitly listed non-private buffers as well +# 2018-04-10, Sébastien Helleu +# version 0.5: fix infolist_time for WeeChat >= 2.2 (WeeChat returns a long +# integer instead of a string) # 2016-02-05, ixti # version 0.4: Add Python3 support # 2009-12-15, xt @@ -34,7 +39,7 @@ SCRIPT_NAME = "buffer_autoclose" SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "0.4" +SCRIPT_VERSION = "0.6" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Automatically close inactive private message buffers" @@ -42,6 +47,7 @@ 'interval': '1', # How often in minutes to check 'age_limit': '30', # How old in minutes before auto close 'ignore': '', # Buffers to ignore (use full name: server.buffer_name) + 'prefer': '', # Buffers to prefer, even if they are not private (use full name: server.buffer_name) } if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, @@ -61,8 +67,13 @@ def get_all_buffers(): '''Returns list with pointers of all open buffers.''' buffers = [] infolist = w.infolist_get('buffer', '', '') + preferlist = w.config_get_plugin('prefer').split(',') while w.infolist_next(infolist): buffer_type = w.buffer_get_string(w.infolist_pointer(infolist, 'pointer'), 'localvar_type') + name = w.buffer_get_string(w.infolist_pointer(infolist, 'pointer'), 'name') + if name in preferlist: + buffers.append(w.infolist_pointer(infolist, 'pointer')) + continue if buffer_type == 'private': # we only close private message buffers for now buffers.append(w.infolist_pointer(infolist, 'pointer')) w.infolist_free(infolist) @@ -73,6 +84,10 @@ def get_last_line_date(buffer): infolist = w.infolist_get('buffer_lines', buffer, '') while w.infolist_prev(infolist): date = w.infolist_time(infolist, 'date') + # since WeeChat 2.2, infolist_time returns a long integer instead of + # a string + if not isinstance(date, str): + date = time.strftime('%F %T', time.localtime(int(date))) if date != '1970-01-01 01:00:00': # Some lines like "Day changed to" message doesn't have date # set so loop until we find a message that does diff --git a/python/buffer_autohide.py b/python/buffer_autohide.py new file mode 100644 index 00000000..141addda --- /dev/null +++ b/python/buffer_autohide.py @@ -0,0 +1,430 @@ +# -*- coding: utf-8 -*- +# MIT License +# +# Copyright (c) 2017-2019 Matthias Adamczyk +# Copyright (c) 2019 Marco Trevisan +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +""" +Automatically hide read buffers and unhide them on new activity. + +Requires WeeChat version 1.0 or higher. + +Configuration: + plugins.var.python.buffer_autohide.hide_inactive: Hide inactive buffers (default: "off") + plugins.var.python.buffer_autohide.hide_private: Hide private buffers (default: "off") + plugins.var.python.buffer_autohide.unhide_low: Unhide a buffer when a low priority message (like JOIN, + PART, etc.) has been received (default: "off"), + plugins.var.python.buffer_autohide.exemptions: An enumeration of buffers that should not become hidden (default: "") + plugins.var.python.buffer_autohide.keep_open: Keep a buffer open for a short amount of time (default: "off") + plugins.var.python.buffer_autohide.keep_open_timeout: Timeout in milliseconds for how long a selected buffer should be kept around (default: "60 * 1000") + +History: +2017-03-19: Matthias Adamczyk + version 0.1: Initial release +2018-06-28: yeled + version 0.2: Only skip irc.servers +2018-12-07: Matthias Adamczyk + version 0.3: Add a functionality to define exemptions for certain buffers +2018-12-07: Marco Trevisan + version 0.4: Keep buffers active for a given time before hide them again if they should +2019-01-31: Trygve Aaberge + version 0.5: Support buffers from plugins other than IRC as well + +https://github.com/notmatti/buffer_autohide +""" +from __future__ import print_function +import ast +import operator as op +import_ok = True +try: + import weechat + from weechat import WEECHAT_RC_OK +except ImportError: + print("Script must be run under weechat. https://weechat.org") + import_ok = False + + +SCRIPT_NAME = "buffer_autohide" +SCRIPT_AUTHOR = "Matthias Adamczyk " +SCRIPT_VERSION = "0.5" +SCRIPT_LICENSE = "MIT" +SCRIPT_DESC = "Automatically hide read buffers and unhide them on new activity" +SCRIPT_COMMAND = SCRIPT_NAME + +DELIMITER = "|@|" +MINIMUM_BUFFER_LIFE = 500 # How many ms are enough to consider a buffer valid +KEEP_ALIVE_TIMEOUT = 60 * 1000 # How long a selected buffer should be kept around + +CURRENT_BUFFER = "0x0" # pointer string representation +CURRENT_BUFFER_TIMER_HOOK = None # Timeout hook reference +KEEP_ALIVE_BUFFERS = {} # {pointer_string_rep: timeout_hook} + + +def config_init(): + """Add configuration options to weechat.""" + global KEEP_ALIVE_TIMEOUT + + config = { + "hide_inactive": ("off", "Hide inactive buffers"), + "hide_private": ("off", "Hide private buffers"), + "unhide_low": ("off", + "Unhide a buffer when a low priority message (like JOIN, PART, etc.) has been received"), + "exemptions": ("", "An enumeration of buffers that should not get hidden"), + "keep_open": ("off", "Keep a buffer open for a short amount of time"), + "keep_open_timeout": ("60 * 1000", "Timeout in milliseconds for how long a selected buffer should be kept around"), + } + for option, default_value in config.items(): + if weechat.config_get_plugin(option) == "": + weechat.config_set_plugin(option, default_value[0]) + weechat.config_set_desc_plugin( + option, '{} (default: "{}")'.format(default_value[1], default_value[0])) + + weechat.hook_config("plugins.var.python.buffer_autohide.keep_open_timeout", "timeout_config_changed_cb", "") + if weechat.config_is_set_plugin("keep_open_timeout"): + KEEP_ALIVE_TIMEOUT = eval_expr(weechat.config_get_plugin("keep_open_timeout")) + + +def eval_expr(expr): + """Evaluate a mathematical expression. + + >>> eval_expr('2 * 6') + 12 + """ + def evaluate(node): + # supported operators + operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, + ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor, + ast.USub: op.neg} + if isinstance(node, ast.Num): # + return node.n + elif isinstance(node, ast.BinOp): # + return operators[type(node.op)](evaluate(node.left), evaluate(node.right)) + elif isinstance(node, ast.UnaryOp): # e.g., -1 + return operators[type(node.op)](evaluate(node.operand)) + else: + raise TypeError(node) + + return evaluate(ast.parse(expr, mode='eval').body) + + +def timeout_config_changed_cb(data, option, value): + """Set the new keep_alive timeout upon change of the corresponding value in plugins.conf.""" + global KEEP_ALIVE_TIMEOUT + KEEP_ALIVE_TIMEOUT = eval_expr(value) + return WEECHAT_RC_OK + + +def hotlist_dict(): + """Return the contents of the hotlist as a dictionary. + + The returned dictionary has the following structure: + >>> hotlist = { + ... "0x0": { # string representation of the buffer pointer + ... "count_low": 0, + ... "count_message": 0, + ... "count_private": 0, + ... "count_highlight": 0, + ... } + ... } + """ + hotlist = {} + infolist = weechat.infolist_get("hotlist", "", "") + while weechat.infolist_next(infolist): + buffer_pointer = weechat.infolist_pointer(infolist, "buffer_pointer") + hotlist[buffer_pointer] = {} + hotlist[buffer_pointer]["count_low"] = weechat.infolist_integer( + infolist, "count_00") + hotlist[buffer_pointer]["count_message"] = weechat.infolist_integer( + infolist, "count_01") + hotlist[buffer_pointer]["count_private"] = weechat.infolist_integer( + infolist, "count_02") + hotlist[buffer_pointer]["count_highlight"] = weechat.infolist_integer( + infolist, "count_03") + weechat.infolist_free(infolist) + return hotlist + + +def on_temporary_active_buffer_timeout(buffer, remaining_calls): + remove_keep_alive(buffer) + maybe_hide_buffer(buffer) + + return weechat.WEECHAT_RC_OK + + +def keep_alive_buffer(buffer): + remove_keep_alive(buffer) + + if buffer_is_hidable(buffer): + KEEP_ALIVE_BUFFERS[buffer] = weechat.hook_timer(KEEP_ALIVE_TIMEOUT, 0, 1, + "on_temporary_active_buffer_timeout", buffer) + + +def remove_keep_alive(buffer): + global KEEP_ALIVE_BUFFERS + + if buffer in KEEP_ALIVE_BUFFERS.keys(): + weechat.unhook(KEEP_ALIVE_BUFFERS.pop(buffer)) + + +def switch_current_buffer(): + """Save current buffer and ensure that it's visible, then if the + buffer is elegible to be hidden, we add it to the list of the buffers + to be hidden after a delay + """ + global CURRENT_BUFFER + global CURRENT_BUFFER_TIMER_HOOK + + previous_buffer = CURRENT_BUFFER + CURRENT_BUFFER = weechat.current_buffer() + + if previous_buffer == CURRENT_BUFFER: + return + + if weechat.buffer_get_integer(CURRENT_BUFFER, "hidden") == 1: + weechat.buffer_set(CURRENT_BUFFER, "hidden", "0") + + if weechat.config_get_plugin("keep_open") != "off": + if CURRENT_BUFFER_TIMER_HOOK is not None: + weechat.unhook(CURRENT_BUFFER_TIMER_HOOK) + CURRENT_BUFFER_TIMER_HOOK = None + maybe_hide_buffer(previous_buffer) + else: + keep_alive_buffer(previous_buffer) + + CURRENT_BUFFER_TIMER_HOOK = weechat.hook_timer(MINIMUM_BUFFER_LIFE, 0, 1, + "on_current_buffer_is_still_active_timeout", "") + else: + maybe_hide_buffer(previous_buffer) + + +def on_current_buffer_is_still_active_timeout(pointer, remaining_calls): + global CURRENT_BUFFER_TIMER_HOOK + global KEEP_ALIVE_BUFFERS + + CURRENT_BUFFER_TIMER_HOOK = None + remove_keep_alive(CURRENT_BUFFER) + + return weechat.WEECHAT_RC_OK + + +def switch_buffer_cb(data, signal, signal_data): + """ + :param data: Pointer + :param signal: Signal sent by Weechat + :param signal_data: Data sent with signal + :returns: callback return value expected by Weechat. + """ + switch_current_buffer() + return WEECHAT_RC_OK + + +def buffer_is_hidable(buffer): + """Check if passed buffer can be hidden. + + If configuration option ``hide_private`` is enabled, + private buffers will become hidden as well. + + If the previous buffer name matches any of the exemptions defined in ``exemptions``, + it will not become hidden. + + :param buffer: Buffer string representation + """ + if buffer == weechat.current_buffer(): + return False + + if buffer in KEEP_ALIVE_BUFFERS.keys(): + return False + + full_name = weechat.buffer_get_string(buffer, "full_name") + + if full_name.startswith("irc.server"): + return False + + buffer_type = weechat.buffer_get_string(buffer, 'localvar_type') + + if (buffer_type == "private" + and weechat.config_get_plugin("hide_private") == "off"): + return False + + if weechat.config_get_plugin("hide_inactive") == "off": + nicks_count = weechat.buffer_get_integer(buffer, 'nicklist_nicks_count') + if nicks_count == 0: + return False + + for entry in list_exemptions(): + if entry in full_name: + return False + + return True + + +def maybe_hide_buffer(buffer): + """Hide a buffer if all the conditions are met""" + if buffer_is_hidable(buffer): + weechat.buffer_set(buffer, "hidden", "1") + + +def unhide_buffer_cb(data, signal, signal_data): + """Unhide a buffer on new activity. + + This callback unhides a buffer in which a new message has been received. + If configuration option ``unhide_low`` is enabled, + buffers with only low priority messages (like JOIN, PART, etc.) will be unhidden as well. + + :param data: Pointer + :param signal: Signal sent by Weechat + :param signal_data: Data sent with signal + :returns: Callback return value expected by Weechat. + """ + hotlist = hotlist_dict() + line_data = weechat.hdata_pointer(weechat.hdata_get('line'), signal_data, 'data') + buffer = weechat.hdata_pointer(weechat.hdata_get('line_data'), line_data, 'buffer') + + if not buffer in hotlist.keys(): + # just some background noise + return WEECHAT_RC_OK + + if (weechat.config_get_plugin("unhide_low") == "on" + and hotlist[buffer]["count_low"] > 0 + or hotlist[buffer]["count_message"] > 0 + or hotlist[buffer]["count_private"] > 0 + or hotlist[buffer]["count_highlight"] > 0): + remove_keep_alive(buffer) + weechat.buffer_set(buffer, "hidden", "0") + + return WEECHAT_RC_OK + + +def list_exemptions(): + """Return a list of exemption defined in ``exemptions``. + + :returns: A list of defined exemptions. + """ + return [x for x in weechat.config_get_plugin("exemptions").split(DELIMITER) if x != ""] + + +def add_to_exemptions(entry): + """Add an entry to the list of exemptions. + + An entry can be either a #channel or server_name.#channel + + :param entry: The entry to add. + :returns: the new list of entries. The return value is only used for unit testing. + """ + entries = list_exemptions() + entries.append(entry) + weechat.config_set_plugin("exemptions", DELIMITER.join(entries)) + weechat.prnt("", "[{}] add: {} added to exemptions.".format(SCRIPT_COMMAND, entry)) + return entries + + +def del_from_exemptions(entry): + """Remove an entry from the list of defined exemptions. + + :param entry: The entry to delete, which can be specified by the position in the list or by the name itself. + :returns: the new list of entries. The return value is only used for unit testing. + """ + entries = list_exemptions() + try: + # by index + try: + index = int(entry) - 1 + if index < 0: + raise IndexError + entry = entries.pop(index) + except IndexError: + weechat.prnt("", "[{}] del: Index out of range".format(SCRIPT_COMMAND)) + return entries + except ValueError: + try: + # by name + entries.remove(entry) + weechat.config_set_plugin("exemptions", DELIMITER.join(entries)) + except ValueError: + weechat.prnt("", "[{}] del: Could not find {}".format(SCRIPT_COMMAND, entry)) + return entries + + weechat.config_set_plugin("exemptions", DELIMITER.join(entries)) + weechat.prnt("", "[{}] del: Removed {} from exemptions.".format(SCRIPT_COMMAND, entry)) + return entries + + +def print_exemptions(): + """Print all exemptions defined in ``exemptions``""" + entries = list_exemptions() + if entries: + count = 1 + for entry in entries: + weechat.prnt("", "[{}] {}: {}".format(SCRIPT_COMMAND, count, entry)) + count += 1 + else: + weechat.prnt("", "[{}] list: No exemptions defined so far.".format(SCRIPT_COMMAND)) + + +def command_cb(data, buffer, args): + """Weechat callback for parsing and executing the given command. + + :returns: Callback return value expected by Weechat. + """ + list_args = args.split(" ") + + if list_args[0] not in ["add", "del", "list"]: + weechat.prnt("", "[{0}] bad option while using /{0} command, try '/help {0}' for more info".format( + SCRIPT_COMMAND)) + + elif list_args[0] == "add": + if len(list_args) == 2: + add_to_exemptions(list_args[1]) + + elif list_args[0] == "del": + if len(list_args) == 2: + del_from_exemptions(list_args[1]) + + elif list_args[0] == "list": + print_exemptions() + else: + weechat.command("", "/help " + SCRIPT_COMMAND) + + return WEECHAT_RC_OK + + +if (__name__ == '__main__' and import_ok and weechat.register( + SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', '')): + weechat_version = weechat.info_get("version_number", "") or 0 + if int(weechat_version) >= 0x01000000: + config_init() + CURRENT_BUFFER = weechat.current_buffer() + weechat.hook_signal("buffer_switch", "switch_buffer_cb", "") + weechat.hook_signal("buffer_line_added", "unhide_buffer_cb", "") + weechat.hook_command( + SCRIPT_NAME, + SCRIPT_DESC, + "add $buffer_name | del { $buffer_name | $list_position } | list", + " add : Add $buffer_name to the list of exemptions\n" + " $buffer_name can be either #channel or server_name.#channel\n" + " del : Delete $buffer_name from the list of exemptions\n" + " list : Return a list of all buffers that should not become hidden.", + "add|del|list", + "command_cb", + "" + ) + else: + weechat.prnt("", "{}{} requires WeeChat version 1.0 or higher".format( + weechat.prefix('error'), SCRIPT_NAME)) diff --git a/python/buffer_autoset.py b/python/buffer_autoset.py index 305bd6b2..9725cd76 100644 --- a/python/buffer_autoset.py +++ b/python/buffer_autoset.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2010-2015 Sébastien Helleu +# Copyright (C) 2010-2017 Sébastien Helleu # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,14 +22,20 @@ # # History: # +# 2021-06-02, Sébastien Helleu : +# version 1.2: fix /help buffer_autoset +# 2018-04-14, Kim B. Heino: +# version 1.1: on startup apply settings to already opened buffers +# 2017-06-21, Sébastien Helleu : +# version 1.0: rename command /autosetbuffer to /buffer_autoset # 2015-09-28, Simmo Saan : # version 0.9: instantly apply properties # 2015-07-12, Sébastien Helleu : # version 0.8: add option buffer_autoset.look.timer to add a small timer # before setting buffer properties -# 2015-04-05, Nils Görs : +# 2015-04-05, Nils Görs : # version 0.7: increase priority of hook_signal('buffer_opened') -# 2012-12-09, Nils Görs : +# 2012-12-09, Nils Görs : # version 0.6: add support of core buffer # 2012-03-09, Sébastien Helleu : # version 0.5: fix reload of config file @@ -46,11 +52,11 @@ SCRIPT_NAME = "buffer_autoset" SCRIPT_AUTHOR = "Sébastien Helleu " -SCRIPT_VERSION = "0.9" +SCRIPT_VERSION = "1.2" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Auto-set buffer properties when a buffer is opened" -SCRIPT_COMMAND = "autosetbuffer" +SCRIPT_COMMAND = SCRIPT_NAME import_ok = True @@ -143,7 +149,7 @@ def bas_config_write(): # ================================[ command ]================================= def bas_cmd(data, buffer, args): - """Callback for /autosetbuffer command.""" + """Callback for /buffer_autoset command.""" args = args.strip() if args == "": weechat.command("", "/set %s.buffer.*" % CONFIG_FILE_NAME) @@ -172,7 +178,7 @@ def bas_completion_current_buffer_cb(data, completion_item, buffer, completion): """ Complete with current buffer name (plugin.name), - for command '/autosetbuffer'. + for command '/buffer_autoset'. """ name = "%s.%s" % (weechat.buffer_get_string(buffer, "plugin"), weechat.buffer_get_string(buffer, "name")) @@ -182,7 +188,7 @@ def bas_completion_current_buffer_cb(data, completion_item, buffer, def bas_completion_options_cb(data, completion_item, buffer, completion): - """Complete with config options, for command '/autosetbuffer'.""" + """Complete with config options, for command '/buffer_autoset'.""" options = weechat.infolist_get("option", "", "%s.buffer.*" % CONFIG_FILE_NAME) if options: @@ -240,11 +246,12 @@ def bas_signal_buffer_opened_cb(data, signal, signal_data): weechat.buffer_get_string(buffer, "full_name")) return weechat.WEECHAT_RC_OK + def bas_config_option_cb(data, option, value): if not weechat.config_boolean(bas_options["look_instant"]): return weechat.WEECHAT_RC_OK - if not weechat.config_get(option): # option was deleted + if not weechat.config_get(option): # option was deleted return weechat.WEECHAT_RC_OK option = option[len("%s.buffer." % CONFIG_FILE_NAME):] @@ -267,6 +274,7 @@ def bas_config_option_cb(data, option, value): return weechat.WEECHAT_RC_OK + # ==================================[ main ]================================== if __name__ == "__main__" and import_ok: @@ -285,26 +293,25 @@ def bas_config_option_cb(data, option, value): "[add buffer property value] | [del option]", " add: add a buffer/property/value in configuration file\n" " del: delete an option from configuration file\n" - " buffer: name of a buffer (can start or end with \"*\" as " - "wildcard)\n" + " buffer: name of a buffer (wildcard \"*\" is allowed)\n" "property: buffer property\n" " value: value for property\n" " option: name of option from configuration file\n\n" "Examples:\n" " disable timestamp on channel #weechat:\n" - " /" + SCRIPT_COMMAND + " add irc.freenode.#weechat " + " /" + SCRIPT_COMMAND + " add irc.libera.#weechat " "time_for_each_line 0\n" " add word \"weechat\" in highlight list on channel " "#savannah:\n" - " /" + SCRIPT_COMMAND + " add irc.freenode.#savannah " + " /" + SCRIPT_COMMAND + " add irc.libera.#savannah " "highlight_words_add weechat\n" - " disable highlights from nick \"mike\" on freenode server, " + " disable highlights from nick \"mike\" on libera server, " "channel #weechat (requires WeeChat >= 0.3.4):\n" - " /" + SCRIPT_COMMAND + " add irc.freenode.#weechat " + " /" + SCRIPT_COMMAND + " add irc.libera.#weechat " "hotlist_max_level_nicks_add mike:2\n" - " disable hotlist changes for nick \"bot\" on freenode " + " disable hotlist changes for nick \"bot\" on libera " "server (all channels) (requires WeeChat >= 0.3.4):\n" - " /" + SCRIPT_COMMAND + " add irc.freenode.* " + " /" + SCRIPT_COMMAND + " add irc.libera.* " "hotlist_max_level_nicks_add bot:-1", "add %(buffers_plugins_names)|" "%(buffer_autoset_current_buffer) " @@ -324,8 +331,13 @@ def bas_config_option_cb(data, option, value): weechat.hook_config("%s.buffer.*" % CONFIG_FILE_NAME, "bas_config_option_cb", "") - # core buffer is already open on script startup, check manually! - bas_signal_buffer_opened_cb("", "", weechat.buffer_search_main()) + # apply settings to all already opened buffers + buffers = weechat.infolist_get("buffer", "", "") + if buffers: + while weechat.infolist_next(buffers): + buffer = weechat.infolist_pointer(buffers, "pointer") + bas_signal_buffer_opened_cb("", "", buffer) + weechat.infolist_free(buffers) # ==================================[ end ]=================================== diff --git a/python/buffer_bind.py b/python/buffer_bind.py new file mode 100644 index 00000000..418abedd --- /dev/null +++ b/python/buffer_bind.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import weechat as w + +SCRIPT_NAME = "buffer_bind" +SCRIPT_AUTHOR = "Trevor 'tee' Slocum " +SCRIPT_VERSION = "1.0" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = "Bind meta- to the current buffer" +SCRIPT_NOTE = """Case sensitivity is controlled via plugins.var.python.%s.case_sensitive (default: off) + +%s is a port of irssi's window_alias written by veli@piipiip.net""" % (SCRIPT_NAME, SCRIPT_NAME) + +SETTINGS = { + "case_sensitive": "off" +} + + +def command_buffer_bind(data, buffer, args): + if len(args) == 1 and args[0] != "": + bindkey = args[0] + buffername = w.buffer_get_string(buffer, "name") + + bind_keys = [bindkey] + if w.config_get_plugin("case_sensitive") == "off" and bindkey.isalpha(): + bind_keys.append(bindkey.swapcase()) + for bind_keys_i in bind_keys: + w.command(buffer, "/key bind meta-%s /buffer %s" % (bind_keys_i, buffername)) + + w.prnt(buffer, "Buffer %s is now accessible with meta-%s" % (buffername, bindkey)) + else: + w.command(buffer, "/help %s" % SCRIPT_NAME) + + return w.WEECHAT_RC_OK_EAT + + +if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, + SCRIPT_DESC, "", ""): + for option, value in SETTINGS.items(): + if not w.config_is_set_plugin(option): + w.config_set_plugin(option, value) + + w.hook_command(SCRIPT_NAME, SCRIPT_DESC, "", SCRIPT_NOTE, "key", "command_buffer_bind", "") diff --git a/python/buffer_dmenu.py b/python/buffer_dmenu.py new file mode 100644 index 00000000..16893d84 --- /dev/null +++ b/python/buffer_dmenu.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2020 by Ferus Castor +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + + +# Select a buffer from dmenu, rofi, or fzf-tmux +# Screenshot with fzf-tmux: https://seirdy.one/misc/buffer-dmenu-tmux.png +# To call externally (IE: from i3), enable weechat fifo and run: +# $ echo "core.weechat */buffer_dmenu" >> $(find ~/.weechat -type p) +# +# Optionally requires i3-py [py2] (or i3ipc [py3]) to focus weechat in i3 +# +# History: +# 2021-03-19, Seirdy +# version 0.2.1: add support for fzf-tmux +# 2020-06-08, Ferus +# version 0.2: drop support of py2 and fix error when closing +# dmenu/rofi with no choice selected +# 2020-02024, Seirdy +# version 0.1.2: py3-ok +# 2017-05-03, Ferus +# version 0.1.1: fix argument error for config_set_plugin +# 2016-05-01, Ferus +# version 0.1: initial release - requires WeeChat ≥ 0.3.7 + +# TODO: +# Option to remove certain buffer types +# Implement `focus` for other window managers +# if buffer == currentbuffer: switch to previous buffer + +# pylint: disable=I0011,W0603,W1401 + +SCRIPT_NAME = "buffer_dmenu" +SCRIPT_AUTHOR = "Ferus " +SCRIPT_VERSION = "0.2.1" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = ( + "List buffers in dmenu (or rofi/fzf-tmux), changes active window to selected buffer" +) +SCRIPT_COMMAND = "buffer_dmenu" + +import os +import subprocess + +try: + import weechat as w +except ImportError as e: + print("This script must be run under WeeChat.") + exit(1) + +try: + import i3ipc as i3 + + have_i3 = True +except ImportError as e: + have_i3 = False + +settings = { + "launcher": ("dmenu", "launcher to use (supported: dmenu, rofi, fzf_tmux)"), + "focus": ( + "false", + "whether to immediately focus the terminal after selecting buffer", + ), + "focus.wm": ("i3", "wm focus logic to use (supported: i3)"), + "dmenu.command": ("dmenu -b -i -l 20", "command used to call dmenu"), + "rofi.command": ( + "rofi -p '# ' -dmenu -lines 10 -columns 8 -auto-select -mesg 'Pick a buffer to jump to:'", + "command used to call rofi", + ), + "fzf_tmux.command": ( + "sed -e \"s/b'//\" -e s#\\\\n#\\n#g | fzf-tmux -w 40 -h 70%", + "command used to call fzf-tmux", + ), + "title.regex": ("WeeChat \d+\.\d+", "regex used to match weechat's title window"), +} + + +def check_dmenu(): + devnull = open(os.devnull) + retcode = subprocess.call(["which", "dmenu"], stdout=devnull, stderr=devnull) + return True if retcode == 0 else False + + +def check_rofi(): + devnull = open(os.devnull) + retcode = subprocess.call(["which", "rofi"], stdout=devnull, stderr=devnull) + return True if retcode == 0 else False + + +def check_fzf_tmux(): + devnull = open(os.devnull) + retcode = subprocess.call(["which", "fzf-tmux"], stdout=devnull, stderr=devnull) + return True if retcode == 0 else False + + +def get_launcher(): + launcher = w.config_get_plugin("launcher") + command = None + if launcher == "dmenu": + if check_dmenu(): + command = w.config_get_plugin("dmenu.command") + elif launcher == "rofi": + if check_rofi(): + command = w.config_get_plugin("rofi.command") + elif launcher == "fzf_tmux": + if check_fzf_tmux(): + command = w.config_get_plugin("fzf_tmux.command") + return command + + +def launch(options): + launcher = get_launcher() + if launcher: + call(launcher, options) + return True + + +def focus(): + if w.config_string_to_boolean(w.config_get_plugin("focus")): + if w.config_get_plugin("focus.wm") == "i3": + focus_i3() + + +def focus_i3(): + if have_i3: + regex = w.config_get_plugin("title.regex") + + i3conn = i3.Connection() + weechat = i3conn.get_tree().find_named(regex)[0] + weechat.command("focus") + + +def call(command, options): + options = "\n".join(options) + + w.hook_process_hashtable( + "sh", + {"arg1": "-c", "arg2": 'echo "{0}" | {1}'.format(options, command)}, + 10 * 1000, + "launch_process_cb", + "", + ) + + +process_output = "" + + +def launch_process_cb(data, command, rc, out, err): + global process_output + if out == "" or ":" not in out: + return w.WEECHAT_RC_ERROR + process_output += out + if int(rc) >= 0: + selected = process_output.strip("\n") + _, name = selected.split(":") + process_output = "" + switch_to_buffer(name) + focus() + return w.WEECHAT_RC_OK + + +def get_open_buffers(): + buffers = [] + infolist = w.infolist_get("buffer", "", "") + if infolist: + while w.infolist_next(infolist): + name = w.infolist_string(infolist, "name") + number = w.infolist_integer(infolist, "number") + _ = "{0}:{1}".format(number, name) + buffers.append(_) + w.infolist_free(infolist) + return buffers + + +def get_hotlist_buffers(): + buffers = [] + infolist = w.infolist_get("hotlist", "", "") + if infolist: + while w.infolist_next(infolist): + number = w.infolist_integer(infolist, "buffer_number") + buffer = w.infolist_pointer(infolist, "buffer_pointer") + name = w.buffer_get_string(buffer, "name") + _ = "{0}:{1}".format(number, name) + buffers.append(_) + w.infolist_free(infolist) + return buffers + + +def switch_to_buffer(buffer_name): + w.command("", "/buffer {0}".format(buffer_name)) + + +def dmenu_cmd_cb(data, buffer, args): + """ Command /buffers_dmenu """ + if args == "hotlist": + buffers = get_hotlist_buffers() + else: + buffers = get_open_buffers() + + if not launch(buffers): + return w.WEECHAT_RC_ERROR + return w.WEECHAT_RC_OK + + +if w.register( + SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "" +): + version = w.info_get("version_number", "") or 0 + for option, value in settings.items(): + if not w.config_is_set_plugin(option): + w.config_set_plugin(option, value[0]) + if int(version) >= 0x00030500: + w.config_set_desc_plugin( + option, '{0} (default: "{1}")'.format(value[1], value[0]) + ) + + w.hook_command( + SCRIPT_COMMAND, + "Show a list of all buffers in dmenu", + "[hotlist]", + " hotlist: shows hotlist buffers only\n" + "\n" + "To call externally (IE: from i3), enable weechat fifo and run:\n" + " $ echo 'core.weechat */buffer_dmenu' >> $(find ~/.weechat -type p)\n" + "\n" + "To focus the terminal containing WeeChat for the following WM:\n" + " i3: requires i3ipc from pip3\n", + "", + "dmenu_cmd_cb", + "", + ) diff --git a/python/buffer_open.py b/python/buffer_open.py new file mode 100644 index 00000000..250ae6e4 --- /dev/null +++ b/python/buffer_open.py @@ -0,0 +1,345 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019 by Simmo Saan +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# +# History: +# +# 2021-11-06, Sébastien Helleu +# version 0.3: make script compatible with WeeChat >= 3.4 +# (new parameters in function hdata_search) +# 2019-10-20, Simmo Saan +# version 0.2: improve script description +# 2019-10-18, Simmo Saan +# version 0.1: initial script +# + +""" +Open buffers by full name, reopen closed recently closed buffers, open layout buffers +""" + +# Adding handler for other full names in this script (only if necessary) or other scripts (preferred): +# +# def buffer_open_full_name_cb(data, signal, hashtable): +# full_name = hashtable["full_name"] +# noswitch = bool(int(hashtable.get("noswitch", "0"))) +# +# if full_name == "my.buffer.full.name": +# # open my.buffer.full.name, considering noswitch if possible +# return weechat.WEECHAT_RC_OK_EAT # prevent other callbacks from handling this full name +# +# return weechat.WEECHAT_RC_OK # let other callbacks handle this full name +# +# weechat.hook_hsignal("buffer_open_full_name", "buffer_open_full_name_cb", "") + +from __future__ import print_function + +SCRIPT_NAME = "buffer_open" +SCRIPT_AUTHOR = "Simmo Saan " +SCRIPT_VERSION = "0.3" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = "Open buffers by full name, reopen closed recently closed buffers, open layout buffers" + +SCRIPT_REPO = "https://github.com/sim642/buffer_open" + +SCRIPT_COMMAND = SCRIPT_NAME + +IMPORT_OK = True + +try: + import weechat +except ImportError: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org/") + IMPORT_OK = False + +import re +import collections + + +SETTINGS = { + "layout_apply": ( + "off", + "open closed layout buffers on /layout apply" + ), + "max_closed": ( + "10", + "maximum number of closed buffers to remember" + ) +} + + +def log(string): + weechat.prnt("", "{}: {}".format(SCRIPT_NAME, string)) + + +def error(string): + weechat.prnt("", "{}{}: {}".format(weechat.prefix("error"), SCRIPT_NAME, string)) + + +def command_plugin(plugin, command): + weechat.command("", "/command {} {}".format(plugin, command)) + + +def buffer_open_full_name_opened_cb(data, signal, hashtable): + full_name = hashtable["full_name"] + buffer = weechat.buffer_search("==", full_name) + if buffer: + # already open, do nothing + return weechat.WEECHAT_RC_OK_EAT + + return weechat.WEECHAT_RC_OK + + +def buffer_open_full_name_unhandled_cb(data, signal, hashtable): + full_name = hashtable["full_name"] + error("no handler for opening buffer {}".format(full_name)) + return weechat.WEECHAT_RC_OK + + +TABLE = { + # "full_name": ("plugin", "command"), + "core.secured_data": ("core", "/secure"), + "core.color": ("core", "/color"), + "core.weechat": ("core", ""), # do nothing because always open + "fset.fset": ("fset", "/fset"), + "irc.irc_raw": ("irc", "/server raw"), + "relay.relay.list": ("relay", "/relay"), + "relay.relay_raw": ("relay", "/relay raw"), + "script.scripts": ("script", "/script"), + "trigger.monitor": ("trigger", "/trigger monitor"), + "xfer.xfer.list": ("xfer", "/xfer"), # TODO: xfer DCC chat buffer +} + + +def buffer_open_full_name_table_cb(data, signal, hashtable): + full_name = hashtable["full_name"] + + if full_name in TABLE: + plugin, command = TABLE[full_name] + command_plugin(plugin, command) + return weechat.WEECHAT_RC_OK_EAT + + return weechat.WEECHAT_RC_OK + + +IRC_SERVER_RE = re.compile(r"^irc\.server\.(.+)$") +IRC_BUFFER_RE = re.compile(r"^irc\.([^.]+)\.(.+)$") + +irc_server_connected_opens = collections.defaultdict(set) + + +def irc_server_open(server, noswitch): + command_plugin("irc", "/connect {}".format(server)) # /connect doesn't have -noswitch + + +def hdata_search(hdata, pointer, expr, name): + weechat_version = int(weechat.info_get("version_number", "") or 0) + if weechat_version >= 0x03040000: + return weechat.hdata_search( + hdata, + pointer, + expr + " == ${name}", + {}, + {"name": name}, + {}, + 1, + ) + return weechat.hdata_search(hdata, pointer, expr + " == " + name, 1) + + +def irc_buffer_open(server, name, noswitch): + hdata_irc_server = weechat.hdata_get("irc_server") + irc_servers = weechat.hdata_get_list(hdata_irc_server, "irc_servers") + irc_server = hdata_search(hdata_irc_server, irc_servers, + "${irc_server.name}", server) + chantypes = weechat.hdata_string(hdata_irc_server, irc_server, "chantypes") + is_channel = name[0] in chantypes + + noswitch_flag = "-noswitch " if noswitch else "" + if is_channel: + command_plugin("irc", "/join {}-server {} {}".format(noswitch_flag, server, name)) + else: + command_plugin("irc", "/query {}-server {} {}".format(noswitch_flag, server, name)) + + +def irc_server_connected_cb(data, signal, server): + for name, noswitch in irc_server_connected_opens[server]: + irc_buffer_open(server, name, noswitch) + + irc_server_connected_opens[server] = set() + return weechat.WEECHAT_RC_OK + + +def buffer_open_full_name_irc_cb(data, signal, hashtable): + full_name = hashtable["full_name"] + noswitch = bool(int(hashtable.get("noswitch", "0"))) + + m = IRC_SERVER_RE.match(full_name) + if m: + server = m.group(1) + irc_server_open(server, noswitch) + return weechat.WEECHAT_RC_OK_EAT + + m = IRC_BUFFER_RE.match(full_name) + if m: + server = m.group(1) + name = m.group(2) + + hdata_irc_server = weechat.hdata_get("irc_server") + irc_servers = weechat.hdata_get_list(hdata_irc_server, "irc_servers") + irc_server = hdata_search(hdata_irc_server, irc_servers, + "${irc_server.name} == ", server) + if irc_server: + is_connected = bool(weechat.hdata_integer(hdata_irc_server, irc_server, "is_connected")) + is_connecting = bool(weechat.hdata_pointer(hdata_irc_server, irc_server, "hook_connect")) + if is_connected: + irc_buffer_open(server, name, noswitch) + else: + irc_server_connected_opens[server].add((name, noswitch)) + if not is_connecting: + irc_server_open(server, noswitch) + else: + error("unknown server {}".format(server)) + + return weechat.WEECHAT_RC_OK_EAT + + return weechat.WEECHAT_RC_OK + + +def buffer_open_full_name(full_name, noswitch=None): + hashtable = { + "full_name": full_name + } + if noswitch is not None: + hashtable["noswitch"] = str(int(noswitch)) # must be str for API + + weechat.hook_hsignal_send("buffer_open_full_name", hashtable) + + +def command_cb(data, buffer, args): + args = args.split() + if len(args) >= 1 and args[0] == "closed": + if len(args) >= 2 and args[1] == "-list": + if buffer_closed_stack: + weechat.prnt("", "closed buffers (latest first):") + for full_name in reversed(buffer_closed_stack): + weechat.prnt("", " {}".format(full_name)) + else: + weechat.prnt("", "no known closed buffers") + else: + if buffer_closed_stack: + noswitch = len(args) >= 2 and args[1] == "-noswitch" + full_name = buffer_closed_stack.pop() + buffer_open_full_name(full_name, noswitch=noswitch) + else: + error("no known closed buffers") + return weechat.WEECHAT_RC_ERROR + elif len(args) >= 1: + noswitch = args[0] == "-noswitch" + if noswitch: + if len(args) >= 2: + full_name = args[1] + else: + error("missing full name") + return weechat.WEECHAT_RC_ERROR + else: + full_name = args[0] + buffer_open_full_name(full_name, noswitch=noswitch) + else: + error("unknown subcommand") + return weechat.WEECHAT_RC_ERROR + + return weechat.WEECHAT_RC_OK + + +buffer_closed_stack = [] + + +def buffer_closing_cb(data, signal, buffer): + global buffer_closed_stack + + full_name = weechat.buffer_get_string(buffer, "full_name") + buffer_closed_stack.append(full_name) + + max_closed = int(weechat.config_get_plugin("max_closed")) + buffer_closed_stack = buffer_closed_stack[max(0, len(buffer_closed_stack) - max_closed):] + return weechat.WEECHAT_RC_OK + + +LAYOUT_APPLY_RE = re.compile(r"^/layout apply(?:\s+(\S+)(?:\s+buffers)?)?$") + + +def layout_apply_cb(data, buffer, command): + if weechat.config_string_to_boolean(weechat.config_get_plugin("layout_apply")): + m = LAYOUT_APPLY_RE.match(command) + if m: + layout_name = m.group(1) or "default" + hdata_layout = weechat.hdata_get("layout") + layouts = weechat.hdata_get_list(hdata_layout, "gui_layouts") + layout = hdata_search(hdata_layout, layouts, + "${layout.name}", layout_name) + if layout: + hdata_layout_buffer = weechat.hdata_get("layout_buffer") + layout_buffer = weechat.hdata_pointer(hdata_layout, layout, "layout_buffers") + while layout_buffer: + plugin_name = weechat.hdata_string(hdata_layout_buffer, layout_buffer, "plugin_name") + buffer_name = weechat.hdata_string(hdata_layout_buffer, layout_buffer, "buffer_name") + full_name = "{}.{}".format(plugin_name, buffer_name) + + buffer = weechat.buffer_search("==", full_name) + if not buffer: + buffer_open_full_name(full_name, noswitch=True) + + layout_buffer = weechat.hdata_move(hdata_layout_buffer, layout_buffer, 1) + return weechat.WEECHAT_RC_OK + + +if __name__ == "__main__" and IMPORT_OK: + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + weechat.hook_hsignal("10000|buffer_open_full_name", "buffer_open_full_name_opened_cb", "") + weechat.hook_hsignal("0|buffer_open_full_name", "buffer_open_full_name_unhandled_cb", "") + + weechat.hook_hsignal("500|buffer_open_full_name", "buffer_open_full_name_table_cb", "") + + weechat.hook_hsignal("500|buffer_open_full_name", "buffer_open_full_name_irc_cb", "") + weechat.hook_signal("irc_server_connected", "irc_server_connected_cb", "") + + weechat.hook_command(SCRIPT_COMMAND, SCRIPT_DESC, +"""closed [-noswitch|-list] + || [-noswitch] """, +""" closed: open most recently closed buffer +closed -list: list most recently closed buffers + -noswitch: try not to switch to new buffer + +Without subcommand, this command opens a buffer with given full name. + +Option "{prefix}.max_closed" specifies the number of most recently closed buffers that are remembered. + +If option "{prefix}.layout_apply" is on and "/layout apply" is executed, closed buffers in the layout are opened.""".format(prefix="plugins.var.python.{}".format(SCRIPT_NAME)), +"""closed -noswitch|-list %- + || -noswitch""".replace("\n", ""), + "command_cb", "") + + weechat.hook_signal("buffer_closing", "buffer_closing_cb", "") + weechat.hook_command_run("/layout apply*", "layout_apply_cb", "") + + for option, value in SETTINGS.items(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value[0]) + + weechat.config_set_desc_plugin(option, "{} (default: \"{}\")".format(value[1], value[0])) diff --git a/python/buffer_swap.py b/python/buffer_swap.py deleted file mode 100644 index 3863ae03..00000000 --- a/python/buffer_swap.py +++ /dev/null @@ -1,135 +0,0 @@ -# -*- coding: utf-8 -*- -# -# buffer_swap, version 0.3 for WeeChat version 0.3 -# Latest development version: https://github.com/FiXato/weechat_scripts -# -# Swaps given 2 buffers. Requested by kakazza -# -## Example: -# Swaps buffers 3 and 5 -# /swap 3 5 -# -# Swaps current buffer with the #weechat buffer -# /swap #weechat -# -## History: -### 2011-09-18: FiXato: -# -# * version 0.1: initial release. -# * Allow switching 2 given buffers -# -# * version 0.2: cleanup -# * Made the command example more clear that it requires 2 buffer *numbers* -# * After switching, now switches back to your original buffer. -# -# * version 0.3: current buffer support -# * If you only specify 1 buffer, the current buffer will be used -# -## Acknowledgements: -# * Sebastien "Flashcode" Helleu, for developing the kick-ass chat/IRC -# client WeeChat -# -## TODO: -# - Check if given buffers exist. -# -## Copyright (c) 2011 Filip H.F. "FiXato" Slagter, -# -# http://google.com/profiles/FiXato -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -SCRIPT_NAME = "buffer_swap" -SCRIPT_AUTHOR = "Filip H.F. 'FiXato' Slagter " -SCRIPT_VERSION = "0.3" -SCRIPT_LICENSE = "MIT" -SCRIPT_DESC = "Swaps given 2 buffers" -SCRIPT_COMMAND = "swap" -SCRIPT_CLOSE_CB = "close_cb" - -import_ok = True - -try: - import weechat -except ImportError: - print "This script must be run under WeeChat." - import_ok = False - -def close_cb(*kwargs): - return weechat.WEECHAT_RC_OK - -def command_main(data, buffer, args): - args = args.split() - curr_buffer = weechat.current_buffer() - curr_buffer_number = weechat.buffer_get_integer(curr_buffer, "number") - - if len(args) != 1 and len(args) != 2: - weechat.prnt("", "You need to specify 1 or 2 buffers") - return weechat.WEECHAT_RC_ERROR - - if len(args) == 2: - weechat.command("", "/buffer %s" % args[0]) - first_buffer = weechat.current_buffer() - first_buffer_number = weechat.buffer_get_integer(first_buffer, "number") - - weechat.command("", "/buffer %s" % args[1]) - second_buffer = weechat.current_buffer() - second_buffer_number = weechat.buffer_get_integer(second_buffer, "number") - else: - first_buffer = weechat.current_buffer() - first_buffer_number = weechat.buffer_get_integer(first_buffer, "number") - - weechat.command("", "/buffer %s" % args[0]) - second_buffer = weechat.current_buffer() - second_buffer_number = weechat.buffer_get_integer(second_buffer, "number") - - weechat.buffer_set(first_buffer, "number", str(second_buffer_number)) - weechat.buffer_set(second_buffer, "number", str(first_buffer_number)) - - weechat.command("", "/buffer %s" % str(curr_buffer_number)) - - return weechat.WEECHAT_RC_OK - -if __name__ == "__main__" and import_ok: - if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, - SCRIPT_LICENSE, SCRIPT_DESC, SCRIPT_CLOSE_CB, ""): - # # Set default settings - # for option, default_value in cs_settings.iteritems(): - # if not weechat.config_is_set_plugin(option): - # weechat.config_set_plugin(option, default_value) - - weechat.hook_command(SCRIPT_COMMAND, - SCRIPT_DESC, - "[buffer] ", - "Swaps given buffers: \n" - "the /swap command accepts anything that /buffer would accept for switching buffers\n" - "/swap 3 10\n" - "would swap buffer 3 and 10 of place\n" - "/swap 3\n" - "would swap current buffer with buffer number 10\n" - "/swap 3 #weechat\n" - "would swap buffer 3 and the #weechat buffer of place\n" - "/swap #weechat\n" - "would swap current buffer with the #weechat buffer", - - "", - - "command_main", "") - - diff --git a/python/bufsave.py b/python/bufsave.py index 0591127c..88ad9f63 100644 --- a/python/bufsave.py +++ b/python/bufsave.py @@ -1,8 +1,7 @@ -''' Buffer saver ''' # -*- coding: utf-8 -*- # # Copyright (c) 2009 by xt -# Copyright (c) 2012 by Sebastien Helleu +# Copyright (c) 2012-2021 by Sebastien Helleu # Based on bufsave.pl for 0.2.x by FlashCode # # This program is free software; you can redistribute it and/or modify @@ -24,55 +23,72 @@ # (this script requires WeeChat 0.3.0 or newer) # # History: -# 2012-08-28, Sebastien Helleu : -# version 0.3: compatibility with WeeChat >= 0.3.9 (hdata_time is now long instead of string) -# 2012-08-23, Sebastien Helleu : +# 2021-05-02, Sébastien Helleu : +# version 0.5: add compatibility with WeeChat >= 3.2 (XDG directories) +# 2017-12-02, Benjamin Roberts : +# version 0.4: added support for home path expansion +# 2012-08-28, Sébastien Helleu : +# version 0.3: compatibility with WeeChat >= 0.3.9 (hdata_time is now long +# instead of string) +# 2012-08-23, Sébastien Helleu : # version 0.2: use hdata for WeeChat >= 0.3.6 (improve performance) # 2009-06-10, xt # version 0.1: initial release # + +"""Buffer saver.""" + import weechat as w from os.path import exists import time -SCRIPT_NAME = "bufsave" -SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "0.3" +SCRIPT_NAME = "bufsave" +SCRIPT_AUTHOR = "xt " +SCRIPT_VERSION = "0.5" SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Save buffer to a file" -SCRIPT_COMMAND = SCRIPT_NAME +SCRIPT_DESC = "Save buffer to a file" +SCRIPT_COMMAND = SCRIPT_NAME -if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - w.hook_command(SCRIPT_COMMAND, - "save current buffer to a file", - "[filename]", - " filename: target file (must not exist)\n", - "%f", - "bufsave_cmd", - '') +if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, + SCRIPT_DESC, "", ""): + w.hook_command( + SCRIPT_COMMAND, + "save current buffer to a file", + "[filename]", + " filename: target file (must not exist)\n", + "%f", + "bufsave_cmd", + '', + ) -def cstrip(text): - ''' Use weechat color strip on text''' +def cstrip(text): + """Use weechat color strip on text.""" return w.string_remove_color(text, '') + def bufsave_cmd(data, buffer, args): - ''' Callback for /bufsave command ''' + """Callback for /bufsave command.""" - filename = args + filename_raw = args - if not filename: - w.command('', '/help %s' %SCRIPT_COMMAND) + if not filename_raw: + w.command('', '/help %s' % SCRIPT_COMMAND) return w.WEECHAT_RC_OK + options = { + 'directory': 'data', + } + filename = w.string_eval_path_home(filename_raw, {}, {}, options) + if exists(filename): w.prnt('', 'Error: target file already exists!') return w.WEECHAT_RC_OK try: fp = open(filename, 'w') - except: + except Exception: w.prnt('', 'Error writing to target file!') return w.WEECHAT_RC_OK @@ -81,31 +97,37 @@ def bufsave_cmd(data, buffer, args): # use hdata with WeeChat >= 0.3.6 (direct access to data, very fast) own_lines = w.hdata_pointer(w.hdata_get('buffer'), buffer, 'own_lines') if own_lines: - line = w.hdata_pointer(w.hdata_get('lines'), own_lines, 'first_line') + line = w.hdata_pointer(w.hdata_get('lines'), + own_lines, 'first_line') hdata_line = w.hdata_get('line') hdata_line_data = w.hdata_get('line_data') while line: data = w.hdata_pointer(hdata_line, line, 'data') if data: date = w.hdata_time(hdata_line_data, data, 'date') - # since WeeChat 0.3.9, hdata_time returns long instead of string + # since WeeChat 0.3.9, hdata_time returns long instead of + # string if not isinstance(date, str): - date = time.strftime('%F %T', time.localtime(int(date))) - fp.write('%s %s %s\n' %(\ - date, - cstrip(w.hdata_string(hdata_line_data, data, 'prefix')), - cstrip(w.hdata_string(hdata_line_data, data, 'message')), - )) + date = time.strftime('%F %T', + time.localtime(int(date))) + fp.write('%s %s %s\n' % ( + date, + cstrip(w.hdata_string(hdata_line_data, + data, 'prefix')), + cstrip(w.hdata_string(hdata_line_data, + data, 'message')), + )) line = w.hdata_move(hdata_line, line, 1) else: - # use infolist with WeeChat <= 0.3.5 (full duplication of lines, slow and uses memory) + # use infolist with WeeChat <= 0.3.5 (full duplication of lines, + # slow and uses memory) infolist = w.infolist_get('buffer_lines', buffer, '') while w.infolist_next(infolist): - fp.write('%s %s %s\n' %(\ - w.infolist_time(infolist, 'date'), - cstrip(w.infolist_string(infolist, 'prefix')), - cstrip(w.infolist_string(infolist, 'message')), - )) + fp.write('%s %s %s\n' % ( + w.infolist_time(infolist, 'date'), + cstrip(w.infolist_string(infolist, 'prefix')), + cstrip(w.infolist_string(infolist, 'message')), + )) w.infolist_free(infolist) fp.close() diff --git a/python/bufsize.py b/python/bufsize.py index d0f82d2f..153c9bc4 100644 --- a/python/bufsize.py +++ b/python/bufsize.py @@ -18,8 +18,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +# 2017-08-17: nils_2 (freenode.#weechat) +# 0.8: add support for buffer_filters_enabled and buffer_filters_disabled (WeeChat ≥ 2.0) # 2016-12-16: nils_2 (freenode.#weechat) -# 0.7: add option show_scroll (idea by earnestly) +# 0.7: add option show_scroll (idea by earnestly) # 2016-04-23: wdbw # 0.6.2 : fix: type of filters_enabled # 2014-02-24: nesthib (freenode.#weechat) @@ -56,7 +58,7 @@ SCRIPT_NAME = "bufsize" SCRIPT_AUTHOR = "nils_2 " -SCRIPT_VERSION = "0.7" +SCRIPT_VERSION = "0.8" SCRIPT_LICENSE = "GPL" SCRIPT_DESC = "scroll indicator; displaying number of lines below last line, overall lines in buffer, number of current line and percent displayed" @@ -235,7 +237,7 @@ def toggle_refresh(pointer, name, value): weechat.hook_signal('buffer_line_added','update_cb','') weechat.hook_signal('window_scrolled','update_cb','') weechat.hook_signal('buffer_switch','update_cb','') - weechat.hook_signal('filters_*','filtered_update_cb','') + weechat.hook_signal('*filters*','filtered_update_cb','') weechat.hook_command_run('/buffer clear*','update_cb','') weechat.hook_command_run('/window page*','update_cb','') weechat.hook_command_run('/input zoom_merged_buffer','update_cb','') diff --git a/python/chanact.py b/python/chanact.py index c747533c..2a51a9c3 100644 --- a/python/chanact.py +++ b/python/chanact.py @@ -4,6 +4,10 @@ # (this script requires WeeChat 0.3.0 or newer) # # History: +# 2020-06-10, Sébastien Helleu +# version 1.0: Fix undefined variable +# 2020-06-10, squigz +# version 0.9: Update for Python 3 # 2013-09-16, d33tah # version 0.8: Added sort_by_number configuration variable. # 2013-03-18, mythmon @@ -63,7 +67,7 @@ SCRIPT_NAME = "chanact" SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "0.8" +SCRIPT_VERSION = "1.0" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Hotlist replacement, use names and keybindings instead of numbers" @@ -185,7 +189,7 @@ def chanact_cb(*args): entry = '%s%s%s' % ( w.color(color), number, - w.color(reset)) + w.color('reset')) activity.append((entry, thebuffer, sort_rank(thebuffer, priority), int_number)) @@ -211,7 +215,7 @@ def chanact_update(*args): if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): - for option, default_value in settings.iteritems(): + for option, default_value in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_value) diff --git a/python/chancomp.py b/python/chancomp.py index 33e79622..443a39e1 100644 --- a/python/chancomp.py +++ b/python/chancomp.py @@ -27,19 +27,19 @@ # This script compares channels with a different user via WHOIS and returns which # channels you share. The script comes with a command that can be used to force # comparisons (/chancomp ). Depending on your settings, the script will also -# function during normal /WHOIS requests and alternatively offer extra messages +# function during normal /whois requests and alternatively offer extra messages # when the verbose setting is turned on. # # Script options: # compare_only_on_command (default: off, options: on, off) # Require usage of /chancomp to do comparisons, and do not perform comparisons on -# normal /WHOIS requests. +# normal /whois requests. # # ignored_servers (default: "", expects a comma separated string of servers to ignore.) # The script does not do comparisons on ignored servers. This setting expects # a comma separated list of servers (i.e.: "freenode,notfreenode") - ignored # servers will stop /chancomp from functioning and will not display comparisons -# in your /WHOIS data. +# in your /whois data. # # output_priority (default: smart, options: smart, shared, not_shared) # In order to not display too much information to consume, the output_priority @@ -68,6 +68,8 @@ # Script creation # version 1.1 - 2015-12-16 # Honour irc.look.msgbuffer_fallback +# version 1.2 - 2023-02-05 +# Replace command /WHOIS by /whois (compatibility with WeeChat 3.9) # # TODOs: # - Possibly support a verbose output of "sharing all their channels" @@ -90,7 +92,7 @@ SCRIPT_NAME = "chancomp" SCRIPT_AUTHOR = "Zarthus " -SCRIPT_VERSION = "1.1" +SCRIPT_VERSION = "1.2" SCRIPT_LICENSE = "MIT" SCRIPT_DESC = "List shared channels with user on command or WHOIS" SCRIPT_COMMAND = "chancomp" @@ -105,7 +107,7 @@ def cmd_chancomp(data, buffer, target): global _force_comparison _force_comparison = True - w.command("", "/WHOIS {}".format(target)) + w.command("", "/whois {}".format(target)) return w.WEECHAT_RC_OK diff --git a/python/chanop.py b/python/chanop.py index 848837e3..697782e0 100644 --- a/python/chanop.py +++ b/python/chanop.py @@ -194,6 +194,22 @@ # # # History: +# +# +# 2023-02-05 +# version 0.3.5: replace command /VERSION by /version +# (compatibility with WeeChat 3.9) +# +# 2021-05-02 +# version 0.3.4: add compatibility with WeeChat >= 3.2 (XDG directories) +# +# 2020-10-18 +# version 0.3.3: make script compatible with Python 3 only +# (drop Python 2 compatibility) +# +# 2020-06-21 +# version 0.3.2: make call to bar_new compatible with WeeChat >= 2.9 +# # 2013-05-24 # version 0.3.1: bug fixes # * fix exceptions while fetching bans with /mode @@ -280,7 +296,7 @@ SCRIPT_NAME = "chanop" SCRIPT_AUTHOR = "Elián Hanisch " -SCRIPT_VERSION = "0.3.1" +SCRIPT_VERSION = "0.3.5" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Helper script for IRC Channel Operators" @@ -306,7 +322,6 @@ print("Get WeeChat now at: http://www.weechat.org/") import_ok = False -import os import re import time import string @@ -314,7 +329,7 @@ from collections import defaultdict from shelve import DbfilenameShelf as Shelf -chars = string.maketrans('', '') +chars = str.maketrans('', '') # ----------------------------------------------------------------------------- # Messages @@ -576,9 +591,9 @@ def function(*args, **kwargs): def callback(method): """This function will take a bound method or function and make it a callback.""" # try to create a descriptive and unique name. - func = method.func_name + func = method.__name__ try: - im_self = method.im_self + im_self = method.__self__ try: inst = im_self.__name__ except AttributeError: @@ -660,11 +675,11 @@ def _flagsAsString(self, n): def __iter__(self): def generator(): - while self.next(): + while next(self): yield self return generator() - def next(self): + def __next__(self): self.cursor = weechat.infolist_next(self.pointer) return self.cursor @@ -767,10 +782,19 @@ def new(self): assert not self._pointer, "Bar %s already created" % self.name pointer = weechat.bar_search(self.name) if not pointer: - pointer = weechat.bar_new(self.name, boolDict[self.hidden], '0', 'window', - 'active', 'bottom', 'horizontal', 'vertical', - '0', '1', 'default', 'cyan', 'blue', 'off', - self._items) + version = int(weechat.info_get('version_number', '')) or 0 + if version >= 0x02090000: + pointer = weechat.bar_new( + self.name, boolDict[self.hidden], '0', 'window', + 'active', 'bottom', 'horizontal', 'vertical', + '0', '1', 'default', 'cyan', 'blue', 'blue', 'off', + self._items) + else: + pointer = weechat.bar_new( + self.name, boolDict[self.hidden], '0', 'window', + 'active', 'bottom', 'horizontal', 'vertical', + '0', '1', 'default', 'cyan', 'blue', 'off', + self._items) if not pointer: raise Exception("bar_new failed: %s %s" % (SCRIPT_NAME, self.name)) @@ -1052,7 +1076,7 @@ def __init__(self, buffer): def checkOp(self): infolist = nick_infolist(self.server, self.channel) - while infolist.next(): + while next(infolist): if infolist['name'] == self.nick: return '@' in infolist['prefixes'] return False @@ -1141,7 +1165,7 @@ def __repr__(self): # ----------------------------------------------------------------------------- # User/Mask classes -_rfc1459trans = string.maketrans(string.ascii_uppercase + r'\[]', +_rfc1459trans = str.maketrans(string.ascii_uppercase + r'\[]', string.ascii_lowercase + r'|{}') def IRClower(s): return s.translate(_rfc1459trans) @@ -1167,7 +1191,7 @@ class CaseInsensibleDict(dict): key = staticmethod(caseInsensibleKey) def __init__(self, **kwargs): - for k, v in kwargs.items(): + for k, v in list(kwargs.items()): self[k] = v def __setitem__(self, k, v): @@ -1192,14 +1216,14 @@ class CaseInsensibleSet(set): normalize = staticmethod(caseInsensibleKey) def __init__(self, iterable=()): - iterable = map(self.normalize, iterable) + iterable = list(map(self.normalize, iterable)) set.__init__(self, iterable) def __contains__(self, v): return set.__contains__(self, self.normalize(v)) def update(self, L): - set.update(self, map(self.normalize, L)) + set.update(self, list(map(self.normalize, L))) def add(self, v): set.add(self, self.normalize(v)) @@ -1218,7 +1242,7 @@ def __updateFromConfig(self): self._updated = True infolist = Infolist('option', 'plugins.var.python.%s.watchlist.*' %SCRIPT_NAME) n = len('python.%s.watchlist.' %SCRIPT_NAME) - while infolist.next(): + while next(infolist): name = infolist['option_name'] value = infolist['value'] server = name[n:] @@ -1239,11 +1263,11 @@ def getChannels(self, server, item=None): return [ chan for serv, chan in self if serv == server ] def purge(self): - for key in self.keys(): + for key in list(self.keys()): if key not in chanopChannels: debug('removing %s mask list, not in watchlist.', key) del self[key] - for data in self.values(): + for data in list(self.values()): data.purge() # ----------------------------------------------------------------------------- @@ -1306,7 +1330,7 @@ def add(self, mask, **kwargs): if mask in self: # mask exists, update it ban = self[mask] - for attr, value in kwargs.items(): + for attr, value in list(kwargs.items()): if value and not getattr(ban, attr): setattr(ban, attr, value) else: @@ -1324,7 +1348,7 @@ def search(self, pattern, reverseMatch=False): if reverseMatch: L = [ mask for mask in self if hostmask_match(mask, pattern) ] else: - L = pattern_match_list(pattern, self.keys()) + L = pattern_match_list(pattern, list(self.keys())) return L def purge(self): @@ -1353,7 +1377,10 @@ def remove(self, server, channel, mask=None):#, hostmask=None): class ChanopCache(Shelf): def __init__(self, filename): - path = os.path.join(weechat.info_get('weechat_dir', ''), filename) + options = { + 'directory': 'data', + } + path = weechat.string_eval_path_home('%%h/%s' % filename, {}, {}, options) Shelf.__init__(self, path, writeback=True) class ModeCache(ChanopCache): @@ -1364,8 +1391,8 @@ def __init__(self, filename): self.map = CaseInsensibleDict() # reset all sync timers - for cache in self.values(): - for masklist in cache.values(): + for cache in list(self.values()): + for masklist in list(cache.values()): masklist.synced = 0 def registerMode(self, mode, *args): @@ -1394,7 +1421,7 @@ def remove(self, server, channel, mode, mask): self[mode].remove(server, channel, mask) def purge(self): - for cache in self.values(): + for cache in list(self.values()): cache.purge() class MaskSync(object): @@ -1522,7 +1549,7 @@ def _endCallback(self, data, modifier, modifier_data, string): if (server, channel) in maskCache: masklist = maskCache[server, channel] banmasks = [ L[0] for L in self._maskbuffer[server, channel] ] - for mask in masklist.keys(): + for mask in list(masklist.keys()): if mask not in banmasks: del masklist[mask] @@ -1546,7 +1573,7 @@ def _endCallback(self, data, modifier, modifier_data, string): next = self.queue[0] self._fetch(*next) else: - assert not self._maskbuffer, "mask buffer not empty: %s" % self._maskbuffer.keys() + assert not self._maskbuffer, "mask buffer not empty: %s" % list(self._maskbuffer.keys()) self._hide_msg = False return string @@ -1599,7 +1626,7 @@ def getHostmask(self, nick): def purge(self): """Purge old nicks""" n = now() - for nick, user in self.items(): + for nick, user in list(self.items()): if user._channels < 1 and (n - user.seen) > self._purge_time: #debug('purging old user: %s' % nick) del self[nick] @@ -1635,7 +1662,7 @@ def values(self): def hostmasks(self, sorted=False, all=False): if sorted: - users = self.values() + users = list(self.values()) else: users = ServerUserList.values(self) if all: @@ -1662,7 +1689,7 @@ def getHostmask(self, nick): def purge(self): """Purge old nicks""" n = now() - for nick, user in self._purge_list.items(): + for nick, user in list(self._purge_list.items()): if (n - user.seen) > self._purge_time: #debug('%s %s: forgeting about %s', self.server, self.channel, nick) user._channels -= 1 @@ -1688,7 +1715,7 @@ def generateCache(self, server, channel): #debug('invalid buffer') return users - while infolist.next(): + while next(infolist): nick = infolist['name'] host = infolist['host'] if host: @@ -1730,7 +1757,7 @@ def __delitem__(self, k): # when we delete a channel, we need to reduce user._channels count # so they can be purged later. #debug('forgeting about %s', k) - for user in self[k].values(): + for user in list(self[k].values()): user._channels -= 1 ServerChannelDict.__delitem__(self, k) @@ -1787,7 +1814,7 @@ def _endWhoCallback(self, data, modifier, modifier_data, string): def purge(self): ServerChannelDict.purge(self) - for cache in self.servercache.values(): + for cache in list(self.servercache.values()): cache.purge() userCache = UserCache() @@ -1829,13 +1856,13 @@ def nick_infolist(self): def has_op(self, nick): nicks = self.nick_infolist() - while nicks.next(): + while next(nicks): if nicks['name'] == nick: return '@' in nicks['prefixes'] def has_voice(self, nick): nicks = self.nick_infolist() - while nicks.next(): + while next(nicks): if nicks['name'] == nick: return '+' in nicks['prefixes'] @@ -2440,7 +2467,7 @@ def execute(self): if masklist: mask_count = len(masklist) self.prnt('\n%s[%s %s]' %(color_channel, key[0], key[1])) - masks = [ m for m in masklist.values() ] + masks = [ m for m in list(masklist.values()) ] masks.sort(key=lambda x: x.date) for ban in masks: op = self.server @@ -2478,7 +2505,7 @@ def decorator(data, signal, signal_data): hostmask = signal_data[1:signal_data.find(' ')] #debug('%s %s', signal, signal_data) return f(server, channel, nick, hostmask, signal_data) - decorator.func_name = f.func_name + decorator.__name__ = f.__name__ return decorator def signal_parse_no_channel(f): @@ -2492,7 +2519,7 @@ def decorator(data, signal, signal_data): #debug('%s %s', signal, signal_data) return f(server, channels, nick, hostmask, signal_data) return WEECHAT_RC_OK - decorator.func_name = f.func_name + decorator.__name__ = f.__name__ return decorator isupport = {} @@ -2516,7 +2543,7 @@ def get_isupport_value(server, feature): if '/VERSION' in isupport[server]: return '' buffer = weechat.buffer_search('irc', 'server.%s' %server) - weechat_command(buffer, '/VERSION') + weechat_command(buffer, '/version') isupport[server]['/VERSION'] = True return v @@ -2601,7 +2628,7 @@ def mode_cb(server, channel, nick, opHostmask, signal_data): # check if channel is in watchlist key = (server, channel) allkeys = CaseInsensibleSet() - for maskCache in modeCache.values(): + for maskCache in list(modeCache.values()): allkeys.update(maskCache) if key not in allkeys and key not in chanopChannels: # from a channel we're not tracking @@ -2619,7 +2646,7 @@ def mode_cb(server, channel, nick, opHostmask, signal_data): args = args.split() # user channel mode, such as +v or +o, get only the letters and not the prefixes - usermodes = ''.join(map(lambda c: c.isalpha() and c or '', prefix)) + usermodes = ''.join([c.isalpha() and c or '' for c in prefix]) chanmodes = chanmodes.split(',') # modes not supported by script, like +e +I notsupported = chanmodes[0].translate(chars, servermodes) @@ -2822,7 +2849,7 @@ def cmpl_unban(masklist): return elif not masklist: return - for mask in masklist.keys(): + for mask in list(masklist.keys()): #debug('unban mask: %s', mask) weechat.hook_completion_list_add(completion, mask, 0, weechat.WEECHAT_LIST_POS_END) @@ -3132,7 +3159,7 @@ def unload_chanop(): error('WeeChat < 0.3.4: using irc_nick infolist workaround.') Infolist._use_flags = True - for opt, val in settings.items(): + for opt, val in list(settings.items()): if not weechat.config_is_set_plugin(opt): weechat.config_set_plugin(opt, val) @@ -3146,7 +3173,7 @@ def unload_chanop(): prefix = 'python.%s.chanmask' % SCRIPT_NAME infolist = Infolist('option', 'plugins.var.%s.*' % prefix) n = len(prefix) - while infolist.next(): + while next(infolist): option = infolist['option_name'][n + 1:] server, channel, mode, mask = option.split('.', 3) if mode in modeCache: diff --git a/python/chanotify.py b/python/chanotify.py new file mode 100644 index 00000000..3bb3f619 --- /dev/null +++ b/python/chanotify.py @@ -0,0 +1,118 @@ +# Project: chanotify +# Description: A library that call notify-send when message is received +# on specific server or channel +# Author: manzerbredes +# License: GPL3 +# +# 0.1.0 +# First version, please ask for feature or bugs on my email ! + +import weechat as weechat + +NAME="chanotify" +VERSION="0.1.0" +LICENCE="GPL3" +AUTHOR="manzerbredes" +DESCRIPTION="Call notify-send command when receive a message on a specific server and channel." +HOMEPAGE="http://people.rennes.inria.fr/Loic.Guegan" +CONFIG= { + "filters": ("*:*", "List of : separated by comma that will where chanotify will notify. \ + Note that or can be * to match every server or channel."), + "status": ("on", "On/Off chanotify") +} +# Convenient variables +CHANNEL_BYPASS=list() # Contain channels name that will be automatically notified +SERVER_BYPASS=list() # Contain servers name that will be automatically notifier +ALL_BYPASS=False # If true every server and channel will be notified +FILTERS=dict() # Contain list of channels associated with each servers + +# Parse filters parameters +def parse_filters(filters): + global CHANNEL_BYPASS + global SERVER_BYPASS + global ALL_BYPASS + global FILTERS + + elts=filters.split(",") + for elt in elts: + server, channel=elt.split(":") + if server==channel and server=="*": + ALL_BYPASS=True + elif server == "*": + CHANNEL_BYPASS.append(channel) + elif channel == "*": + SERVER_BYPASS.append(server) + else: + if server in FILTERS: + FILTERS[server].append(channel) + else: + FILTERS[server]=[channel] + +def isNotifiable(server,channel): + global CHANNEL_BYPASS + global SERVER_BYPASS + global ALL_BYPASS + global FILTERS + global CONFIG + + if CONFIG["status"][0]=="on": + if (channel in CHANNEL_BYPASS) or (server in SERVER_BYPASS) or ALL_BYPASS: + return True + else: + if server in FILTERS: + return (channel in FILTERS[server]) + return False + +def on_receive(data, signal, signal_data): + # Fetch server, msg and buffer + server = signal.split(",")[0] + msg = weechat.info_get_hashtable("irc_message_parse", {"message": signal_data}) + buffer = weechat.info_get("irc_buffer", "%s,%s" % (server, msg["channel"])) + + # Notify if we get the buffer + if buffer and isNotifiable(server,msg["channel"]): + notify_title="On "+msg["channel"] + notify_msg=msg["nick"]+"> "+msg["text"] + weechat.hook_process_hashtable("notify-send", + { "arg1": "-i", "arg2": "weechat", + "arg3": "-a", "arg4": "WeeChat", + "arg5": notify_title, "arg6": notify_msg}, + 20000, "", "") + return weechat.WEECHAT_RC_OK + +# Load the configuration +def update_config(data, option, value): + global CONFIG + option=option.split(".")[-1] + if option != "filters": + CONFIG[option]=(value,CONFIG[option][1]) + else: # Reset existing filter configuration and set them + CHANNEL_BYPASS=list() + SERVER_BYPASS=list() + ALL_BYPASS=False + FILTERS=dict() + parse_filters(CONFIG["filters"][0]) + + return weechat.WEECHAT_RC_OK + +# Load the script +if __name__ == "__main__": + # Register plugin + weechat.register(NAME, AUTHOR, VERSION, LICENCE, DESCRIPTION, "", "UTF-8") + + # Load or set configuration + for (option,value) in CONFIG.items(): + c = weechat.config_get_plugin(option) + if len(c) == 0: # Set + weechat.config_set_plugin(option, value[0]) + else: # Load + CONFIG[option]=(c,CONFIG[option][1]) + + # Watch config changes + weechat.hook_config("*%s.*"%NAME, "update_config", "") + + # Parse filters + parse_filters(CONFIG["filters"][0]) + + # Watch incoming message + weechat.hook_signal("*,irc_in2_privmsg", "on_receive", "") diff --git a/python/chanstat.py b/python/chanstat.py deleted file mode 100644 index 1aa75674..00000000 --- a/python/chanstat.py +++ /dev/null @@ -1,681 +0,0 @@ -# -*- coding: utf-8 -*- -### -# Copyright (c) 2010 by Elián Hanisch -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -### - -### -# Shows highest and lowest user count for joined channels, -# and an average (for a period of a month) -# -# -# Commands: -# * /chanstat -# Prints current channel stats, see /help chanstat -# -# -# Settings: -# * plugins.var.python.chanstat.path: -# path where to store stat files, deault '%h/chanstat' -# -# * plugins.var.python.chanstat.averge_period: -# Period of time for calculate the average stats. This means the avegare will be calculated with -# the users present in the last x days. Default is 30 days. -# -# * plugins.var.python.chanstat.show_peaks: -# If 'on' it will display a message when there's a user peak in any channel. -# Valid values: on, off -# -# * plugins.var.python.chanstat.show_lows: -# If 'on' it will display a message when there's a user low in any channel. -# Valid values: on, off -# -# -# History: -# 2010-06-08 -# version 0.1: initial release. -# -### - -SCRIPT_NAME = "chanstat" -SCRIPT_AUTHOR = "Elián Hanisch " -SCRIPT_VERSION = "0.1" -SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Channel statistics" - -try: - import weechat - WEECHAT_RC_OK = weechat.WEECHAT_RC_OK - import_ok = True -except ImportError: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://weechat.flashtux.org/" - import_ok = False - -import time -now = lambda : int(time.time()) - -time_hour = 3600 -time_day = 86400 -time_year = 31536000 - -### messages -def debug(s, args=(), prefix='', name_suffix='debug', level=1): - """Debug msg""" - l = weechat.config_get_plugin('debug') - if not (l and int(l) >= level): return - buffer_name = '%s_%s' %(SCRIPT_NAME, name_suffix) - buffer = weechat.buffer_search('python', buffer_name) - if not buffer: - buffer = weechat.buffer_new(buffer_name, '', '', '', '') - weechat.buffer_set(buffer, 'nicklist', '0') - weechat.buffer_set(buffer, 'localvar_set_no_log', '1') - weechat.prnt(buffer, '%s\t%s' %(prefix, s %args)) - -def error(s, prefix=SCRIPT_NAME, buffer=''): - """Error msg""" - prefix = prefix or script_nick - weechat.prnt(buffer, '%s%s %s' %(weechat.prefix('error'), prefix, s)) - -def say(s, prefix=None, buffer=''): - """Normal msg""" - prefix = prefix or script_nick - weechat.prnt(buffer, '%s\t%s' %(prefix, s)) - -### config and value validation -boolDict = {'on':True, 'off':False} -def get_config_boolean(config): - value = weechat.config_get_plugin(config) - try: - return boolDict[value] - except KeyError: - default = settings[config] - error("Error while fetching config '%s'. Using default value '%s'." %(config, default)) - error("'%s' is invalid, allowed: 'on', 'off'" %value) - return boolDict[default] - -def get_config_int(config): - value = weechat.config_get_plugin(config) - try: - return int(value) - except ValueError: - default = settings[config] - error("Error while fetching config '%s'. Using default value '%s'." %(config, default)) - error("'%s' is not a number." %value) - return int(default) - -def get_dir(filename): - import os - basedir = weechat.config_get_plugin('path').replace('%h', weechat.info_get('weechat_dir', '')) - if not os.path.isdir(basedir): - os.makedirs(basedir) - return os.path.join(basedir, filename.lower()) - - -class CaseInsensibleString(str): - def __init__(self, s=''): - self.lowered = s.lower() - - def __eq__(self, s): - try: - return self.lowered == s.lower() - except: - return False - - def __ne__(self, s): - return not self == s - - def __hash__(self): - return hash(self.lowered) - -def caseInsensibleKey(k): - if isinstance(k, str): - return CaseInsensibleString(k) - elif isinstance(k, tuple): - return tuple([ caseInsensibleKey(v) for v in k ]) - return k - -class CaseInsensibleDict(dict): - key = staticmethod(caseInsensibleKey) - - def __setitem__(self, k, v): - dict.__setitem__(self, self.key(k), v) - - def __getitem__(self, k): - return dict.__getitem__(self, self.key(k)) - - def __delitem__(self, k): - dict.__delitem__(self, self.key(k)) - - def __contains__(self, k): - return dict.__contains__(self, self.key(k)) - - -class Channel(object): - def __init__(self, max=None, min=None, max_date=None, min_date=None, avrg_date=None, - avrg_period=None, average=None, count=0): - if not max: - max = count - if not min: - min = 0 - if not average: - average = float(count) - if not max_date: - max_date = now() - if not min_date: - min_date = now() - if not avrg_date: - avrg_date = now() - if not avrg_period: - avrg_period = 0 - self.max = max - self.min = min - self.max_date = max_date - self.min_date = min_date - self.avrg_date = avrg_date - self.avrg_period = avrg_period - self.average = average - - def __iter__(self): - return iter((self.max, self.min, self.max_date, self.min_date, self.avrg_date, - self.avrg_period, self.average)) - - def __str__(self): - return 'Channel(max=%s, average=%s, min_delta=%s)' %(self.max, self.average, self.min) - - -class StatLog(object): - def __init__(self): - self.writers = CaseInsensibleDict() - - @staticmethod - def make_log(key): - return get_dir('%s_%s.cvs' %key) - - def log(self, key, *args): - if key in self.writers: - writer = self.writers[key] - else: - import csv - filename = self.make_log(key) - writer = csv.writer(open(filename, 'ab')) - self.writers[key] = writer - - writer.writerow(args) - - def get_reader(self, key): - if key in self.writers: - del self.writers[key] - import csv - return csv.reader(open(self.make_log(key))) - - def close(self): - self.writers = {} - - -class ChanStatDB(CaseInsensibleDict): - def __init__(self): - self.logger = StatLog() - - def __setitem__(self, key, value): - if not value: - return - debug(' ** stats update for %s', args=key[1]) - _now = now() - avrg = 0 - if key in self: - chan = self[key] - if value > chan.max: - debug('PEAK, %s: %s', args=(key[1], value)) - chan.max = value - new_channel_peak(key, value, chan.max_date) - chan.max_date = _now - elif (chan.max - value) > chan.min: - # we save the difference between max and min rather the min absolute value, because - # the minimum min value for a channel would be 1, and that's isn't interesting. - min_delta = chan.max - value - debug('LOW, %s: %s', args=(key[1], value)) - chan.min = min_delta - new_channel_low(key, value, chan.min_date) - chan.min_date = _now - # calculate average aproximation - diff = _now - chan.avrg_date - #period = 30 * time_day - period = get_config_int('average_period') * time_day - if not period: - period = time_day - avrg_period = chan.avrg_period - avrg_period += diff - if avrg_period > period: - avrg_period = period - if diff > avrg_period // 1000 and diff > 600: - # calc average after 1000th part of the period (10 min minimum) - max = period // 100 - if diff > max: - # too much time have passed since last check, average will be skewed by current - # user count, so we reduce the average period. - excess = diff - max - debug('avrg period correction %s e:%.4f%%', args=(key[1], - excess*100.0/avrg_period)) - diff = max - avrg_period -= excess - if avrg_period < diff: - avrg_period = diff - avrg = chan.average - avrg = (avrg * (avrg_period - diff) + value * diff) / avrg_period - chan.avrg_date = _now - chan.avrg_period = avrg_period - # make sure avrg is between max and 1 - if avrg > chan.max: - avrg = chan.max - elif avrg < 1: - avrg = 1 - debug('avrg %s %.2f → %.2f (%.4f%% %.4f)', args=(key[1], chan.average, avrg, - diff*100.0/avrg_period, avrg - chan.average)) - chan.average = avrg - else: - CaseInsensibleDict.__setitem__(self, key, Channel(count=value)) - - #if avrg: - # self.logger.log(key, _now, value, avrg) - #else: - # self.logger.log(key, _now, value) - - def initchan(self, key, *args): - CaseInsensibleDict.__setitem__(self, key, Channel(*args)) - - def iterchan(self): - def generator(): - for key in self.keys(): - chan = self[key] - row = list(key) - row.extend(chan) - yield row - - return generator() - - def keys(self): - """Returns keys sorted""" - L = dict.keys(self) - L.sort() - return L - - def close(self): - self.logger.close() - -channel_stats = ChanStatDB() - - -def write_database(): - import csv - filename = get_dir('peak_data.csv') - try: - writer = csv.writer(open(filename, 'wb')) - writer.writerows(channel_stats.iterchan()) - except IOError: - error('Failed to write chanstat database in %s' %file) - -def load_database(): - import csv - filename = get_dir('peak_data.csv') - try: - reader = csv.reader(open(filename, 'rb')) - except IOError: - return - channel_stats.clear() - for row in reader: - key = tuple(row[0:2]) - values = row[2:-1] - values = map(int, values) - average = row[-1] - average = float(average) - values.append(average) - channel_stats.initchan(key, *values) - -def update_user_count(server=None, channel=None): - if isinstance(channel, str): - channel = set((channel, )) - elif channel: - channel = set(channel) - - def update_channel(server, channel=None): - channel_infolist = weechat.infolist_get('irc_channel', '', server) - while weechat.infolist_next(channel_infolist): - _channel = weechat.infolist_string(channel_infolist, 'name') - if channel: - _channel = caseInsensibleKey(_channel) - if _channel not in channel: - continue - channel_stats[server, _channel] = weechat.infolist_integer(channel_infolist, 'nicks_count') - weechat.infolist_free(channel_infolist) - - if not server: - server_infolist = weechat.infolist_get('irc_server', '', '') - while weechat.infolist_next(server_infolist): - server = weechat.infolist_string(server_infolist, 'name') - update_channel(server) - weechat.infolist_free(server_infolist) - else: - update_channel(server, channel) - -def time_elapsed(elapsed, ret=None, level=2): - if ret is None: - ret = [] - - if not elapsed: - return '' - - if elapsed > time_year: - years, elapsed = elapsed // time_year, elapsed % time_year - ret.append('%s%s' %(years, 'y')) - elif elapsed > time_day: - days, elapsed = elapsed // time_day, elapsed % time_day - ret.append('%s%s' %(days, 'd')) - elif elapsed > time_hour: - hours, elapsed = elapsed // time_hour, elapsed % time_hour - ret.append('%s%s' %(hours, 'h')) - elif elapsed > 60: - mins, elapsed = elapsed // 60, elapsed % 60 - ret.append('%s%s' %(mins, 'm')) - else: - secs, elapsed = elapsed, 0 - ret.append('%s%s' %(secs, 's')) - - if len(ret) >= level or not elapsed: - return ' '.join(ret) - - ret = time_elapsed(elapsed, ret, level) - return ret - -channel_peak_hooks = CaseInsensibleDict() -msg_queue_timeout = 15 -def new_channel_peak(key, count, time=0): - if not get_config_boolean('show_peaks'): - return - if key in channel_peak_hooks: - weechat.unhook(channel_peak_hooks[key][0]) - time = channel_peak_hooks[key][1] - else: - time -= 60 * msg_queue_timeout # add delay in showing the msg - - if time: - elapsed = time_elapsed(now() - time) - if elapsed: - elapsed = '(last peak was %s ago)' %elapsed - else: - elapsed = '' - - # hook it for show msg 10 min later - channel_peak_hooks[key] = (weechat.hook_timer(60000 * msg_queue_timeout, 0, 1, 'new_channel_peak_cb', - '%s,%s,New user peak: %s users %s' %(key[0], key[1], count, elapsed)), time) - -def new_channel_peak_cb(data, count): - debug(data) - server, channel, s = data.split(',', 2) - buffer = weechat.info_get('irc_buffer', '%s,%s' %(server, channel)) - if buffer: - say('%s%s' %(color_peak, s), buffer=buffer) - else: - debug('XX falied to get buffer: %s.%s', args=(server, channel)) - del channel_peak_hooks[server, channel] - return WEECHAT_RC_OK - -channel_low_hooks = CaseInsensibleDict() -def new_channel_low(key, count, time=0): - if not get_config_boolean('show_lows'): - return - if key in channel_low_hooks: - weechat.unhook(channel_low_hooks[key][0]) - time = channel_low_hooks[key][1] - else: - time -= 60 * msg_queue_timeout # add delay in showing the msg - - if time: - elapsed = time_elapsed(now() - time) - if elapsed: - elapsed = '(last low was %s ago)' %elapsed - else: - elapsed = '' - - # hook it for show msg 10 min later - channel_low_hooks[key] = (weechat.hook_timer(60000 * msg_queue_timeout, 0, 1, 'new_channel_low_cb', - '%s,%s,New user low: %s %s' %(key[0], key[1], count, elapsed)), time) - -def new_channel_low_cb(data, count): - debug(data) - server, channel, s = data.split(',', 2) - buffer = weechat.buffer_search('irc', '%s.%s' %(server, channel)) - if buffer: - say('%s%s' %(color_low, s), buffer=buffer) - else: - debug('XX falied to get buffer: %s.%s', args=(server, channel)) - del channel_low_hooks[server, channel] - return WEECHAT_RC_OK - -# chanstat command -def chanstat_cmd(data, buffer, args): - if args == '--save': - write_database() - channel_stats.close() - say('Channel statistics saved.') - return WEECHAT_RC_OK - elif args == '--load': - load_database() - say('Channel statistics loaded.') - return WEECHAT_RC_OK -# elif args == '--print': -# prnt = weechat.command - - channel = weechat.buffer_get_string(buffer, 'localvar_channel') - server = weechat.buffer_get_string(buffer, 'localvar_server') - key = (server, channel) - - update_user_count(server, channel) - # clear any update in queue - if key in update_channel_hook: - weechat.unhook(update_channel_hook[key][0]) - del update_channel_hook[key] - - try: - chan = channel_stats[server, channel] - _now = now() - peak_time = time_elapsed(_now - chan.max_date) - low_time = time_elapsed(_now - chan.min_date) - if peak_time: - peak_time = ' (%s ago)' %peak_time - if low_time: - low_time = ' (%s ago)' %low_time - if chan.avrg_period > time_hour: - average = ' average: %s%.2f%s users (%s period)' %(color_avg, chan.average, - color_reset, time_elapsed(chan.avrg_period, level=1)) - else: - average = ' (no average yet)' - say('%s%s%s user peak: %s%s%s%s lowest: %s%s%s%s%s' %( - color_bold, channel, color_reset, - color_peak, chan.max, color_reset, - peak_time, - color_low, chan.max - chan.min, color_reset, - low_time, average), buffer=buffer) - - # clear any new peak or low msg in queue - if key in channel_peak_hooks: - weechat.unhook(channel_peak_hooks[key][0]) - del channel_peak_hooks[key] - if key in channel_low_hooks: - weechat.unhook(channel_low_hooks[key][0]) - del channel_low_hooks[key] - except KeyError: - say('No statistics available.', buffer=buffer) - - return WEECHAT_RC_OK - -def cmd_debug(data, buffer, args): - dbg = lambda s, a: debug(s, args=a, name_suffix='dump') - dbg('\nStats DB: %s', len(channel_stats)) - for key in channel_stats.keys(): - dbg('%s %s - %s', (key[0], key[1], channel_stats[key])) - - dbg('\nUsers: %s', len(domain_list)) - return WEECHAT_RC_OK - -class Queue(dict): - """User queue, for ignore many joins from same host (clones)""" - def __contains__(self, key): - self.clear() - return dict.__contains__(self, key) - - def clear(self): - _now = now() - for key, time in self.items(): - if (_now - time) > 60: - #debug('clearing domain %s from list (count: %s)' %(key, len(self))) - del self[key] - -domain_list = Queue() - - -# signal callbacks -def join_cb(data, signal, signal_data): - #debug('%s %s\n%s' %(data, signal, signal_data), 'SIGNAL') - global netsplit - if netsplit: - debug('ignoring, netsplit') - if (now() - netsplit) > 30*60: # wait 30 min - netsplit = 0 - return WEECHAT_RC_OK - - server = signal[:signal.find(',')] - signal_data = signal_data.split() - channel = signal_data[2].strip(':') - host = signal_data[0].strip(':') - domain = '%s,%s' %(channel, host[host.find('@')+1:]) - if domain in domain_list: - #debug('ignoring %s', args=domain) - return WEECHAT_RC_OK - else: - domain_list[domain] = now() - debug(' -- ping %s (%s)', args=(channel,signal[-4:])) - add_update_user_hook(server, channel) - return WEECHAT_RC_OK - -netsplit = 0 -def quit_cb(data, signal, signal_data): - #debug('%s %s\n%s' %(data, signal, signal_data), 'SIGNAL') - global netsplit - if netsplit: - return WEECHAT_RC_OK - quit_msg = signal_data[signal_data.rfind(':')+1:] - if quit_msg_is_split(quit_msg): - netsplit = now() - for hook, when in update_channel_hook.itervalues(): - weechat.unhook(hook) - update_channel_hook.clear() - debug('NETSPLIT') - - return WEECHAT_RC_OK - -def quit_msg_is_split(s): - #if 'peer' in s: return True - if s.count(' ') == 1: - sp = s.find(' ') - d1 = s.find('.') - d2 = s.rfind('.') - if 0 < d1 and 4 < d2 and d1 < sp < d2 and d2 + 1 < len(s): - return True - return False - -update_channel_hook = CaseInsensibleDict() -update_queue_timeout = 120 -def add_update_user_hook(server, channel): - key = (server, channel) - if key in update_channel_hook: - hook, when = update_channel_hook[key] - if (now() - when) > update_queue_timeout//2: - debug(' vv rescheduling %s', args=key[1]) - weechat.unhook(hook) - else: - return - else: - debug(' >> scheduling %s', args=key[1]) - - # we schedule the channel check for later so we can filter quick joins/parts and netsplits - update_channel_hook[key] = (weechat.hook_timer(update_queue_timeout * 1000, 0, 1, 'update_user_count_cb', - ','.join(key)), now()) - -def update_user_count_cb(data, count): - server, channel = data.split(',', 1) - channels = [ chan for serv, chan in update_channel_hook if server == serv ] - if channels: - update_user_count(server, channels) - for chan in channels: - hook, when = update_channel_hook[server, chan] - weechat.unhook(hook) - del update_channel_hook[server, chan] - return WEECHAT_RC_OK - -def script_load(): - load_database() - update_user_count() - -def script_unload(): - write_database() - channel_stats.close() - return WEECHAT_RC_OK - -# default settings -settings = { - 'path' :'%h/chanstat', - 'average_period':'30', - 'show_peaks' :'on', - 'show_lows' :'on', - } - -if __name__ == '__main__' and import_ok and \ - weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, 'script_unload', ''): - - # colors - color_delimiter = weechat.color('chat_delimiters') - color_chat_nick = weechat.color('chat_nick') - color_reset = weechat.color('reset') - color_peak = weechat.color('green') - color_low = weechat.color('red') - color_avg = weechat.color('brown') - color_bold = weechat.color('white') - - # pretty [chanop] - script_nick = '%s[%s%s%s]%s' %(color_delimiter, color_chat_nick, SCRIPT_NAME, color_delimiter, - color_reset) - - for opt, val in settings.iteritems(): - if not weechat.config_is_set_plugin(opt): - weechat.config_set_plugin(opt, val) - - script_load() - - weechat.hook_signal('*,irc_in2_join', 'join_cb', '') - weechat.hook_signal('*,irc_in2_part', 'join_cb', '') - weechat.hook_signal('*,irc_in2_quit', 'quit_cb', '') - - weechat.hook_command('chanstat', "Display channel's statistics.", '[--save | --load]', - "Displays channel peak, lowest and average users for current channel.\n" - " --save: forces saving the stats database.\n" - " --load: forces loading the stats database (Overwriting actual values).\n", - #" --print: sends /chanstat output to the current channel.", - '--save|--load', 'chanstat_cmd', '') - - weechat.hook_command('chanstat_debug', '', '', '', '', 'cmd_debug', '') - -# vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100: diff --git a/python/clemenshow.py b/python/clemenshow.py index de79bf45..371d4b64 100644 --- a/python/clemenshow.py +++ b/python/clemenshow.py @@ -8,7 +8,7 @@ """ SCRIPT_NAME = "clemenshow" SCRIPT_AUTHOR = "Leigh MacDonald " -SCRIPT_VERSION = "1.0" +SCRIPT_VERSION = "1.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Clementine now playing script" SCRIPT_COMMAND = "np" @@ -18,22 +18,24 @@ try: import weechat + IMPORT_OK = True except ImportError: - print "This script must be run under WeeChat 3.4 or better." - print "Get WeeChat now at: http://www.weechat.org/" - sys.exit() + print("This script must be run under WeeChat 3.4 or better.") + print("Get WeeChat now at: https://weechat.org/") + IMPORT_OK = False + try: from dbus import Bus, DBusException except ImportError: - print "Please install python-dbus" - sys.exit() + print("Please install python-dbus") + IMPORT_OK = False -bus = Bus(Bus.TYPE_SESSION) def get_type(path): p = path.split(".") return p[len(p)-1].upper() + #@DebugArgs def np_command(data, buffer, args): try: @@ -46,11 +48,14 @@ def np_command(data, buffer, args): int(f['audio-samplerate']))) except DBusException: weechat.prnt(buffer, "Doesnt look like clementine is running, if it is make sure dbus is running") - except Exception, err: + except Exception as err: weechat.prnt(buffer, err) finally: return weechat.WEECHAT_RC_OK -weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "on_shutdown", "") -weechat.hook_command(SCRIPT_COMMAND, SCRIPT_DESC, "", "", "", "np_command", "") -weechat.prnt("", "%s | %s" % (SCRIPT_NAME, SCRIPT_AUTHOR)) + +if __name__ == "__main__" and IMPORT_OK: + weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "") + weechat.hook_command(SCRIPT_COMMAND, SCRIPT_DESC, "", "", "", "np_command", "") + weechat.prnt("", "%s | %s" % (SCRIPT_NAME, SCRIPT_AUTHOR)) + bus = Bus(Bus.TYPE_SESSION) diff --git a/python/clemy.py b/python/clemy.py index ca8eb1cf..0ec05882 100644 --- a/python/clemy.py +++ b/python/clemy.py @@ -22,20 +22,15 @@ # Imports try: import weechat - + IMPORT_OK = True except ImportError: - import sys - - print '\nError: Script must be run under Weechat.\n' - - sys.exit(2) + print('Error: Script must be run under Weechat.') + IMPORT_OK = False import dbus -weechat.register('clemy', "Your mommy's boyfriend", '0.1.1', 'GPLv3', 'Control yo Clementine like boom-blaka!', '', '') - -err_message = '\nSomething silly just happend. Make sure Clementine is running mah dude.' +err_message = '\nSomething silly just happend. Make sure Clementine is running mah dude.' def help(): @@ -187,9 +182,11 @@ def weechat_np(data, buffer, args): return weechat.WEECHAT_RC_OK -weechat.hook_command('clemynp', 'Get/output now playing info', '', '', '', 'weechat_np', '') -weechat.hook_command('clemy', 'Control Clementine', "[play] | [pause] | [next] | [prev] | [stop] | [vol+] | [vol-] | [vol+by ] | [vol-by ] | [playtrack ] | [help]", -""" +if __name__ == "__main__" and IMPORT_OK: + weechat.register('clemy', "Your mommy's boyfriend", '0.1.2', 'GPLv3', 'Control yo Clementine like boom-blaka!', '', '') + weechat.hook_command('clemynp', 'Get/output now playing info', '', '', '', 'weechat_np', '') + weechat.hook_command('clemy', 'Control Clementine', "[play] | [pause] | [next] | [prev] | [stop] | [vol+] | [vol-] | [vol+by ] | [vol-by ] | [playtrack ] | [help]", + """ play: Play song. pause: Pause song. next: Play next song. diff --git a/python/clone_scanner.py b/python/clone_scanner.py index e8205c2a..b4822e85 100644 --- a/python/clone_scanner.py +++ b/python/clone_scanner.py @@ -126,6 +126,9 @@ # * Updated advertise option to include /script install clone_scanner.py # * Updated min version to 0.3.6, though this will prob change in the next version # +### 2024-08-18: Sébastien Helleu: +# * version 1.5: make script compatible with Python 3 +# ## Acknowledgements: # * Sebastien "Flashcode" Helleu, for developing the kick-ass chat/IRC # client WeeChat @@ -170,19 +173,18 @@ # SCRIPT_NAME = "clone_scanner" SCRIPT_AUTHOR = "Filip H.F. 'FiXato' Slagter " -SCRIPT_VERSION = "1.4" +SCRIPT_VERSION = "1.5" SCRIPT_LICENSE = "MIT" SCRIPT_DESC = "A Clone Scanner that can manually scan channels and automatically scans joins for users on the channel with multiple nicknames from the same host." SCRIPT_COMMAND = "clone_scanner" SCRIPT_CLOSE_CB = "cs_close_cb" -import_ok = True - try: import weechat + IMPORT_OK = True except ImportError: - print "This script must be run under WeeChat." - import_ok = False + print("This script must be run under WeeChat.") + IMPORT_OK = False import re cs_buffer = None @@ -262,7 +264,7 @@ def on_join_scan_cb(data, signal, signal_data): return weechat.WEECHAT_RC_OK joined_nick = weechat.info_get("irc_nick_from_host", signal_data) - join_match_data = re.match(':[^!]+!([^@]+@(\S+)) JOIN :?([#&]\S*)', signal_data) + join_match_data = re.match(r':[^!]+!([^@]+@(\S+)) JOIN :?([#&]\S*)', signal_data) parsed_ident_host = join_match_data.group(1).lower() parsed_host = join_match_data.group(2).lower() if OPTIONS["compare_idents"] == "on": @@ -274,7 +276,7 @@ def on_join_scan_cb(data, signal, signal_data): network_chan_name = "%s.%s" % (network, chan_name) chan_buffer = weechat.info_get("irc_buffer", "%s,%s" % (network, chan_name)) if not chan_buffer: - print "No IRC channel buffer found for %s" % network_chan_name + weechat.prnt("", "No IRC channel buffer found for %s" % network_chan_name) return weechat.WEECHAT_RC_OK if OPTIONS["display_join_messages"] == "on": @@ -293,8 +295,8 @@ def on_join_scan_cb(data, signal, signal_data): if clones: key = get_validated_key_from_config("clone_onjoin_alert_key") - filtered_clones = filter(lambda clone: clone['nick'] != joined_nick, clones[hostkey]) - match_strings = map(lambda m: format_from_config(m[key], "colors.onjoin_alert.matches"), filtered_clones) + filtered_clones = [clone for clone in clones[hostkey] if clone['nick'] != joined_nick] + match_strings = [format_from_config(m[key], "colors.onjoin_alert.matches") for m in filtered_clones] join_string = format_from_config(' and ',"colors.onjoin_alert.message") masks = join_string.join(match_strings) @@ -347,7 +349,7 @@ def get_channel_from_buffer_args(buffer, args): if not channel_name: channel_name = weechat.buffer_get_string(buffer, "localvar_channel") - match_data = re.match('\A(irc.)?([^.]+)\.([#&]\S*)\Z', channel_name) + match_data = re.match(r'\A(irc.)?([^.]+)\.([#&]\S*)\Z', channel_name) if match_data: channel_name = match_data.group(3) server_name = match_data.group(2) @@ -360,7 +362,7 @@ def get_clones_for_buffer(infolist_buffer_name, hostname_to_match=None): infolist = weechat.infolist_get("irc_nick", "", infolist_buffer_name) while(weechat.infolist_next(infolist)): ident_hostname = weechat.infolist_string(infolist, "host") - host_matchdata = re.match('([^@]+)@(\S+)', ident_hostname) + host_matchdata = re.match(r'([^@]+)@(\S+)', ident_hostname) if not host_matchdata: continue @@ -388,7 +390,7 @@ def get_clones_for_buffer(infolist_buffer_name, hostname_to_match=None): weechat.infolist_free(infolist) #Select only the results that have more than 1 match for a host - return dict((k, v) for (k, v) in matches.iteritems() if len(v) > 1) + return dict((k, v) for (k, v) in matches.items() if len(v) > 1) def report_clones(clones, scanned_buffer_name, target_buffer=None): # Default to clone_scanner buffer @@ -405,7 +407,7 @@ def report_clones(clones, scanned_buffer_name, target_buffer=None): clone_report_header = format_from_config(clone_report_header, "colors.clone_report.header.message") weechat.prnt(target_buffer, clone_report_header) - for (host, clones) in clones.iteritems(): + for (host, clones) in clones.items(): host_message = "%s %s %s %s" % ( format_from_config(host, "colors.clone_report.subheader.host"), format_from_config("is online from", "colors.clone_report.subheader.message"), @@ -476,7 +478,7 @@ def remove_hooks(): weechat.unhook(hook) hooks = set([]) -if __name__ == "__main__" and import_ok: +if __name__ == "__main__" and IMPORT_OK: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, SCRIPT_CLOSE_CB, ""): version = weechat.info_get("version_number", "") or 0 if int(version) >= 0x00030600: diff --git a/python/cmd_help.py b/python/cmd_help.py index b0de8d9a..679a05cd 100644 --- a/python/cmd_help.py +++ b/python/cmd_help.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2011-2012 Sebastien Helleu +# Copyright (C) 2011-2018 Sébastien Helleu # Copyright (C) 2012 ArZa # # This program is free software; you can redistribute it and/or modify @@ -23,22 +23,25 @@ # # History: # +# 2018-04-10, Sébastien Helleu : +# version 0.5: fix infolist_time for WeeChat >= 2.2 (WeeChat returns a long +# integer instead of a string), fix PEP8 errors # 2012-01-04, ArZa : # version 0.4: settings for right align and space before help -# 2012-01-03, Sebastien Helleu : +# 2012-01-03, Sébastien Helleu : # version 0.3: make script compatible with Python 3.x -# 2011-05-18, Sebastien Helleu : +# 2011-05-18, Sébastien Helleu : # version 0.2: add options for aliases, start on load, list of commands to # ignore; add default value in help of script options -# 2011-05-15, Sebastien Helleu : +# 2011-05-15, Sébastien Helleu : # version 0.1: initial release # -SCRIPT_NAME = 'cmd_help' -SCRIPT_AUTHOR = 'Sebastien Helleu ' -SCRIPT_VERSION = '0.4' +SCRIPT_NAME = 'cmd_help' +SCRIPT_AUTHOR = 'Sébastien Helleu ' +SCRIPT_VERSION = '0.5' SCRIPT_LICENSE = 'GPL3' -SCRIPT_DESC = 'Contextual command line help' +SCRIPT_DESC = 'Contextual command line help' SCRIPT_COMMAND = 'cmd_help' @@ -53,43 +56,116 @@ try: import re + import time except ImportError as message: print('Missing package(s) for %s: %s' % (SCRIPT_NAME, message)) import_ok = False -cmdhelp_hooks = { 'modifier' : '', - 'timer' : '', - 'command_run': '' } +cmdhelp_hooks = { + 'modifier': '', + 'timer': '', + 'command_run': '', +} cmdhelp_option_infolist = '' cmdhelp_option_infolist_fields = {} # script options cmdhelp_settings_default = { - 'display_no_help' : ['on', 'display "No help" when command is not found'], - 'start_on_load' : ['off', 'auto start help when script is loaded'], - 'stop_on_enter' : ['on', 'enter key stop help'], - 'timer' : ['0', 'number of seconds help is displayed (0 = display until help is toggled)'], - 'prefix' : ['[', 'string displayed before help'], - 'suffix' : [']', 'string displayed after help'], - 'format_option' : ['(${white:type}) ${description_nls}', 'format of help for options: free text with identifiers using format: ${name} or ${color:name}: color is a WeeChat color (optional), name is a field of infolist "option"'], - 'max_options' : ['5', 'max number of options displayed in list'], - 'ignore_commands' : ['map,me,die,restart', 'comma-separated list of commands (without leading "/") to ignore'], - 'color_alias' : ['white', 'color for text "Alias"'], - 'color_alias_name' : ['green', 'color for alias name'], - 'color_alias_value': ['green', 'color for alias value'], - 'color_delimiters' : ['lightgreen', 'color for delimiters'], - 'color_no_help' : ['red', 'color for text "No help"'], - 'color_list_count' : ['white', 'color for number of commands/options in list found'], - 'color_list' : ['green', 'color for list of commands/options'], - 'color_arguments' : ['cyan', 'color for command arguments'], - 'color_option_name': ['yellow', 'color for name of option found (by adding "*" to option name)'], - 'color_option_help': ['brown', 'color for help on option'], - 'right_align' : ['off', 'align help to right'], - 'right_padding' : ['15', 'padding to right when aligned to right'], - 'space' : ['2', 'minimum space before help'], + 'display_no_help': [ + 'on', + 'display "No help" when command is not found', + ], + 'start_on_load': [ + 'off', + 'auto start help when script is loaded', + ], + 'stop_on_enter': [ + 'on', + 'enter key stop help', + ], + 'timer': [ + '0', + ('number of seconds help is displayed (0 = display until help is ' + 'toggled)'), + ], + 'prefix': [ + '[', + 'string displayed before help', + ], + 'suffix': [ + ']', + 'string displayed after help', + ], + 'format_option': [ + '(${white:type}) ${description_nls}', + ('format of help for options: free text with identifiers using ' + 'format: ${name} or ${color:name}: color is a WeeChat ' + 'color (optional), name is a field of infolist "option"'), + ], + 'max_options': [ + '5', + 'max number of options displayed in list', + ], + 'ignore_commands': [ + 'map,me,die,restart', + 'comma-separated list of commands (without leading "/") to ignore', + ], + 'color_alias': [ + 'white', + 'color for text "Alias"', + ], + 'color_alias_name': [ + 'green', + 'color for alias name', + ], + 'color_alias_value': [ + 'green', + 'color for alias value', + ], + 'color_delimiters': [ + 'lightgreen', + 'color for delimiters', + ], + 'color_no_help': [ + 'red', + 'color for text "No help"', + ], + 'color_list_count': [ + 'white', + 'color for number of commands/options in list found', + ], + 'color_list': [ + 'green', + 'color for list of commands/options', + ], + 'color_arguments': [ + 'cyan', + 'color for command arguments', + ], + 'color_option_name': [ + 'yellow', + 'color for name of option found (by adding "*" to option name)', + ], + 'color_option_help': [ + 'brown', + 'color for help on option', + ], + 'right_align': [ + 'off', + 'align help to right', + ], + 'right_padding': [ + '15', + 'padding to right when aligned to right', + ], + 'space': [ + '2', + 'minimum space before help', + ], } cmdhelp_settings = {} + def unhook(hooks): """Unhook something hooked by this script.""" global cmdhelp_hooks @@ -98,6 +174,7 @@ def unhook(hooks): weechat.unhook(cmdhelp_hooks[hook]) cmdhelp_hooks[hook] = '' + def config_cb(data, option, value): """Called when a script option is changed.""" global cmdhelp_settings, cmdhelp_hooks @@ -108,12 +185,13 @@ def config_cb(data, option, value): cmdhelp_settings[name] = value if name == 'stop_on_enter': if value == 'on' and not cmdhelp_hooks['command_run']: - cmdhelp_hooks['command_run'] = weechat.hook_command_run('/input return', - 'command_run_cb', '') + cmdhelp_hooks['command_run'] = weechat.hook_command_run( + '/input return', 'command_run_cb', '') elif value != 'on' and cmdhelp_hooks['command_run']: unhook(('command_run',)) return weechat.WEECHAT_RC_OK + def command_run_cb(data, buffer, command): """Callback for "command_run" hook.""" global cmdhelp_hooks, cmdhelp_settings @@ -121,9 +199,11 @@ def command_run_cb(data, buffer, command): unhook(('timer', 'modifier')) return weechat.WEECHAT_RC_OK + def format_option(match): """Replace ${xxx} by its value in option format.""" - global cmdhelp_settings, cmdhelp_option_infolist, cmdhelp_option_infolist_fields + global cmdhelp_settings, cmdhelp_option_infolist + global cmdhelp_option_infolist_fields string = match.group() end = string.find('}') if end < 0: @@ -146,30 +226,40 @@ def format_option(match): elif fieldtype == 'p': string = weechat.infolist_pointer(cmdhelp_option_infolist, field) elif fieldtype == 't': - string = weechat.infolist_time(cmdhelp_option_infolist, field) + date = weechat.infolist_time(cmdhelp_option_infolist, field) + # since WeeChat 2.2, infolist_time returns a long integer instead of + # a string + if not isinstance(date, str): + date = time.strftime('%F %T', time.localtime(int(date))) + string = date return '%s%s%s' % (color1, string, color2) + def get_option_list_and_desc(option, displayname): """Get list of options and description for option(s).""" - global cmdhelp_settings, cmdhelp_option_infolist, cmdhelp_option_infolist_fields + global cmdhelp_settings, cmdhelp_option_infolist + global cmdhelp_option_infolist_fields options = [] description = '' cmdhelp_option_infolist = weechat.infolist_get('option', '', option) if cmdhelp_option_infolist: cmdhelp_option_infolist_fields = {} while weechat.infolist_next(cmdhelp_option_infolist): - options.append(weechat.infolist_string(cmdhelp_option_infolist, 'full_name')) + options.append(weechat.infolist_string(cmdhelp_option_infolist, + 'full_name')) if not description: fields = weechat.infolist_fields(cmdhelp_option_infolist) for field in fields.split(','): items = field.split(':', 1) if len(items) == 2: cmdhelp_option_infolist_fields[items[1]] = items[0] - description = re.compile(r'\$\{[^\}]+\}').sub(format_option, cmdhelp_settings['format_option']) + description = re.compile(r'\$\{[^\}]+\}').sub( + format_option, cmdhelp_settings['format_option']) if displayname: description = '%s%s%s: %s' % ( weechat.color(cmdhelp_settings['color_option_name']), - weechat.infolist_string(cmdhelp_option_infolist, 'full_name'), + weechat.infolist_string(cmdhelp_option_infolist, + 'full_name'), weechat.color(cmdhelp_settings['color_option_help']), description) weechat.infolist_free(cmdhelp_option_infolist) @@ -177,9 +267,11 @@ def get_option_list_and_desc(option, displayname): cmdhelp_option_infolist_fields = {} return options, description + def get_help_option(input_args): """Get help about option or values authorized for option.""" - global cmdhelp_settings, cmdhelp_option_infolist, cmdhelp_option_infolist_fields + global cmdhelp_settings, cmdhelp_option_infolist + global cmdhelp_option_infolist_fields pos = input_args.find(' ') if pos > 0: option = input_args[0:pos] @@ -191,7 +283,7 @@ def get_help_option(input_args): if len(options) > 1: try: max_options = int(cmdhelp_settings['max_options']) - except: + except ValueError: max_options = 5 if len(options) > max_options: text = '%s...' % ', '.join(options[0:max_options]) @@ -203,8 +295,11 @@ def get_help_option(input_args): weechat.color(cmdhelp_settings['color_list']), text) if description: - return '%s%s' % (weechat.color(cmdhelp_settings['color_option_help']), description) - return '%sNo help for option %s' % (weechat.color(cmdhelp_settings['color_no_help']), option) + return '%s%s' % (weechat.color(cmdhelp_settings['color_option_help']), + description) + return '%sNo help for option %s' % ( + weechat.color(cmdhelp_settings['color_no_help']), option) + def get_command_arguments(input_args, cmd_args): """Get command arguments according to command arguments given in input.""" @@ -225,6 +320,7 @@ def get_command_arguments(input_args, cmd_args): return partial return cmd_args + def get_help_command(plugin, input_cmd, input_args): """Get help for command in input.""" global cmdhelp_settings @@ -236,7 +332,8 @@ def get_help_command(plugin, input_cmd, input_args): cmd_args = '' cmd_desc = '' while weechat.infolist_next(infolist): - cmd_plugin_name = weechat.infolist_string(infolist, 'plugin_name') or 'core' + cmd_plugin_name = (weechat.infolist_string(infolist, 'plugin_name') or + 'core') cmd_command = weechat.infolist_string(infolist, 'command') cmd_args = weechat.infolist_string(infolist, 'args_nls') cmd_desc = weechat.infolist_string(infolist, 'description') @@ -244,17 +341,21 @@ def get_help_command(plugin, input_cmd, input_args): break weechat.infolist_free(infolist) if cmd_plugin_name == 'alias': - return '%sAlias %s%s%s => %s%s' % (weechat.color(cmdhelp_settings['color_alias']), - weechat.color(cmdhelp_settings['color_alias_name']), - cmd_command, - weechat.color(cmdhelp_settings['color_alias']), - weechat.color(cmdhelp_settings['color_alias_value']), - cmd_desc) + return '%sAlias %s%s%s => %s%s' % ( + weechat.color(cmdhelp_settings['color_alias']), + weechat.color(cmdhelp_settings['color_alias_name']), + cmd_command, + weechat.color(cmdhelp_settings['color_alias']), + weechat.color(cmdhelp_settings['color_alias_value']), + cmd_desc, + ) if input_args: cmd_args = get_command_arguments(input_args, cmd_args) if not cmd_args: return None - return '%s%s' % (weechat.color(cmdhelp_settings['color_arguments']), cmd_args) + return '%s%s' % (weechat.color(cmdhelp_settings['color_arguments']), + cmd_args) + def get_list_commands(plugin, input_cmd, input_args): """Get list of commands (beginning with current input).""" @@ -264,7 +365,8 @@ def get_list_commands(plugin, input_cmd, input_args): plugin_names = [] while weechat.infolist_next(infolist): commands.append(weechat.infolist_string(infolist, 'command')) - plugin_names.append(weechat.infolist_string(infolist, 'plugin_name') or 'core') + plugin_names.append( + weechat.infolist_string(infolist, 'plugin_name') or 'core') weechat.infolist_free(infolist) if commands: if len(commands) > 1 or commands[0].lower() != input_cmd.lower(): @@ -281,6 +383,7 @@ def get_list_commands(plugin, input_cmd, input_args): ', '.join(commands2)) return None + def input_modifier_cb(data, modifier, modifier_data, string): """Modifier that will add help on command line (for display only).""" global cmdhelp_settings @@ -303,7 +406,8 @@ def input_modifier_cb(data, modifier, modifier_data, string): current_buffer = weechat.current_buffer() current_window = weechat.current_window() plugin = weechat.buffer_get_pointer(current_buffer, 'plugin') - msg_help = get_help_command(plugin, command[1:], arguments) or get_list_commands(plugin, command[1:], arguments) + msg_help = (get_help_command(plugin, command[1:], arguments) or + get_list_commands(plugin, command[1:], arguments)) if not msg_help: if cmdhelp_settings['display_no_help'] != 'on': return string @@ -315,7 +419,8 @@ def input_modifier_cb(data, modifier, modifier_data, string): if cmdhelp_settings['right_align'] == 'on': win_width = weechat.window_get_integer(current_window, 'win_width') - input_length = weechat.buffer_get_integer(current_buffer, 'input_length') + input_length = weechat.buffer_get_integer(current_buffer, + 'input_length') help_length = len(weechat.string_remove_color(msg_help, "")) min_space = int(cmdhelp_settings['space']) padding = int(cmdhelp_settings['right_padding']) @@ -334,6 +439,7 @@ def input_modifier_cb(data, modifier, modifier_data, string): weechat.color(color_delimiters), cmdhelp_settings['suffix']) + def timer_cb(data, remaining_calls): """Timer callback.""" global cmdhelp_hooks @@ -342,32 +448,35 @@ def timer_cb(data, remaining_calls): weechat.bar_item_update('input_text') return weechat.WEECHAT_RC_OK + def cmd_help_toggle(): """Toggle help on/off.""" global cmdhelp_hooks, cmdhelp_settings if cmdhelp_hooks['modifier']: unhook(('timer', 'modifier')) else: - cmdhelp_hooks['modifier'] = weechat.hook_modifier('input_text_display_with_cursor', - 'input_modifier_cb', '') + cmdhelp_hooks['modifier'] = weechat.hook_modifier( + 'input_text_display_with_cursor', 'input_modifier_cb', '') timer = cmdhelp_settings['timer'] if timer and timer != '0': try: value = float(timer) if value > 0: weechat.hook_timer(value * 1000, 0, 1, 'timer_cb', '') - except: + except ValueError: pass weechat.bar_item_update('input_text') + def cmd_help_cb(data, buffer, args): """Callback for /cmd_help command.""" cmd_help_toggle() return weechat.WEECHAT_RC_OK + if __name__ == '__main__' and import_ok: - if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, '', ''): + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, + SCRIPT_LICENSE, SCRIPT_DESC, '', ''): # set allowed fields in option "format_option" fields = [] infolist = weechat.infolist_get('option', '', 'weechat.plugin.*') @@ -380,7 +489,8 @@ def cmd_help_cb(data, buffer, args): fields.append(items[1]) weechat.infolist_free(infolist) if fields: - cmdhelp_settings_default['format_option'][1] += ': %s' % ', '.join(fields) + cmdhelp_settings_default['format_option'][1] += ( + ': %s' % ', '.join(fields)) # set default settings version = weechat.info_get("version_number", "") or 0 @@ -391,30 +501,35 @@ def cmd_help_cb(data, buffer, args): weechat.config_set_plugin(option, value[0]) cmdhelp_settings[option] = value[0] if int(version) >= 0x00030500: - weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) + weechat.config_set_desc_plugin( + option, + '%s (default: "%s")' % (value[1], value[0])) # detect config changes - weechat.hook_config('plugins.var.python.%s.*' % SCRIPT_NAME, 'config_cb', '') + weechat.hook_config('plugins.var.python.%s.*' % SCRIPT_NAME, + 'config_cb', '') # add hook to catch "enter" key if cmdhelp_settings['stop_on_enter'] == 'on': - cmdhelp_hooks['command_run'] = weechat.hook_command_run('/input return', - 'command_run_cb', '') + cmdhelp_hooks['command_run'] = weechat.hook_command_run( + '/input return', 'command_run_cb', '') # add command - weechat.hook_command(SCRIPT_COMMAND, - 'Contextual command line help.', - '', - 'This comand toggles help on command line.\n\n' - 'It is recommended to bind this command on a key, for example F1:\n' - ' /key bind /cmd_help\n' - 'which will give, according to your terminal something like:\n' - ' /key bind meta-OP /cmd_help\n' - ' or:\n' - ' /key bind meta2-11~ /cmd_help\n\n' - 'To try: type "/server" (without pressing enter) and press F1 ' - '(then you can add arguments and enjoy dynamic help!)', - '', 'cmd_help_cb', '') + weechat.hook_command( + SCRIPT_COMMAND, + 'Contextual command line help.', + '', + 'This comand toggles help on command line.\n\n' + 'It is recommended to bind this command on a key, for example ' + 'F1:\n' + ' /key bind /cmd_help\n' + 'which will give, according to your terminal something like:\n' + ' /key bind meta-OP /cmd_help\n' + ' or:\n' + ' /key bind meta2-11~ /cmd_help\n\n' + 'To try: type "/server" (without pressing enter) and press F1 ' + '(then you can add arguments and enjoy dynamic help!)', + '', 'cmd_help_cb', '') # auto start help if cmdhelp_settings['start_on_load'] == 'on': diff --git a/python/cmdqueue.py b/python/cmdqueue.py new file mode 100644 index 00000000..ae45f184 --- /dev/null +++ b/python/cmdqueue.py @@ -0,0 +1,459 @@ +# ==================================================== # +# Script Name: cmdqueue.py +# Script Author: walk +# Script Purpose: Command queing at its finest. Hopefully. +# +# Copyright (C) 2011 walk +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Version History: +# 0.4.5 - November 9th, 2021 +# Fix inconsistent use of tabs and spaces (now uses spaces as sugggested by PEP 8) +# Improve formatting of code for better readability +# +# 0.4.4 - Sep 11th, 2021 +# Rename script to cmdqueue.py. +# +# 0.4.3 - May 5th, 2021 +# Add Python 3 compatibility. +# Add compatibility with XDG directories (WeeChat >= 3.2). +# +# 0.4.2 - Nov 22nd, 2015 +# Add saving of static queues to disk and reloading them on startup. +# Added by Tim Kuhlman - https://github.com/tkuhlman +# +# 0.4.1 - Jan 20th, 2011 +# Multi-list queuing seems to work flawlessly so far. Expanded on the /help qu text. +# Properties are fully-functional. As for loading/saving of lists, I want to hold off until +# I get some feedback on whether or not that would be applicable. Please email me +# at the listed address in this script and let me know if you want this feature and/or +# any other features. +# +# 0.4.0 - Jan 16th, 2011 +# Finished adding multi-list queuing. So far, no bugs that I can tell. +# Perhaps an ability to load/save lists for multiple uses? Now working on +# setting individual properties per list then taking a few days off this thing. +# ** TESTED ON WeeChat 0.3.3 and 0.3.4 ** +# +# 0.3.5 - Jan 15th, 2011 +# Started to implement multiple queue lists. +# +# 0.3.5 - Jan 14th, 2011 +# Wrote Queue class and merged with code. I did this because +# some future release will include an ability to use multiple +# queue lists. Also fixed some bugs in parsing and executing +# arguments with /qu. /qu del by itself raised an error, /qu del j +# raised a valueerror, fixed. /qu add singlearg wouldn't add. Fixed. +# +# 0.3.4 - Jan 11th, 2011 +# Modified configuration code to use plugins.conf +# +# 0.3.3 - Jan 9th, 2011 +# Big code clean-up. Reduced a few lines by optimizing code. +# Next step, convertcommand_qu dictionary to array like +# I should have done in the first place. Could save some lines. +# +# 0.3.2 - Jan 9th, 2011 +# Added RAINBOW option (requested) +# Set as option in configuration +# +# 0.3.1 - Jan 8th, 2011 +# Added configurations (verbose, core output only) +# +# 0.3.0 - Jan 6th, 2011 +# Worked on script for quite a while. All code is functional. +# Continuing to test for bugs and improve code +# Fixed indexing issue after using /qu del with remove_index function +# +# 0.2.0 - Jan 5th, 2011 +# Cleaned up temporary testing code, built upon foundation. +# Next to include, del function and list function +# +# 0.1.0 - Jan 4th, 2011 +# Wrote basic outline with minimal functionality +# ==================================================== # + +import os +import pickle + +import_ok = True +try: + import weechat +except ImportError: + print("This script requires WeeChat") + print("To obtain a copy, for free, visit http://weechat.org") + import_ok = False + + +class Queue(): + """ A Queuing Class """ + + def __init__(self): + self.data = [] + self.index = len(self.data) + self.__clearable__ = True + self.__ldl__ = "" + self.__locked__ = False + + def __iter__(self): + for char in range(self.index): + yield self.data[char] + + def __len__(self): + return len(self.data) + + def add(self, queue_text): + if self.__locked__ == False: + self.data.append(queue_text) + self.index = len(self.data) + + def remove(self, info): + if self.__locked__ == False: + tmp = "" + if info > 0: + tmp = str(self.data[info-1]) + del self.data[info-1] + self.__ldl__ = tmp + self.index = len(self.data) + elif info == 0: + tmp = str(self.data[info]) + del self.data[info] + self.__ldl__ = tmp + self.index = len(self.data) + + return self.__ldl__ + + def viewqueue(self): + list = "" + if not len(self.data) == 0: + for each in range(len(self.data)): + list += str(each+1) + ". " + str(self.data[each]) + "\n" + else: + list = "Nothing in queue" + + return list.strip("\n") + + def clearqueue(self): + if self.__clearable__ == True: + self.data = [] + self.index = 0 + + def isClear(self, cOpt): + if not cOpt in (True, False): + return + + self.__clearable__ = cOpt + + def isLocked(self, lockOpt): + if not lockOpt in (True, False): + return + + self.__locked__ = lockOpt + + def isEmpty(self): + if len(self.data) == 0: + return True + else: + return False + + +SCRIPT_NAME = "cmdqueue" +SCRIPT_AUTHOR = "walk" +SCRIPT_VERSION = "0.4.5" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = "Command queuing" + +COMM_CMD = "qu" +COMM_DESC = "Queuing commands in WeeChat" +COMM_ARGS = "[add [command] | del [index] | new [list] | dellist [list] | set [property] [on|off] |list | clear | exec | listview]" +COMM_ARGS_DESC = "Examples: \n\ + /qu add /msg chanserv op #foo bar \n\ + /qu del 1 \n\ + /qu new weechat \n\ + - Use the 'new' argument to switch to already defined lists as well. \n\ + /qu dellist weechat \n\ + /qu list - List commands in current list \n\ + /qu list weechat - With optional parameter, you can choose to list the commands of a specified list. \n\ + /qu clear - Clear current list.. add a listname to clear a specified list. \n\ + /qu exec - Execute the commands of the current list.. you can also specify a list here as well. \n\ + /qu listview - Outputs the names of all your lists. \n\ + /qu save - Save static lists to disk \n\ + /qu set static on - Sets static property to ON for current list. This means that when executed, the list WILL NOT clear. The clear command will not work either.\n \ + \n\ + PROPERTIES (for set command):\n \ + static - prevents a list from clearing manually or automatically but can still add and del commands.\n \ + lock - prevents the user from adding/deleting entries to a list. Can be combined with static." +COMM_COMPL = "add|del|list|exec|new|listview|dellist" + +COMMAND_QU = {"default": Queue()} +CURR_LIST = "default" + + +def __config__(): + """ Configuration initialization """ + + if not weechat.config_get_plugin("core_output_only") in ("yes", "no"): + weechat.config_set_plugin("core_output_only", "yes") + if not weechat.config_get_plugin("rainbow_allow") in ("yes", "no"): + weechat.config_set_plugin("rainbow_allow", "no") + if not weechat.config_get_plugin("verbose") in ("yes", "no"): + weechat.config_set_plugin("verbose", "yes") + + load() + return weechat.WEECHAT_RC_OK + + +def load(): + """ Load saved queues from pickle. """ + global COMMAND_QU + data_dir = weechat.info_get("weechat_data_dir", "") \ + or weechat.info_get("weechat_dir", "") + pickle_path = os.path.join(data_dir, "queue.pickle") + if os.path.exists(pickle_path): + with open(pickle_path, "rb") as qu_pickle: + COMMAND_QU = pickle.load(qu_pickle) + + if "default" not in COMMAND_QU: + COMMAND_QU["default"] = Queue() + + +def rainbow(data): + """ Not my favorite option but a requested one """ + + colors = "red yellow green blue magenta" + c = colors.split() + count = 0 + colorHolder = "" + + for each in data: + if count > 4: + count = 0 + if not each == " ": + colorHolder += weechat.color(c[count])+each + count += 1 + else: + colorHolder += " " + + return str(colorHolder) + + +def prntcore(data, essential=0, rb=0): + """ Built more on weechat.prnt """ + + if weechat.config_get_plugin("verbose") == "yes" or essential == 1: + if weechat.config_get_plugin("core_output_only") == "yes": + buffer = "" + else: + buffer = weechat.current_buffer() + if rb == 0: + weechat.prnt(buffer, data) + else: + weechat.prnt(buffer, rainbow(data)) + return weechat.WEECHAT_RC_OK + + +def save(): + """ Save to disk all static lists as a pickle. """ + global COMMAND_QU + data_dir = weechat.info_get("weechat_data_dir", "") \ + or weechat.info_get("weechat_dir", "") + pickle_path = os.path.join(data_dir, "queue.pickle") + to_save = {} + for name, qu in COMMAND_QU.items(): + if not qu.__clearable__: # Note isClear method doesn't show status it sets it + to_save[name] = qu + + with open(pickle_path, "wb") as qu_pickle: + pickle.dump(to_save, qu_pickle, pickle.HIGHEST_PROTOCOL) + + +def rejoin(data, delimiter=" "): + """ Rejoins a split string """ + tmpString = "" + for each in data: + tmpString += each+delimiter + + tmpString = tmpString.strip() + return tmpString + + +def qu_cb(data, buffer, args): + """ Process hook_command info """ + + global CURR_LIST, COMMAND_QU + if weechat.config_get_plugin("rainbow_allow") == "no": + rainbowit = 0 + else: + rainbowit = 1 + + if args == "": + return weechat.WEECHAT_RC_OK + + argv = args.split() + arglist = ["add", "del", "new", "dellist", "list", "clear", "exec", "listview", "save", "set"] + + if not argv[0] in arglist: + prntcore("[ queue -> not a valid argument: {0}".format(argv[0]), rb=rainbowit) + return weechat.WEECHAT_RC_OK + + if argv[0].lower() == "add" and len(argv) > 1: + if not COMMAND_QU[CURR_LIST].__locked__ == True: + COMMAND_QU[CURR_LIST].add(rejoin(argv[1:])) + prntcore("[ queue added -> "+str(rejoin(argv[1:])) + " ]", rb=rainbowit) + else: + prntcore("[ queue -> the lock property is enabled for this list ({0}). please disable it before adding/deleting. ]", rb=rainbowit) + + elif argv[0].lower() == "del" and len(argv) > 1: + if not COMMAND_QU[CURR_LIST].__locked__ == True: + try: + rmd = COMMAND_QU[CURR_LIST].remove(int(argv[1])) + prntcore("[ queue -> deleted: ({0}) {1} ]".format(argv[1], rmd), rb=rainbowit) + except (IndexError, ValueError): + prntcore("[ queue -> invalid reference. please check /qu list and try again. ]", rb=rainbowit) + else: + prntcore("[ queue -> the lock property is enabled for this list ({0}). please disable it before adding/deleting. ]".format(CURR_LIST), rb=rainbowit) + + elif argv[0].lower() == "clear": + this_list = None + if len(argv) > 1 and argv[1].lower() in COMMAND_QU.keys(): + this_list = CURR_LIST + CURR_LIST = argv[1].lower() + + if COMMAND_QU[CURR_LIST].__clearable__ == True: + if not COMMAND_QU[CURR_LIST].isEmpty(): + COMMAND_QU[CURR_LIST].clearqueue() + prntcore("[ queue -> command queue list cleared. ]", rb=rainbowit) + else: + prntcore("[ queue -> command queue already empty. ]", rb=rainbowit) + else: + prntcore("[ queue -> please turn off the static property to clear the {0} list. ]".format(CURR_LIST), rb=rainbowit) + + if not this_list == None: + CURR_LIST = this_list + this_list = None + + elif argv[0].lower() == "list": + this_list = None + if len(argv) > 1 and argv[1].lower() in COMMAND_QU.keys(): + this_list = CURR_LIST + CURR_LIST = argv[1].lower() + + qHeader = "[ COMMAND QUEUE: {0} ]".format(CURR_LIST) + prntcore(" ", 1) + prntcore("-"*len(qHeader), 1, rb=rainbowit) + prntcore(qHeader, 1, rainbowit) + prntcore("-"*len(qHeader), 1, rainbowit) + prntcore(COMMAND_QU[CURR_LIST].viewqueue(), 1, rb=rainbowit) + + if not this_list == None: + CURR_LIST = this_list + this_list = None + + elif argv[0].lower() == "exec": + + this_list = None + if len(argv) > 1 and argv[1].lower() in COMMAND_QU.keys(): + this_list = CURR_LIST + CURR_LIST = argv[1].lower() + + if len(COMMAND_QU[CURR_LIST]) > 0: + + for each in COMMAND_QU[CURR_LIST]: + weechat.command(buffer, each) + COMMAND_QU[CURR_LIST].clearqueue() + if COMMAND_QU[CURR_LIST].__clearable__ == True: + prntcore("[ queue -> finished executing list: {0}. command list cleared. ]".format(CURR_LIST), rb=rainbowit) + else: + prntcore("[ queue -> finished executing list: {0} ]".format(CURR_LIST), rb=rainbowit) + else: + prntcore("[ queue -> nothing to execute. please add to the queue using /qu add ", rb=rainbowit) + + if not this_list == None: + CURR_LIST = this_list + this_list = None + + elif argv[0].lower() == "new" and len(args.split()) > 1: + if argv[1].lower() in COMMAND_QU.keys(): + CURR_LIST = argv[1].lower() + prntcore("[ queue -> switched queue list to: {0}".format(CURR_LIST), rb=rainbowit) + else: + COMMAND_QU[argv[1].lower()] = Queue() + CURR_LIST = argv[1].lower() + prntcore("[ queue -> created new list. current list is: {0}".format(CURR_LIST), rb=rainbowit) + + elif argv[0].lower() == "listview": + qHeader = "QUEUE LISTS" + listCount = 1 + + prntcore(" ", 1) + prntcore("-"*len(qHeader), 1, rb=rainbowit) + prntcore(qHeader, 1, rb=rainbowit) + prntcore("-"*len(qHeader), 1, rb=rainbowit) + + for each in COMMAND_QU.keys(): + prntcore(str(listCount) + ". " + str(each), 1, rb=rainbowit) + listCount += 1 + + elif argv[0].lower() == "dellist" and len(args.split()) > 1: + if not argv[1].lower() in COMMAND_QU.keys(): + prntcore("[ queue -> {0} is not a list. ]".format(argv[1].lower()), rb=rainbowit) + elif argv[1].lower() == "default": + prntcore("[ queue -> cannot delete the default list. ]", rb=rainbowit) + else: + if argv[1].lower() == CURR_LIST: + CURR_LIST = "default" + del COMMAND_QU[argv[1].lower()] + prntcore("[ queue -> {0} successfully deleted.".format(argv[1].lower()), rb=rainbowit) + + elif argv[0].lower() == "save": + save() + elif argv[0].lower() == "set" and len(argv) == 4: + setargs = args.split() + list_name = setargs[1].lower() + set_prop = setargs[2].lower() + toggle = setargs[3].lower() + properties = ["static", "lock"] + + if not list_name in COMMAND_QU.keys(): + prntcore("[ queue -> list must be created before you can set properties ]", rb=rainbowit) + elif not set_prop in properties: + prntcore("[ queue -> invalid property. please try again. ]", rb=rainbowit) + elif not toggle in ("on", "off"): + prntcore("[ queue -> only valid options for a property are ON or OFF ]", rb=rainbowit) + else: + if set_prop == "static": + if toggle == "on": + COMMAND_QU[list_name].isClear(False) + prntcore("[ queue -> static property toggled on for: {0} ]".format(list_name), rb=rainbowit) + save() + else: + COMMAND_QU[list_name].isClear(True) + prntcore("[ queue -> static property toggled off for: {0} ]".format(list_name), rb=rainbowit) + save() + + elif set_prop == "lock": + if toggle == "on": + COMMAND_QU[list_name].isLocked(True) + prntcore("[ queue -> lock property toggled on for: {0} ]".format(list_name), rb=rainbowit) + else: + COMMAND_QU[list_name].isLocked(False) + prntcore("[ queue -> lock property toggled off for: {0} ]".format(list_name), rb=rainbowit) + + return weechat.WEECHAT_RC_OK + + +if import_ok and weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + weechat.hook_command(COMM_CMD, COMM_DESC, COMM_ARGS, COMM_ARGS_DESC, COMM_COMPL, "qu_cb", "") + __config__() diff --git a/python/cmus.py b/python/cmus.py index 58f3174c..8a7d9215 100644 --- a/python/cmus.py +++ b/python/cmus.py @@ -16,11 +16,11 @@ # Made as a port of cmus_xchat-v2.0, also made by Isaac Ross. Due to the nature of weechat's plugin/scripting API, # this was mostly made using find/replace in a text editor -import commands +import subprocess import weechat import os -weechat.register("cmus", "Isaac Ross", "1.02", "GPL2", "Adds ability to control cmus and post the currently playing song in a channel", "", "") +weechat.register("cmus", "Isaac Ross", "1.1", "GPL2", "Adds ability to control cmus and post the currently playing song in a channel", "", "") def help(): @@ -41,7 +41,7 @@ def help(): weechat.prnt('', "Keep in mind that most problems will probably be related to cmus-remote, not this script") def np(): - cmus = commands.getoutput('cmus-remote -Q') + cmus = subprocess.getoutput('cmus-remote -Q') lines = cmus.split('\n') #some redundant loops later, but streamline as needed @@ -89,14 +89,14 @@ def control(data, buffer, args): os.system('cmus-remote -S') weechat.prnt('', 'Toggled shuffle on/off.') elif args[0].lower() == 'status': - status = commands.getoutput('cmus-remote -Q') + status = subprocess.getoutput('cmus-remote -Q') status = status.split('\n') for line in status: weechat.prnt('', " -- " + line) elif args[0].lower() == 'help': help() elif args[0].lower() == 'file': - filename = commands.getoutput('cmus-remote -Q') + filename = subprocess.getoutput('cmus-remote -Q') filename = filename.split('\n') newname = filename[1] newname = newname.replace('file', '', 1) diff --git a/python/collapse_channel.py b/python/collapse_channel.py new file mode 100644 index 00000000..20df7d4d --- /dev/null +++ b/python/collapse_channel.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019-2023 by nils_2 +# +# collapse channel buffers from servers without focus +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# 2019-03-13: nils_2, (freenode.#weechat) +# 0.1 : initial release, py3k-ok +# +# 2019-03-19: nils_2, (freenode.#weechat) +# 0.2 : add function exclude hotlist +# +# 2019-03-19: nils_2, (freenode.#weechat) +# 0.3 : add function activity +# +# 2019-03-21: nils_2, (freenode.#weechat) +# 0.4 : workaround for bug https://github.com/weechat/weechat/issues/1325#event-2214793184 +# : workaround for signal buffer_switch, otherwise the warning "/allchan -current" will be printed +# : add command help +# : fix "/allchan -current" warning when /server raw is executed +# +# 2019-03-23: nils_2, (freenode.#weechat) +# 0.5 : fix "/allchan -current" warning when signal "buffer_opened" is called +# : changed default value for hotlist option +# +# 2019-05-09: nils_2, (freenode.#weechat) +# 0.6 : fix hiding of channel buffer when private buffer opens +# +# 2019-09-06: nils_2, (freenode.#weechat) +# 0.7 : fix: ignore "slack" for signal "buffer_switch" +# +# 2020-07-20: Sébastien Helleu +# 0.8 : fix: add missing "/" in /allchan command +# +# 2021-11-06: Sébastien Helleu +# 0.9 : make script compatible with WeeChat >= 3.4 +# (new parameters in function hdata_search) +# +# 2023-09-01: nils_2, (libera.#weechat) +# 1.0 : check for buffer_ptr and for irc buffer +# +# 2023-09-02: nils_2, (libera.#weechat) +# 1.1 : one more check for buffer_ptr +# +# 2023-09-08: nils_2, (libera.#weechat) +# 1.2 : when in non-irc buffers (eg. /server raw) exclude channels are ignored, internal changes +# +# 2024-11-16: nils_2, (libera.#weechat) +# 1.3 : hook_signal(hotlist_changed) fixed. + +# idea and testing by DJ-ArcAngel + +try: + import weechat,re + +except Exception: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org/") + quit() + +SCRIPT_NAME = "collapse_channel" +SCRIPT_AUTHOR = "nils_2 " +SCRIPT_VERSION = "1.3" +SCRIPT_LICENSE = "GPL" +SCRIPT_DESC = "collapse channel buffers from servers without focus" + +OPTIONS = { 'server_exclude' : ('','exclude some server, comma separated list (wildcard "*" is allowed)'), + 'channel_exclude' : ('','exclude some channel, comma separated list. This is server independent (wildcard "*" is allowed)'), + 'single_channel_exclude': ('','exclude specific channels on specific server, space separated list (eg. freenode.#weechat)'), + 'hotlist' : ('4','unhide buffer by activity, when buffer is added to hotlist (0=off, 1=message, 2=private message, 3=highlight, 4=all)'), + 'activity' : ('off','show channels with activity only (see option hotlist). all exclude options will be ignored'), + } + +# ================================[ buffer open/closed ]=============================== +def buffer_opened_closed_cb(data, signal, signal_data): + global OPTIONS + # sadly localvar not set in this moment, when buffer opens! :-( + # server = weechat.buffer_get_string(signal_data, 'localvar_server') # get internal servername + infolist = weechat.infolist_get('buffer', signal_data, '') + weechat.infolist_next(infolist) + plugin_name = weechat.infolist_string(infolist, 'plugin_name') + name = weechat.infolist_string(infolist, 'name') + short_name = weechat.infolist_string(infolist, 'short_name') + full_name = weechat.infolist_string(infolist, 'full_name') + weechat.infolist_free(infolist) + # TODO how about matrix script or other non-irc channel buffer? no idea! help is welcome + if plugin_name != "irc": # for example /fset, /color etc.pp buffer + return weechat.WEECHAT_RC_OK + + if OPTIONS['activity'].lower() == 'no' or OPTIONS['activity'].lower() == 'off' or OPTIONS['activity'].lower() == '0': + weechat.command('','/allchan -exclude=%s /buffer hide' % OPTIONS['channel_exclude']) + if not signal_data: # signal_data available? + weechat.command(signal_data,'/allchan -current /buffer unhide') + else: # signal_data empty! + weechat.command('','/allchan /buffer hide') + if signal_data and name.find('.') != -1: # signal_data available and "name" has separator "." eg "irc_raw" buffer? + server = name.rsplit('.', 1)[-2] # server.buffer + buffer_ptr = weechat.buffer_search('irc', 'server.%s' % server) + if buffer_ptr: + weechat.command(buffer_ptr,'/allchan -current /buffer unhide') + weechat.command('','/allchan -exclude=%s /buffer hide' % OPTIONS['channel_exclude']) + exclude_server() + single_channel_exclude() + else: + weechat.command('','/allchan /buffer hide') + exclude_hotlist() + return weechat.WEECHAT_RC_OK +# ============================[ buffer_switch ]=========================== +def buffer_switch_cb(data, signal, signal_data): + global OPTIONS, version + + plugin_name = weechat.buffer_get_string(signal_data, 'localvar_plugin') # get plugin + if plugin_name != "irc": # script only support irc plugin! + return weechat.WEECHAT_RC_OK + + # when you /join a buffer and irc.look.buffer_switch_join is ON, the new buffer pointer is not useable at this time + weechat.command("","/wait 1ms /mute") + server = weechat.buffer_get_string(signal_data, 'localvar_server') # get internal servername + buffer_ptr = weechat.buffer_search('irc', 'server.%s' % server) # looks for server. (This does not effect eg server raw buffer) + if not buffer_ptr and server != 'irc_raw': # buffer pointer exists? + return weechat.WEECHAT_RC_OK # no! + + if OPTIONS['activity'].lower() == 'no' or OPTIONS['activity'].lower() == 'off' or OPTIONS['activity'].lower() == '0': + # hide all channel but use -exclude + weechat.command('','/allchan -exclude=%s /buffer hide' % OPTIONS['channel_exclude']) + if server == 'irc_raw': # buffer is /server raw + weechat.command('','/allchan /buffer unhide') + weechat.command('','/allchan -exclude=%s /buffer hide' % OPTIONS['channel_exclude']) + elif server != '': # a buffer with server + weechat.command(buffer_ptr,'/allchan -current /buffer unhide') # use buffer pointer from server + exclude_server() + single_channel_exclude() + else: + if int(version) <= 0x02040000: # workaround + weechat.command(signal_data,'/allchan -current /buffer hide') + bufpointer = weechat.window_get_pointer(weechat.current_window(), 'buffer') # get current channel pointer + weechat.command('','/allchan /buffer hide') + weechat.command(bufpointer,'/buffer unhide') # unhide current channel + exclude_hotlist() + return weechat.WEECHAT_RC_OK +# ================================[ hotlist changed ]============================== +def hotlist_changed_cb(data, signal, signal_data): + plugin_name = weechat.buffer_get_string(weechat.current_buffer(), 'localvar_plugin') + # TODO how about matrix script or other non-irc channel buffer? no idea! help is welcome +# if plugin_name != 'irc': # script only support irc plugin! +# return weechat.WEECHAT_RC_OK +# weechat.command('', '/allchan /buffer hide') + if OPTIONS['activity'].lower() == 'no' or OPTIONS['activity'].lower() == 'off' or OPTIONS['activity'].lower() == '0': + exclude_server() + single_channel_exclude() + exclude_hotlist() + return weechat.WEECHAT_RC_OK +# ================================[ window switch ]=============================== +def window_switch_cb(data, signal, signal_data): + bufpointer = weechat.window_get_pointer(signal_data,'buffer') + buffer_switch_cb(data,signal,bufpointer) + return weechat.WEECHAT_RC_OK +# ================================[ server signals ]=============================== +def irc_server_disconnected_cb(data, signal, signal_data): + buffer_switch_cb(data,signal,signal_data) + return weechat.WEECHAT_RC_OK + +def irc_server_connected_cb(data, signal, signal_data): + buffer_switch_cb(data,signal,signal_data) + return weechat.WEECHAT_RC_OK + +def exclude_hotlist(): + if OPTIONS['hotlist'] == '0' or OPTIONS['hotlist'] =='': + return weechat.WEECHAT_RC_OK + infolist = weechat.infolist_get('hotlist', '', '') + while weechat.infolist_next(infolist): + buffer_number = weechat.infolist_integer(infolist, 'buffer_number') + priority = weechat.infolist_integer(infolist, 'priority') + if int(OPTIONS['hotlist']) == priority or OPTIONS['hotlist'] == '4': + weechat.command('','/buffer unhide %s' % buffer_number) + weechat.infolist_free(infolist) + return weechat.WEECHAT_RC_OK + +def exclude_server(): + global OPTIONS + for server_exclude in OPTIONS['server_exclude'].split(','): + if server_exclude == '*': # show buffer for all server + weechat.command('','/buffer unhide -all') # simply unload script, no!? :-) + break + + # search exclude server in list of servers + hdata = weechat.hdata_get('irc_server') + servers = weechat.hdata_get_list(hdata, 'irc_servers') + if int(version) >= 0x03040000: + server = weechat.hdata_search( + hdata, + servers, + '${irc_server.name} =* ${server_name}', + {}, + {'server_name': server_exclude}, + {}, + 1, + ) + else: + server = weechat.hdata_search( + hdata, + servers, + '${irc_server.name} =* %s' % server_exclude, + 1, + ) + if server: +# is_connected = weechat.hdata_integer(hdata, server, "is_connected") +# nick_modes = weechat.hdata_string(hdata, server, "nick_modes") + buffer_ptr = weechat.hdata_pointer(hdata, server, 'buffer') + if buffer_ptr: # buffer pointer exists? + weechat.command(buffer_ptr,'/allchan -current /buffer unhide') # yes! + return + +def single_channel_exclude(): + if OPTIONS['single_channel_exclude']: + # space separated list for /buffer unhide + weechat.command('','/buffer unhide %s' % OPTIONS['single_channel_exclude']) + return +# ================================[ weechat options & description ]=============================== +def init_options(): + for option,value in list(OPTIONS.items()): + weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value[0]) + OPTIONS[option] = value[0] + else: + OPTIONS[option] = weechat.config_get_plugin(option) + +def toggle_refresh(pointer, name, value): + global OPTIONS + option = name[len('plugins.var.python.' + SCRIPT_NAME + '.'):] # get optionname + OPTIONS[option] = value # save new value + + # TODO how about matrix script or other non-irc channel buffer? no idea! help is welcome + server = weechat.buffer_get_string(weechat.current_buffer(), 'localvar_server') + server_ptr = weechat.buffer_search('irc', 'server.%s' % server) + buffer_switch_cb('', '', server_ptr) + return weechat.WEECHAT_RC_OK + +# unhide all buffers when script unloads +def shutdown_cb(): + weechat.command('', '/buffer unhide -all') + return weechat.WEECHAT_RC_OK +# ================================[ main ]=============================== +if __name__ == "__main__": + global version + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, 'shutdown_cb', ''): + weechat.hook_command(SCRIPT_NAME,SCRIPT_DESC, + '', + "Note: channels from disconnected server will be displayed and won't hidden automatically.\n" + '- This script only affects channels from irc plugin.\n' + '- Use the /fset plugin to configure script: /fset collapse_channel', + '', + '', + '') + + version = weechat.info_get('version_number', '') or 0 + init_options() + weechat.hook_config('plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '' ) + + if OPTIONS['activity'].lower() == 'no' or OPTIONS['activity'].lower() == 'off' or OPTIONS['activity'].lower() == '0': + # hide all channels + weechat.command('','/allchan -exclude=%s /buffer hide' % OPTIONS['channel_exclude']) + # show channel from current server + server = weechat.buffer_get_string(weechat.current_buffer(), 'localvar_server') + if server: + weechat.command(server,'/allchan -current /buffer unhide') + exclude_server() + single_channel_exclude() + else: + weechat.command('','/allchan /buffer hide') + exclude_hotlist() + + weechat.hook_signal('buffer_switch', 'buffer_switch_cb', '') + weechat.hook_signal('buffer_opened', 'buffer_opened_closed_cb', '') + weechat.hook_signal('buffer_closed', 'buffer_opened_closed_cb', '') + weechat.hook_signal('window_switch', 'window_switch_cb', '') + weechat.hook_signal('irc_server_connected', 'irc_server_connected_cb', '') + weechat.hook_signal('irc_server_disconnected', 'irc_server_disconnected_cb', '') + weechat.hook_signal('hotlist_changed', 'hotlist_changed_cb', '') diff --git a/python/colorize_nicks.py b/python/colorize_nicks.py index 506a3abd..d441420e 100644 --- a/python/colorize_nicks.py +++ b/python/colorize_nicks.py @@ -1,6 +1,7 @@ -# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: 2010 xt +# SPDX-FileCopyrightText: 2025 ryoskzypu # -# Copyright (c) 2010 by xt +# SPDX-License-Identifier: GPL-3.0-or-later # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,7 +21,35 @@ # not just in the prefix section. # # +# Bugs: +# https://github.com/ryoskzypu/weechat_scripts +# # History: +# 2025-05-08: ryoskzypu +# version 33: add many improvements, features, and fixes +# 2023-10-30: Sébastien Helleu +# version 32: revert to info "nick_color" with WeeChat >= 4.1.1 +# 2023-10-16: Sébastien Helleu +# version 31: use info "irc_nick_color" on IRC buffers with WeeChat >= 4.1.0 +# 2022-11-07: mva +# version 30: add ":" and "," to VALID_NICK regexp, +# to don't reset colorization in input_line +# 2022-07-11: ncfavier +# version 29: check nick for exclusion *after* stripping +# decrease minimum min_nick_length to 1 +# 2020-11-29: jess +# version 28: fix ignore_tags having been broken by weechat 2.9 changes +# 2020-05-09: Sébastien Helleu +# version 27: add compatibility with new weechat_print modifier data +# (WeeChat >= 2.9) +# 2018-04-06: Joey Pabalinas +# version 26: fix freezes with too many nicks in one line +# 2018-03-18: nils_2 +# version 25: fix unable to run function colorize_config_reload_cb() +# 2017-06-20: lbeziaud +# version 24: colorize utf8 nicks +# 2017-03-01, arza +# version 23: don't colorize nicklist group names # 2016-05-01, Simmo Saan # version 22: invalidate cached colors on hash algorithm change # 2015-07-28, xt @@ -67,284 +96,848 @@ # version 0.2: use ignore_channels when populating to increase performance. # 2010-02-03, xt # version 0.1: initial (based on ruby script by dominikh) -# -# Known issues: nicks will not get colorized if they begin with a character -# such as ~ (which some irc networks do happen to accept) import weechat import re -w = weechat -SCRIPT_NAME = "colorize_nicks" -SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "22" -SCRIPT_LICENSE = "GPL" -SCRIPT_DESC = "Use the weechat nick colors in the chat area" +# Debug data structures. +#from pprint import PrettyPrinter +#pp = PrettyPrinter(indent=4) -VALID_NICK = r'([@~&!%+])?([-a-zA-Z0-9\[\]\\`_^\{|\}]+)' -valid_nick_re = re.compile(VALID_NICK) -ignore_channels = [] -ignore_nicks = [] +w = weechat -# Dict with every nick on every channel with its color as lookup value -colored_nicks = {} +SCRIPT_NAME = 'colorize_nicks' +SCRIPT_AUTHOR = 'xt ' +SCRIPT_VERSION = '33' +SCRIPT_LICENSE = 'GPL' +SCRIPT_DESC = 'Use the weechat nick colors in the chat area' -CONFIG_FILE_NAME = "colorize_nicks" +# Config file/options +config_file = '' # Pointer +config_option = {} +ignore_channels = [] # ignore_channels +ignore_nicks = [] # ignore_nicks -# config file and options -colorize_config_file = "" -colorize_config_option = {} +# Dict with every nick on every channel, with its color and prefix as lookup values. +colored_nicks = {} -def colorize_config_init(): +# Regexes + +colors_rgx = r''' + \031 + (?: + \d{2} # Fixed 'weechat.color.chat.*' codes + | + (?: # Foreground + [F*] + [*!\/_%.|]? # IRC colors (00–15) + \d{2} + | + (?: F@ | \*@) # IRC colors (16–99) and WeeChat colors (16–255) + [*!\/_%.|]? + \d{5} + ) + (?: # Background + ~ + (?: \d{2} | @\d{5}) + )? + ) + ''' +attr_rgx = r''' + (?: \032 | \033) + [\001-\006] + | + \031\034 # Reset color and keep attributes + ''' +reset_rgx = r'\034' +split_rgx = rf''' + ({colors_rgx}) # Colors + | + ({attr_rgx}) # Attributes + | + ({reset_rgx}) # Reset all + | + # Chars + ''' +has_colors_rgx = rf'{colors_rgx} | {attr_rgx}' +is_color_rgx = rf'\A(?: {has_colors_rgx})\Z' +exact_color_rgx = rf'\A{colors_rgx}\Z' + +# Dict of regexes to compile. +regex = { + 'colors': colors_rgx, + 'attr': attr_rgx, + 'reset': reset_rgx, + 'split': split_rgx, + 'has_colors': has_colors_rgx, + 'is_color': is_color_rgx, + 'exact_color': exact_color_rgx, +} + +# Reset color code +reset = w.color('reset') + +# Space hex code +space = '\x20' + +# Unique escape codes +uniq_esc_nick = '\36' # Nick +uniq_esc_pref = '\37' # Prefix + +def config_init(): ''' Initialization of configuration file. Sections: look. ''' - global colorize_config_file, colorize_config_option - colorize_config_file = weechat.config_new(CONFIG_FILE_NAME, - "colorize_config_reload_cb", "") - if colorize_config_file == "": - return - - # section "look" - section_look = weechat.config_new_section( - colorize_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "", "") - if section_look == "": - weechat.config_free(colorize_config_file) - return - colorize_config_option["blacklist_channels"] = weechat.config_new_option( - colorize_config_file, section_look, "blacklist_channels", - "string", "Comma separated list of channels", "", 0, 0, - "", "", 0, "", "", "", "", "", "") - colorize_config_option["blacklist_nicks"] = weechat.config_new_option( - colorize_config_file, section_look, "blacklist_nicks", - "string", "Comma separated list of nicks", "", 0, 0, - "so,root", "so,root", 0, "", "", "", "", "", "") - colorize_config_option["min_nick_length"] = weechat.config_new_option( - colorize_config_file, section_look, "min_nick_length", - "integer", "Minimum length nick to colorize", "", - 2, 20, "", "", 0, "", "", "", "", "", "") - colorize_config_option["colorize_input"] = weechat.config_new_option( - colorize_config_file, section_look, "colorize_input", - "boolean", "Whether to colorize input", "", 0, - 0, "off", "off", 0, "", "", "", "", "", "") - colorize_config_option["ignore_tags"] = weechat.config_new_option( - colorize_config_file, section_look, "ignore_tags", - "string", "Comma separated list of tags to ignore; i.e. irc_join,irc_part,irc_quit", "", 0, 0, - "", "", 0, "", "", "", "", "", "") - colorize_config_option["greedy_matching"] = weechat.config_new_option( - colorize_config_file, section_look, "greedy_matching", - "boolean", "If off, then use lazy matching instead", "", 0, - 0, "on", "on", 0, "", "", "", "", "", "") - colorize_config_option["ignore_nicks_in_urls"] = weechat.config_new_option( - colorize_config_file, section_look, "ignore_nicks_in_urls", - "boolean", "If on, don't colorize nicks inside URLs", "", 0, - 0, "off", "off", 0, "", "", "", "", "", "") - -def colorize_config_read(): - ''' Read configuration file. ''' - global colorize_config_file - return weechat.config_read(colorize_config_file) - -def colorize_nick_color(nick, my_nick): - ''' Retrieve nick color from weechat. ''' + + global config_file + + # Create config. + if (config_file := w.config_new(SCRIPT_NAME, '', '')) == '': + return 'failed to create config file' + + # Create 'look' section. + if (section_look := w.config_new_section( + config_file, 'look', 0, 0, '', '', '', '', '', '', '', '', '', '')) == '': + w.config_free(config_file) + return 'failed to create look section' + + # Create 'look' options. + + opts = [ + { + 'option': 'ignore_channels', + 'opt_type': 'string', + 'desc': 'comma separated list of channels to ignore', + 'str_val': '', + 'min_val': 0, + 'max_val': 0, + 'default': '', + 'value': '', + 'null_val': 0, + 'check_val_cb': '', + }, + { + 'option': 'ignore_nicks', + 'opt_type': 'string', + 'desc': 'comma separated list of nicks to ignore', + 'str_val': '', + 'min_val': 0, + 'max_val': 0, + 'default': '', + 'value': '', + 'null_val': 0, + 'check_val_cb': '', + }, + { + 'option': 'colorize_filter', + 'opt_type': 'boolean', + 'desc': 'colorize nicks in filtered messages from /filter', + 'str_val': '', + 'min_val': 0, + 'max_val': 0, + 'default': 'off', + 'value': 'off', + 'null_val': 0, + 'check_val_cb': '', + }, + { + 'option': 'colorize_input', + 'opt_type': 'boolean', + 'desc': 'colorize nicks in input', + 'str_val': '', + 'min_val': 0, + 'max_val': 0, + 'default': 'off', + 'value': 'off', + 'null_val': 0, + 'check_val_cb': '', + }, + { + 'option': 'irc_only', + 'opt_type': 'boolean', + 'desc': 'ignore non IRC messages; i.e. set buffer restrictions: plugin = irc, tags = irc_privmsg and irc_notice, type = channel and private', + 'str_val': '', + 'min_val': 0, + 'max_val': 0, + 'default': 'off', + 'value': 'off', + 'null_val': 0, + 'check_val_cb': '', + }, + { + 'option': 'ignore_tags', + 'opt_type': 'string', + 'desc': 'comma separated list of tags to ignore; i.e. irc_join,irc_part,irc_quit', + 'str_val': '', + 'min_val': 0, + 'max_val': 0, + 'default': '', + 'value': '', + 'null_val': 0, + 'check_val_cb': '', + }, + { + 'option': 'min_nick_length', + 'opt_type': 'integer', + 'desc': 'minimum length of nicks to colorize', + 'str_val': '', + 'min_val': 1, + 'max_val': 20, + 'default': '1', + 'value': '1', + 'null_val': 0, + 'check_val_cb': '', + }, + { + 'option': 'nick_suffixes', + 'opt_type': 'string', + 'desc': 'character set of nick suffixes; matches only one out of several characters', + 'str_val': '', + 'min_val': 0, + 'max_val': 0, + 'default': ':,', + 'value': ':,', + 'null_val': 0, + 'check_val_cb': 'check_affix_cb', + }, + # Default charset is based on IRC channel membership prefixes. + { + 'option': 'nick_prefixes', + 'opt_type': 'string', + 'desc': 'character set of nick prefixes; matches only one out of several characters', + 'str_val': '', + 'min_val': 0, + 'max_val': 0, + 'default': '~&@%+', + 'value': '~&@%+', + 'null_val': 0, + 'check_val_cb': 'check_affix_cb', + } + ] + + if (rc := set_options(config_file, section_look, opts)): + return rc + +def set_options(config, section, options): + ''' Creates config file options of a section. ''' + + for i in options: + option = i['option'] + + config_option[option] = w.config_new_option( + config, + section, + option, + i['opt_type'], + i['desc'], + i['str_val'], + i['min_val'], + i['max_val'], + i['default'], + i['value'], + i['null_val'], + i['check_val_cb'], + '', '', '', '', '') + + if not config_option[option]: + return f"failed to create config '{option}' option" + +def config_read(): + ''' Reads the configuration file and updates config pointers. ''' + + rc = w.config_read(config_file) + + try: + if rc == w.WEECHAT_CONFIG_READ_MEMORY_ERROR: + raise ValueError('not enough memory to read config file') + elif rc == w.WEECHAT_CONFIG_READ_FILE_NOT_FOUND: + raise ValueError('config file was not found') + + except ValueError as err: + w.prnt('', f'{SCRIPT_NAME}\t{err.args[0]}') + raise + +def check_affix_cb(data, option, value): + ''' Checks if affix option is empty. Note that it must have a value, and space + (\x20) is ignored. ''' + + if value == '': + return 0 + + return 1 + +def compile_regexes(): + ''' Compiles all script regexes for reuse. ''' + + for k,v in regex.items(): + regex[k] = re.compile(v, flags=re.VERBOSE) + +def debug_str(var, string): + ''' Displays string information for debugging in core.weechat buffer. ''' + + w.prnt('', f'{var}:') + w.command('', f'/debug unicode {string}') + w.prnt('', '') + +def get_nick_color(buffer, nick, my_nick): + ''' Retrieves nick color code from weechat. ''' + if nick == my_nick: return w.color(w.config_string(w.config_get('weechat.color.chat_nick_self'))) else: - return w.info_get('irc_nick_color', nick) + version = int(w.info_get('version_number', '') or 0) -def colorize_cb(data, modifier, modifier_data, line): - ''' Callback that does the colorizing, and returns new line if changed ''' + # 'irc_nick_color' (deprecated since version 1.5, replaced by 'nick_color') + if w.buffer_get_string(buffer, 'plugin') == 'irc' and version == 0x4010000: + server = w.buffer_get_string(buffer, 'localvar_server') + return w.info_get('irc_nick_color', f'{server},{nick}') - global ignore_nicks, ignore_channels, colored_nicks + return w.info_get('nick_color', nick) +def colorize_priv_nicks(buffer): + ''' Colorizes nicks on IRC private buffers. ''' - full_name = modifier_data.split(';')[1] - channel = '.'.join(full_name.split('.')[1:]) + # Reset the buffer dict to update nicks changes, since there is no nicklist + # in private buffers. + colored_nicks[buffer] = {} - buffer = w.buffer_search('', full_name) - # Check if buffer has colorized nicks - if buffer not in colored_nicks: - return line + my_nick = w.buffer_get_string(buffer, 'localvar_nick') + priv_nick = w.buffer_get_string(buffer, 'localvar_channel') - if channel and channel in ignore_channels: - return line + for nick in my_nick, priv_nick: + nick_color = get_nick_color(buffer, nick, my_nick) - min_length = w.config_integer(colorize_config_option['min_nick_length']) - reset = w.color('reset') - - # Don't colorize if the ignored tag is present in message - tags_line = modifier_data.rsplit(';') - if len(tags_line) >= 3: - tags_line = tags_line[2].split(',') - for i in w.config_string(colorize_config_option['ignore_tags']).split(','): - if i in tags_line: - return line - - for words in valid_nick_re.findall(line): - nick = words[1] - # Check that nick is not ignored and longer than minimum length - if len(nick) < min_length or nick in ignore_nicks: + colored_nicks[buffer][nick] = { + 'color': nick_color, + 'prefix': '', + } + +def colorize_nicks(buffer, min_len, prefixes, suffixes, has_colors, line): + ''' Finds every nick from the dict of colored nicks, in the line and colorizes + them. ''' + + chop_line = line + chop_match = '' + chop_match_after = '' + color_match = '' + colorized_nicks_line = '' + nick_end = reset + + # Mark the nick's end with a unique escape to identify its position on preserve_colors(). + if has_colors is not None: + nick_end = uniq_esc_nick + + # Split words on spaces, since it is the most common word divider and is not + # valid in 'nicks' on popular protocols like IRC and matrix; thus protocols + # that allow spaces in 'nicks' are limited here. + for word in re.split(f'{space}+', line.strip(f'{space}')): + nick_prefix = '' # Reset nick prefix. + + if word == '': continue - # Check that nick is in the dictionary colored_nicks + # Get possible nick from word. + nicks_rgx = rf''' + [{prefixes}]? # Optional prefix char + (?P [^ ]+) + ''' + if (nick := re.search(nicks_rgx, word, flags=re.VERBOSE)) is not None: + nick = re.escape(nick.group('nick')) + + # If the word is not a known nick and its last character is an option + # suffix (e.g. colon ':' or comma ','), try to match the word without it. + # This is necessary as 'foo:' is a valid nick, which could be addressed + # as 'foo::'. + if nick not in colored_nicks[buffer]: + if (suffix := re.search(rf'[{suffixes}]$', nick)) is not None: + nick = nick[:-1] + + # Nick exists on buffer. if nick in colored_nicks[buffer]: - nick_color = colored_nicks[buffer][nick] - - # Let's use greedy matching. Will check against every word in a line. - if w.config_boolean(colorize_config_option['greedy_matching']): - for word in line.split(): - if w.config_boolean(colorize_config_option['ignore_nicks_in_urls']) and \ - word.startswith(('http://', 'https://')): - continue - - if nick in word: - # Is there a nick that contains nick and has a greater lenght? - # If so let's save that nick into var biggest_nick - biggest_nick = "" - for i in colored_nicks[buffer]: - if nick in i and nick != i and len(i) > len(nick): - if i in word: - # If a nick with greater len is found, and that word - # also happens to be in word, then let's save this nick - biggest_nick = i - # If there's a nick with greater len, then let's skip this - # As we will have the chance to colorize when biggest_nick - # iterates being nick. - if len(biggest_nick) > 0 and biggest_nick in word: - pass - elif len(word) < len(biggest_nick) or len(biggest_nick) == 0: - new_word = word.replace(nick, '%s%s%s' % (nick_color, nick, reset)) - line = line.replace(word, new_word) - # Let's use lazy matching for nick - else: - nick_color = colored_nicks[buffer][nick] - # The two .? are in case somebody writes "nick:", "nick,", etc - # to address somebody - regex = r"(\A|\s).?(%s).?(\Z|\s)" % re.escape(nick) - match = re.search(regex, line) - if match is not None: - new_line = line[:match.start(2)] + nick_color+nick+reset + line[match.end(2):] - line = new_line - return line + if nick in ignore_nicks or len(nick) < min_len: + continue + + # Get its color. + nick_color = colored_nicks[buffer][nick]['color'] + + # Find nick in the line. + line_rgx = rf''' + (?: \A | [ ]) # Boundary + (?P [{prefixes}])? # Optional prefix char + (?P {nick}) + [{suffixes}]? # " suffix char + (?: \Z | [ ]) # Boundary + ''' + + # Nick is found in the line. + if (line_match := re.search(line_rgx, chop_line, flags=re.VERBOSE)) is not None: + # In order to prevent the regex engine to needless find the nicks + # at previous match positions, preserve the state by chopping the + # line at the start and end positions of matches. + + # Start position of nick match. + start = line_match.start('nick') + + # Get the real nick prefix from nicklist. + if (pref_match := line_match.group('pref')) is not None: + nick_prefix = colored_nicks[buffer][nick]['prefix'] + + # If it exists, update the start position match. + if pref_match == w.string_remove_color(nick_prefix, ''): + start = line_match.start('pref') + + # Mark the prefix with a unique escape to idenfity its + # position on preserve_colors(). + if has_colors is not None: + nick_prefix = f'{uniq_esc_pref}{nick_prefix}' + else: + nick_prefix = '' + + # End position of nick match. + end = line_match.end('nick') + + # Chop + chop_till_match = chop_line[:end] + chop_after_match = chop_line[end:] + + # Concat the chopped strings while colorizing the nick, then update + # the chopped line. + nick_str = f'{nick_prefix}{nick_color}{nick}{nick_end}' + color_match += f'{chop_till_match[:start]}{nick_str}{chop_till_match[end:]}' + chop_line = chop_after_match + + if color_match: + colorized_nicks_line = f'{color_match}{chop_after_match}' + + return colorized_nicks_line + +def preserve_colors(line, colorized_nicks_line): + ''' + If the line string is already colored, captures every color code before the nick + match, for restoration after nick colorizing. Otherwise string colors after the + nick are reset. -def colorize_input_cb(data, modifier, modifier_data, line): - ''' Callback that does the colorizing in input ''' + Testing: + 1. Create an IRC channel. + /j ##testing-weechat + + 2. Create the nick 'nick111' with perlexec: + /perlexec my $buffer = weechat::buffer_search('==', 'irc.libera.##testing-weechat'); my $group = weechat::nicklist_add_group($buffer, '', 'test_group', 'weechat.color.nicklist_group', 1); weechat::nicklist_add_nick($buffer, $group, 'nick111', 'blue', '@', 'lightgreen', 1) + + 3. Send this message in the channel with script unloaded: + /input insert \x0305<\x03043 \x02\x0307nick111 is awesome\x02 \x0314[0 user] \x0399\x1fhttps://github.com/ \x0305n\x0355i\x0384c\x0302k\x0f\x03921\x03091\x03381 /weechat/ https\x1f\x16:// nick111 .org/ + + 4. Repeat step 3 with the script loaded. It should colorize the nicks and + preserve all colors. + The string is inspired by ##hntop messages and modified to cover some corner cases. + ''' + + new_line = '' + split_line = [] + split_line_nc = [] + color_codes = '' + idx = 0 + match = 0 + + # Split all color codes and bytes from the lines. + split_line = [x for x in regex['split'].split(line) if x is not None and x] + split_line_nc = [y for y in regex['split'].split(colorized_nicks_line) if y is not None and y] + + # Debug split lists. + #w.prnt('', f'split_line:' + pp.pformat(split_line)) + #w.prnt('', f'split_line_nc:' + pp.pformat(split_line_nc)) + + # Iterate through the original split list, comparing every char against the + # uncolored list; while reconstructing the new line with saved color codes. + for i in split_line: + #w.prnt('', f'i: ' + pp.pformat(f'{i}')) + #w.prnt('', f"split_line_nc[{idx}]: " + pp.pformat(f'{split_line_nc[idx]}')) + + # It is a color code, so append its codes to be restored. + if regex['is_color'].search(i) is not None: + color_codes += i + #w.prnt('', f'color_codes: ' + pp.pformat(f'{color_codes}')) + + # Append the codes if not inside a nick match. + if not match: + new_line += i + + continue + # Remove saved codes if a reset code is found. + elif i == reset: + if not match: + new_line += i + + color_codes = '' + continue + elif split_line_nc[idx]: + # It is a char, so compare it against the uncolored's char. + if i == split_line_nc[idx]: + new_line += i + idx += 1 + + continue + # If the char is in a nick match and uncolored's is a unique nick + # escape code, restore the saved codes, then advance the index. + elif match and split_line_nc[idx] == uniq_esc_nick: + #w.prnt('', f"split_line_nc[{idx} + 1]: " + pp.pformat(f'{split_line_nc[idx + 1]}')) + + # If the chars match, advance the index. + if split_line_nc[idx + 1] == i: + new_line += f'{reset}{color_codes}{i}' + idx += 2 + match = 0 + + continue + # It is a unique prefix escape code, so get its color code and char, + # then advance uncolored's index to the start of colorized nick match. + elif split_line_nc[idx] == uniq_esc_pref: + prefix = f'{split_line_nc[idx + 1]}{i}' + new_line += prefix + idx += 3 + + continue + # It is the start of a colorized nick match, so colorize the new line, + # then advance uncolored's index to the current char. + elif (split_match := regex['exact_color'].search(split_line_nc[idx])) is not None: + #w.prnt('', f"split_line_nc[{idx} + 1]: " + pp.pformat(f'{split_line_nc[idx + 1]}')) + + nick_color = split_match.group(0) + idx += 1 + new_line += f'{reset}{nick_color}{split_line_nc[idx]}' + match = 1 + + # If the chars match, advance the index. + if i == split_line_nc[idx]: + idx += 1 + continue + + return new_line + +def init_colorize(buffer, message): + ''' Initializes the process of nicks colorizing. ''' + + colorized_nicks_msg = '' + new_msg = '' + + # Get options. + min_len = w.config_integer(config_option['min_nick_length']) + pref_charset = re.escape(w.config_string(config_option['nick_prefixes'])) + suff_charset = re.escape(w.config_string(config_option['nick_suffixes'])) + + # Check if message has color codes. + has_colors = regex['has_colors'].search(message) + + # Remove any color codes from message in order to match and colorize the strings correctly. + msg_nocolor = w.string_remove_color(message, '') + + # Find and colorize the nicks. + colorized_nicks_msg = colorize_nicks(buffer, min_len, pref_charset, suff_charset, has_colors, msg_nocolor) + + # Preserve colors from message. + if has_colors is not None and colorized_nicks_msg: + new_msg = preserve_colors(message, colorized_nicks_msg) + + # Debug the message string. + #debug_str('message', message) + + # Update the message. + + if colorized_nicks_msg: + #debug_str('colorized_nicks_msg', colorized_nicks_msg) + message = colorized_nicks_msg + + if new_msg: + #debug_str('new_msg', new_msg) + message = new_msg + + return message + +def colorize_cb(data, hashtable): + ''' + Callback that does the colorizing of nicks from messages and returns a new message. + + Testing: + 1. Create an IRC channel: + /j ##testing-weechat + + 2. Create the nicks: alice, :alicee, alicee:, :alicee:, and utf8©nick + with perlexec: + /perlexec my @nicks = qw(alice :alicee alicee: :alicee: utf8©nick); my $buffer = weechat::buffer_search('==', 'irc.libera.##testing-weechat'); my $group = weechat::nicklist_add_group($buffer, '', 'test_group', 'weechat.color.nicklist_group', 1); foreach my $i (@nicks) { weechat::nicklist_add_nick($buffer, $group, $i, 'default', '@', 'lightgreen', 1) } + + 3. Then paste and send this string: + hey alicee and utf8©nickz, how are you? sorry, alice and utf8©nick @alicee: @:alicee @:alicee: aaaliceee @:alicee:: @::alicee:: @alicee:: %alicee:, ~:alicee, Nice to meet you @:alicee,, &:alicee:, @:alicee,: :alicee, :alicee alicee: <3 :alicee: :alicee::: +utf8©nick: :-) bye + + 4. The colors that matter are in the message, so ignore the static nicklist + colors. The nicks in message should be colorized correctly based on weechat's + color algorithm, and respect the script affixes. + Insert a reverse color code (^Cv) at the beggining of string, if having + trouble on seeing the colors. + ''' + + buffer = hashtable['buffer'] + tags = hashtable['tags'].split(',') + displayed = hashtable['displayed'] + message = hashtable['message'] + + plugin = w.buffer_get_string(buffer, 'localvar_plugin') + bufname = w.buffer_get_string(buffer, 'localvar_name') + buftype = w.buffer_get_string(buffer, 'localvar_type') + channel = w.buffer_get_string(buffer, 'localvar_channel') + + irc_only = w.config_boolean(config_option['irc_only']) + + # Colorize only IRC user messages. + if plugin == 'irc' or irc_only and plugin != 'irc': + # There is no point in colorizing non channel/private buffers, and IRC + # tags other than 'irc_privmsg/notice', since tags i.e. irc_join/part/quit + # are already colored. + if buftype != 'channel' and buftype != 'private' or tags[0] != 'irc_privmsg' and tags[0] != 'irc_notice': + return hashtable + + # Colorize nicks on IRC private buffers. + if plugin == 'irc' and buftype == 'private': + colorize_priv_nicks(buffer) + + # Check if buffer has colorized nicks. + if not colored_nicks.get(buffer): + return hashtable + + # Check if channel is ignored. + if channel and channel in ignore_channels: + return hashtable - global ignore_nicks, ignore_channels, colored_nicks + # Do not colorize if an ignored tag is present in message. + tag_ignores = w.config_string(config_option['ignore_tags']).split(',') + for tag in tags: + if tag in tag_ignores: + return hashtable - min_length = w.config_integer(colorize_config_option['min_nick_length']) + # Do not colorize if message is filtered. + if displayed == '0' and not w.config_boolean(config_option['colorize_filter']): + return hashtable - if not w.config_boolean(colorize_config_option['colorize_input']): + # Init colorizing process. + message = init_colorize(buffer, message) + + # Debug the hashtable. + #w.prnt('', 'hashtable:\n' + pp.pformat(hashtable)) + + # Update the hashtable. + hashtable['message'] = message + + return hashtable + +def colorize_input_cb(data, modifier, modifier_data, line): + ''' Callback that does the colorizing of nicks from weechat's input. ''' + + if not w.config_boolean(config_option['colorize_input']): return line - buffer = w.current_buffer() - # Check if buffer has colorized nicks - if buffer not in colored_nicks: + buffer = w.current_buffer() + plugin = w.buffer_get_string(buffer, 'localvar_plugin') + buftype = w.buffer_get_string(buffer, 'localvar_type') + channel = w.buffer_get_string(buffer, 'localvar_channel') + + irc_only = w.config_boolean(config_option['irc_only']) + + # Colorize only IRC user messages. + if plugin == 'irc' or irc_only and plugin != 'irc': + # There is no point in colorizing non channel/private buffers. + if buftype != 'channel' and buftype != 'private': + return line + + # Check if current buffer has colorized nicks. + if not colored_nicks.get(buffer): return line - channel = w.buffer_get_string(buffer, 'name') + # Check if current channel is ignored. if channel and channel in ignore_channels: return line - reset = w.color('reset') + # Decode IRC colors from input. + if plugin == 'irc': + line = w.hook_modifier_exec('irc_color_decode', '1', line) - for words in valid_nick_re.findall(line): - nick = words[1] - # Check that nick is not ignored and longer than minimum length - if len(nick) < min_length or nick in ignore_nicks: - continue - if nick in colored_nicks[buffer]: - nick_color = colored_nicks[buffer][nick] - line = line.replace(nick, '%s%s%s' % (nick_color, nick, reset)) + # Init colorizing process. + line = init_colorize(buffer, line) return line -def populate_nicks(*args): - ''' Fills entire dict with all nicks weechat can see and what color it has - assigned to it. ''' - global colored_nicks +def populate_nicks_cb(*args): + ''' Callback that fills the colored nicks dict with all nicks weechat can see, + and what color and prefix it has assigned to it. ''' - colored_nicks = {} + bufname = '' + prefix_color = '' + nick_prefix = '' + irc_only = w.config_boolean(config_option['irc_only']) + + # Get nicks only in IRC buffers. + if irc_only: + bufname = 'irc.*' + + # Get list of buffers. + if not (buffers := w.infolist_get('buffer', '', bufname)): + w.prnt('', f'{SCRIPT_NAME}\tfailed to get list of buffers') + return w.WEECHAT_RC_ERROR - buffers = w.infolist_get('buffer', '', '') while w.infolist_next(buffers): buffer_ptr = w.infolist_pointer(buffers, 'pointer') + channel = w.buffer_get_string(buffer_ptr, 'localvar_channel') + + # Skip non-IRC channel buffers. + if irc_only and not w.info_get('irc_is_channel', channel): + continue + my_nick = w.buffer_get_string(buffer_ptr, 'localvar_nick') - nicklist = w.infolist_get('nicklist', buffer_ptr, '') - while w.infolist_next(nicklist): - if buffer_ptr not in colored_nicks: - colored_nicks[buffer_ptr] = {} - nick = w.infolist_string(nicklist, 'name') - nick_color = colorize_nick_color(nick, my_nick) + if (nicklist := w.infolist_get('nicklist', buffer_ptr, '')): + while w.infolist_next(nicklist): + if buffer_ptr not in colored_nicks: + colored_nicks[buffer_ptr] = {} - colored_nicks[buffer_ptr][nick] = nick_color + # Skip nick groups. + if w.infolist_string(nicklist, 'type') != 'nick': + continue + + # Get nicks colors. + nick = w.infolist_string(nicklist, 'name') + nick_color = get_nick_color(buffer_ptr, nick, my_nick) + + # Get nicks prefixes. + prefix = w.infolist_string(nicklist, 'prefix') + if prefix != space: + prefix_color = w.color(w.infolist_string(nicklist, 'prefix_color')) + nick_prefix = f'{prefix_color}{prefix}' + + # Populate + colored_nicks[buffer_ptr][nick] = { + 'color': nick_color, + 'prefix': nick_prefix, + } + nick_prefix = '' w.infolist_free(nicklist) w.infolist_free(buffers) + #w.prnt('', 'colored_nicks:\n' + pp.pformat(colored_nicks)) + return w.WEECHAT_RC_OK -def add_nick(data, signal, type_data): - ''' Add nick to dict of colored nicks ''' - global colored_nicks +def add_nick_cb(data, signal, signal_data): + ''' Callback that adds a nick to the dict of colored nicks, when a nick is + added to the nicklist. ''' - # Nicks can have , in them in some protocols - splitted = type_data.split(',') - pointer = splitted[0] - nick = ",".join(splitted[1:]) - if pointer not in colored_nicks: - colored_nicks[pointer] = {} + # Nicks can have ',' in them in some protocols. + buffer, nick = signal_data.split(',', maxsplit=1) - my_nick = w.buffer_get_string(pointer, 'localvar_nick') - nick_color = colorize_nick_color(nick, my_nick) + if buffer not in colored_nicks: + colored_nicks[buffer] = {} - colored_nicks[pointer][nick] = nick_color + # Get nick color. + my_nick = w.buffer_get_string(buffer, 'localvar_nick') + nick_color = get_nick_color(buffer, nick, my_nick) + + # Get nick prefix. + nick_prefix = '' + if (nicklist := w.infolist_get('nicklist', buffer, f'nick_{nick}')): + while w.infolist_next(nicklist): + prefix = w.infolist_string(nicklist, 'prefix') + + if prefix != space: + prefix_color = w.color(w.infolist_string(nicklist, 'prefix_color')) + nick_prefix = f'{prefix_color}{prefix}' + + # Update + colored_nicks[buffer][nick] = { + 'color': nick_color, + 'prefix': nick_prefix, + } + + w.infolist_free(nicklist) + w.infolist_free(buffer) + + #w.prnt('', 'colored_nicks:\n' + pp.pformat(colored_nicks)) + + return w.WEECHAT_RC_OK + +def remove_nick_cb(data, signal, signal_data): + ''' Callback that removes a nick from the dict of colored nicks, when a nick is + removed from the nicklist. ''' + + # Nicks can have ',' in them in some protocols. + buffer, nick = signal_data.split(',', maxsplit=1) + + if buffer in colored_nicks and nick in colored_nicks[buffer]: + del colored_nicks[buffer][nick] + + #w.prnt('', 'colored_nicks:\n' + pp.pformat(colored_nicks)) return w.WEECHAT_RC_OK -def remove_nick(data, signal, type_data): - ''' Remove nick from dict with colored nicks ''' - global colored_nicks +def remove_priv_buffer_cb(data, signal, buffer): + ''' Callback that removes an IRC private buffer from the dict of colored nicks, + when the buffer is closing. ''' + + # For some reason, weechat crashes if the hook signal is set to 'buffer_closed' + # while trying to get the 'localvar_*' strings. + # Perhaps the buffer pointer is not valid anymore because it was closed? + plugin = w.buffer_get_string(buffer, 'localvar_plugin') + buftype = w.buffer_get_string(buffer, 'localvar_type') - # Nicks can have , in them in some protocols - splitted = type_data.split(',') - pointer = splitted[0] - nick = ",".join(splitted[1:]) + if plugin == 'irc' and buftype == 'private' and buffer in colored_nicks: + del colored_nicks[buffer] - if pointer in colored_nicks and nick in colored_nicks[pointer]: - del colored_nicks[pointer][nick] + #w.prnt('', 'colored_nicks:\n' + pp.pformat(colored_nicks)) return w.WEECHAT_RC_OK -def update_blacklist(*args): - ''' Set the blacklist for channels and nicks. ''' +def update_blacklist_cb(*args): + ''' Callback that sets the blacklist for channels and nicks. ''' + global ignore_channels, ignore_nicks - ignore_channels = w.config_string(colorize_config_option['blacklist_channels']).split(',') - ignore_nicks = w.config_string(colorize_config_option['blacklist_nicks']).split(',') + + ignore_channels = w.config_string(config_option['ignore_channels']).split(',') + ignore_nicks = w.config_string(config_option['ignore_nicks']).split(',') + return w.WEECHAT_RC_OK -if __name__ == "__main__": - if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, "", ""): - colorize_config_init() - colorize_config_read() - - # Run once to get data ready - update_blacklist() - populate_nicks() - - w.hook_signal('nicklist_nick_added', 'add_nick', '') - w.hook_signal('nicklist_nick_removed', 'remove_nick', '') - w.hook_modifier('weechat_print', 'colorize_cb', '') - # Hook config for changing colors - w.hook_config('weechat.color.chat_nick_colors', 'populate_nicks', '') - w.hook_config('weechat.look.nick_color_hash', 'populate_nicks', '') - # Hook for working togheter with other scripts (like colorize_lines) - w.hook_modifier('colorize_nicks', 'colorize_cb', '') - # Hook for modifying input - w.hook_modifier('250|input_text_display', 'colorize_input_cb', '') - # Hook for updating blacklist (this could be improved to use fnmatch) - weechat.hook_config('%s.look.blacklist*' % SCRIPT_NAME, 'update_blacklist', '') +if __name__ == '__main__': + if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): + # Initialize config options and regexes. + try: + if (msg := config_init()): + raise ValueError(msg) + + except ValueError as err: + w.prnt('', f'{SCRIPT_NAME}\t{err.args[0]}') + raise + + config_read() + compile_regexes() + + # Run once to get data ready. + update_blacklist_cb() + populate_nicks_cb() + + # Hooks + + # Colorize nicks. + w.hook_line('', '', '', 'colorize_cb', '') # Message + w.hook_modifier('250|input_text_display', 'colorize_input_cb', '') # Input + + # Update nicks. + w.hook_signal('nicklist_nick_added', 'add_nick_cb', '') + w.hook_signal('nicklist_nick_removed', 'remove_nick_cb', '') + w.hook_signal('buffer_closing', 'remove_priv_buffer_cb', '') + + # Repopulate nicks on colors changes from weechat's options. + w.hook_config('weechat.color.chat_nick_colors', 'populate_nicks_cb', '') + w.hook_config('weechat.look.nick_color_hash', 'populate_nicks_cb', '') + w.hook_config('irc.color.nick_prefixes', 'populate_nicks_cb', '') + + # Update blacklists. + w.hook_config(f'{SCRIPT_NAME}.look.ignore_*', 'update_blacklist_cb', '') diff --git a/python/completion.py b/python/completion.py index 62a69ae7..e4d6402b 100644 --- a/python/completion.py +++ b/python/completion.py @@ -31,6 +31,10 @@ # # # History: +# 2019-08-20 +# version 0.3: Ben Harris (benharri) +# * port for python3 +# # 2010-05-08 # version 0.2: # * complete any word behind the cursor, not just the last one in input line. @@ -46,13 +50,13 @@ WEECHAT_RC_OK = weechat.WEECHAT_RC_OK import_ok = True except ImportError: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") import_ok = False SCRIPT_NAME = "completion" SCRIPT_AUTHOR = "Elián Hanisch " -SCRIPT_VERSION = "0.2" +SCRIPT_VERSION = "0.3" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Word completions for WeeChat" SCRIPT_COMMAND = "completion" @@ -63,16 +67,6 @@ } ### Messages ### -def decode(s): - if isinstance(s, str): - s = s.decode('utf-8') - return s - -def encode(u): - if isinstance(u, unicode): - u = u.encode('utf-8') - return u - def debug(s, prefix='', buffer=None): """Debug msg""" #if not weechat.config_get_plugin('debug'): return @@ -84,13 +78,11 @@ def debug(s, prefix='', buffer=None): weechat.buffer_set(buffer, 'nicklist', '0') weechat.buffer_set(buffer, 'time_for_each_line', '0') weechat.buffer_set(buffer, 'localvar_set_no_log', '1') - s = encode(s) weechat.prnt(buffer, '%s\t%s' %(prefix, s)) def error(s, prefix=None, buffer='', trace=''): """Error msg""" prefix = prefix or script_nick - s = encode(s) weechat.prnt(buffer, '%s%s %s' %(weechat.prefix('error'), prefix, s)) if weechat.config_get_plugin('debug'): if not trace: @@ -102,67 +94,34 @@ def error(s, prefix=None, buffer='', trace=''): def say(s, prefix=None, buffer=''): """normal msg""" prefix = prefix or script_nick - s = encode(s) weechat.prnt(buffer, '%s\t%s' %(prefix, s)) print_replace = lambda k,v : say('%s %s=>%s %s' %(k, color_delimiter, color_reset, v)) ### Config functions ### -class UTFDict(dict): - decode = staticmethod(decode) - encode = staticmethod(encode) - - def __init__(self, d={}): - dict.__init__(self) - for k, v in d.iteritems(): - self[k] = v - - def __setitem__(self, k, v): - k = self.decode(k) - v = self.decode(v) - dict.__setitem__(self, k, v) - - def __getitem__(self, k): - k = self.decode(k) - return dict.__getitem__(self, k) - - def __delitem__(self, k): - k = self.decode(k) - dict.__delitem__(self, k) - - def __contains__(self, k): - k = self.decode(k) - return dict.__contains__(self, k) - - def __str__(self): - values = [ '%s=>%s' %(k, v) for k, v in self.iteritems() ] - values = ';;'.join(values) - return self.encode(values) - - def get_config_dict(config): value = weechat.config_get_plugin(config) if not value: return {} - values = value.split(';;') - values = map(lambda s: s.split('=>'), values) + values = [s.split('=>') for s in value.split(';;')] #debug(values) return dict(values) def load_replace_table(): global replace_table - replace_table = UTFDict(get_config_dict('replace_values')) + replace_table = dict(get_config_dict('replace_values')) def save_replace_table(): global replace_table - weechat.config_set_plugin('replace_values', str(replace_table)) + weechat.config_set_plugin('replace_values', + ';;'.join(['%s=>%s' %(k, v) for k, v in replace_table.items()])) ### Commands ### def cmd_completion(data, buffer, args): global replace_table if not args: if replace_table: - for k, v in replace_table.iteritems(): + for k, v in replace_table.items(): print_replace(k, v) else: say('No completions.') @@ -189,7 +148,7 @@ def cmd_completion(data, buffer, args): def completion_replacer(data, completion_item, buffer, completion): global replace_table pos = weechat.buffer_get_integer(buffer, 'input_pos') - input = decode(weechat.buffer_get_string(buffer, 'input')) + input = weechat.buffer_get_string(buffer, 'input') #debug('%r %s %s' %(input, len(input), pos)) if pos > 0 and (pos == len(input) or input[pos] == ' '): n = input.rfind(' ', 0, pos) @@ -202,14 +161,14 @@ def completion_replacer(data, completion_item, buffer, completion): replace += ' ' n = len(word) input = '%s%s%s' %(input[:pos-n], replace, input[pos:]) - weechat.buffer_set(buffer, 'input', encode(input)) + weechat.buffer_set(buffer, 'input', input) weechat.buffer_set(buffer, 'input_pos', str(pos - n + len(replace))) return WEECHAT_RC_OK def completion_keys(data, completion_item, buffer, completion): global replace_table for k in replace_table: - weechat.hook_completion_list_add(completion, encode(k), 0, weechat.WEECHAT_LIST_POS_SORT) + weechat.hook_completion_list_add(completion, k, 0, weechat.WEECHAT_LIST_POS_SORT) return WEECHAT_RC_OK ### Main ### @@ -231,7 +190,7 @@ def completion_keys(data, completion_item, buffer, completion): error('WeeChat 0.3.1 or newer is required for this script.') else: # settings - for opt, val in settings.iteritems(): + for opt, val in settings.items(): if not weechat.config_is_set_plugin(opt): weechat.config_set_plugin(opt, val) diff --git a/python/confversion.py b/python/confversion.py index d41a357b..dd453ce4 100644 --- a/python/confversion.py +++ b/python/confversion.py @@ -22,10 +22,12 @@ # 0.1 drubin - First release. # - Basic functionality to save version history of your config files (only git, bzr) # 0.2 ShockkPony - Fixed massive weechat startup time caused by initial config loading +# 0.3 noctux - Adapt to python 3 +# 0.4 FlashCode - Add compatibility with WeeChat >= 3.2 (XDG directories) SCRIPT_NAME = "confversion" SCRIPT_AUTHOR = "drubin " -SCRIPT_VERSION = "0.2" +SCRIPT_VERSION = "0.4" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Stores version controlled history of your configuration files" @@ -34,8 +36,8 @@ try: import weechat except ImportError: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") import_ok = False @@ -53,14 +55,15 @@ def shell_in_home(cmd): try: - output = file("/dev/null","w") + output = open("/dev/null","w") subprocess.Popen(ver_method()+" "+cmd, cwd = weechat_home(), stdout= output, stderr=output, shell=True) except Exception as e: - print e + print(e) def weechat_home(): - return weechat.info_get ("weechat_dir", "") + return weechat.info_get("weechat_config_dir", "") \ + or weechat.info_get("weechat_dir", "") def ver_method(): return weechat.config_get_plugin("versioning_method") @@ -113,7 +116,7 @@ def confversion_cmd(data, buffer, args): if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - for option, default_value in settings.iteritems(): + for option, default_value in settings.items(): if weechat.config_get_plugin(option) == "": weechat.config_set_plugin(option, default_value) diff --git a/python/correction_completion.py b/python/correction_completion.py index 1a7bf227..3c2a1656 100644 --- a/python/correction_completion.py +++ b/python/correction_completion.py @@ -29,6 +29,8 @@ # too. # Changelog: +# 2020-01-10 -- Version 0.4.0 +# - Transition to Python 3 # # 2012-10-18 -- Version 0.3.0 # - incorrect words have a higher priority (typo completion) @@ -55,17 +57,17 @@ import ctypes import ctypes.util except ImportError: - print "This script depends on ctypes" + print("This script depends on ctypes") try: import weechat as w except ImportError: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") SCRIPT_NAME = "correction_completion" SCRIPT_AUTHOR = "Pascal Wittmann " -SCRIPT_VERSION = "0.3.0" +SCRIPT_VERSION = "0.4.0" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Provides a completion for 's/typo/correct'" SCRIPT_COMMAND = "correction_completion" @@ -227,7 +229,7 @@ def suggest(word): break; else: word = ctypes.c_char_p(wordptr) - list.append(str(word.value)) + list.append(word.value.decode('UTF-8')) aspell.delete_aspell_string_enumeration(elements) return list else: @@ -246,7 +248,7 @@ def load_config(data = "", option = "", value = ""): global speller config = aspell.new_aspell_config() - for option, default in settings.iteritems(): + for option, default in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default) value = w.config_get_plugin(option) diff --git a/python/country.py b/python/country.py deleted file mode 100644 index 530aa792..00000000 --- a/python/country.py +++ /dev/null @@ -1,577 +0,0 @@ -# -*- coding: utf-8 -*- -### -# Copyright (c) 2009-2011 by Elián Hanisch -# Copyright (c) 2013 by Filip H.F. "FiXato" Slagter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -### - -### -# Prints user's country and local time information in -# whois/whowas replies (for WeeChat 0.3.*) -# -# This script uses MaxMind's GeoLite database from -# http://www.maxmind.com/app/geolitecountry -# -# This script depends in pytz third party module for retrieving -# timezone information for a given country. Without it the local time -# for a user won't be displayed. -# Get it from http://pytz.sourceforge.net or from your distro packages, -# python-tz in Ubuntu/Debian -# -# Commands: -# * /country -# Prints country for a given ip, uri or nick. See /help country -# -# Settings: -# * plugins.var.python.country.show_in_whois: -# If 'off' /whois or /whowas replies won't contain country information. -# Valid values: on, off -# * plugins.var.python.country.show_localtime: -# If 'off' timezone and local time infomation won't be looked for. -# Valid values: on, off -# -# -# TODO -# * Add support for IPv6 addresses -# -# -# History: -# 2013-04-28 -# version 0.6: -# * Improved support for target msgbuffer. Takes the following settings into account: -# - irc.msgbuffer.whois -# - irc.msgbuffer.$servername.whois -# - irc.look.msgbuffer_fallback -# -# 2011-08-14 -# version 0.5: -# * make time format configurable. -# * print to private buffer based on msgbuffer setting. -# -# 2011-01-09 -# version 0.4.1: bug fixes -# -# 2010-11-15 -# version 0.4: -# * support for users using webchat (at least in freenode) -# * enable Archlinux workaround. -# -# 2010-01-11 -# version 0.3.1: bug fix -# * irc_nick infolist wasn't freed in get_host_by_nick() -# -# 2009-12-12 -# version 0.3: update WeeChat site. -# -# 2009-09-17 -# version 0.2: added timezone and local time information. -# -# 2009-08-24 -# version 0.1.1: fixed python 2.5 compatibility. -# -# 2009-08-21 -# version 0.1: initial release. -# -### - -SCRIPT_NAME = "country" -SCRIPT_AUTHOR = "Elián Hanisch " -SCRIPT_VERSION = "0.6" -SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Prints user's country and local time in whois replies" -SCRIPT_COMMAND = "country" - -try: - import weechat - from weechat import WEECHAT_RC_OK, prnt - import_ok = True -except ImportError: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" - import_ok = False - -try: - import pytz, datetime - pytz_module = True -except: - pytz_module = False - -import os, re, socket - -### ip database -database_url = 'http://geolite.maxmind.com/download/geoip/database/GeoIPCountryCSV.zip' -database_file = 'GeoIPCountryWhois.csv' - -### config -settings = { - 'time_format': '%x %X %Z', - 'show_in_whois': 'on', - 'show_localtime': 'on' - } - -boolDict = {'on':True, 'off':False} -def get_config_boolean(config): - value = weechat.config_get_plugin(config) - try: - return boolDict[value] - except KeyError: - default = settings[config] - error("Error while fetching config '%s'. Using default value '%s'." %(config, default)) - error("'%s' is invalid, allowed: 'on', 'off'" %value) - return boolDict[default] - -### messages - -script_nick = SCRIPT_NAME -def error(s, buffer=''): - """Error msg""" - prnt(buffer, '%s%s %s' % (weechat.prefix('error'), script_nick, s)) - if weechat.config_get_plugin('debug'): - import traceback - if traceback.sys.exc_type: - trace = traceback.format_exc() - prnt('', trace) - -def say(s, buffer=''): - """normal msg""" - prnt(buffer, '%s\t%s' % (script_nick, s)) - -def whois(nick, string, buffer=''): - """Message formatted like a whois reply.""" - prefix_network = weechat.prefix('network') - color_delimiter = weechat.color('chat_delimiters') - color_nick = weechat.color('chat_nick') - prnt(buffer, '%s%s[%s%s%s] %s' % (prefix_network, - color_delimiter, - color_nick, - nick, - color_delimiter, - string)) - -def string_country(country, code): - """Format for country info string.""" - color_delimiter = weechat.color('chat_delimiters') - color_chat = weechat.color('chat') - return '%s%s %s(%s%s%s)' % (color_chat, - country, - color_delimiter, - color_chat, - code, - color_delimiter) - -def string_time(dt): - """Format for local time info string.""" - if not dt: return '--' - color_delimiter = weechat.color('chat_delimiters') - color_chat = weechat.color('chat') - date = dt.strftime(weechat.config_get_plugin("time_format")) - tz = dt.strftime('UTC%z') - return '%s%s %s(%s%s%s)' % (color_chat, - date, - color_delimiter, - color_chat, - tz, - color_delimiter) - -### functions -def get_script_dir(): - """Returns script's dir, creates it if needed.""" - script_dir = weechat.info_get('weechat_dir', '') - script_dir = os.path.join(script_dir, 'country') - if not os.path.isdir(script_dir): - os.makedirs(script_dir) - return script_dir - -ip_database = '' -def check_database(): - """Check if there's a database already installed.""" - global ip_database - if not ip_database: - ip_database = os.path.join(get_script_dir(), database_file) - return os.path.isfile(ip_database) - -timeout = 1000*60*10 -hook_download = '' -def update_database(): - """Downloads and uncompress the database.""" - global hook_download, ip_database - if not ip_database: - check_database() - if hook_download: - weechat.unhook(hook_download) - hook_download = '' - script_dir = get_script_dir() - say("Downloading IP database...") - python_bin = weechat.info_get('python2_bin', '') or 'python' - hook_download = weechat.hook_process( - python_bin + " -c \"\n" - "import urllib2, zipfile, os, sys\n" - "try:\n" - " temp = os.path.join('%(script_dir)s', 'temp.zip')\n" - " try:\n" - " zip = urllib2.urlopen('%(url)s', timeout=10)\n" - " except TypeError: # python2.5\n" - " import socket\n" - " socket.setdefaulttimeout(10)\n" - " zip = urllib2.urlopen('%(url)s')\n" - " fd = open(temp, 'w')\n" - " fd.write(zip.read())\n" - " fd.close()\n" - " print 'Download complete, uncompressing...'\n" - " zip = zipfile.ZipFile(temp)\n" - " try:\n" - " zip.extractall(path='%(script_dir)s')\n" - " except AttributeError: # python2.5\n" - " fd = open('%(ip_database)s', 'w')\n" - " fd.write(zip.read('%(database_file)s'))\n" - " fd.close()\n" - " os.remove(temp)\n" - "except Exception, e:\n" - " print >>sys.stderr, e\n\"" % {'url':database_url, - 'script_dir':script_dir, - 'ip_database':ip_database, - 'database_file':database_file - }, - timeout, 'update_database_cb', '') - -process_stderr = '' -def update_database_cb(data, command, rc, stdout, stderr): - """callback for our database download.""" - global hook_download, process_stderr - #debug("%s @ stderr: '%s', stdout: '%s'" %(rc, stderr.strip('\n'), stdout.strip('\n'))) - if stdout: - say(stdout) - if stderr: - process_stderr += stderr - if int(rc) >= 0: - if process_stderr: - error(process_stderr) - process_stderr = '' - else: - say('Success.') - hook_download = '' - return WEECHAT_RC_OK - -hook_get_ip = '' -def get_ip_process(host): - """Resolves host to ip.""" - # because getting the ip might take a while, we must hook a process so weechat doesn't hang. - global hook_get_ip - if hook_get_ip: - weechat.unhook(hook_get_ip) - hook_get_ip = '' - python_bin = weechat.info_get('python2_bin', '') or 'python' - hook_get_ip = weechat.hook_process( - python_bin + " -c \"\n" - "import socket, sys\n" - "try:\n" - " ip = socket.gethostbyname('%(host)s')\n" - " print ip\n" - "except Exception, e:\n" - " print >>sys.stderr, e\n\"" %{'host':host}, - timeout, 'get_ip_process_cb', '') - -def get_ip_process_cb(data, command, rc, stdout, stderr): - """Called when uri resolve finished.""" - global hook_get_ip, reply_wrapper - #debug("%s @ stderr: '%s', stdout: '%s'" %(rc, stderr.strip('\n'), stdout.strip('\n'))) - if stdout and reply_wrapper: - code, country = search_in_database(stdout[:-1]) - reply_wrapper(code, country) - reply_wrapper = None - if stderr and reply_wrapper: - reply_wrapper(*unknown) - reply_wrapper = None - if int(rc) >= 0: - hook_get_ip = '' - return WEECHAT_RC_OK - -def is_ip(s): - """Returns whether or not a given string is an IPV4 address.""" - try: - return bool(socket.inet_aton(s)) - except socket.error: - return False - -_valid_label = re.compile(r'^([\da-z]|[\da-z][-\da-z]*[\da-z])$', re.I) -def is_domain(s): - """ - Checks if 's' is a valid domain.""" - if not s or len(s) > 255: - return False - labels = s.split('.') - if len(labels) < 2: - return False - for label in labels: - if not label or len(label) > 63 \ - or not _valid_label.match(label): - return False - return True - -def hex_to_ip(s): - """ - '7f000001' => '127.0.0.1'""" - try: - ip = map(lambda n: s[n:n+2], range(0, len(s), 2)) - ip = map(lambda n: int(n, 16), ip) - return '.'.join(map(str, ip)) - except: - return '' - -def get_userhost_from_nick(buffer, nick): - """Return host of a given nick in buffer.""" - channel = weechat.buffer_get_string(buffer, 'localvar_channel') - server = weechat.buffer_get_string(buffer, 'localvar_server') - if channel and server: - infolist = weechat.infolist_get('irc_nick', '', '%s,%s' %(server, channel)) - if infolist: - try: - while weechat.infolist_next(infolist): - name = weechat.infolist_string(infolist, 'name') - if nick == name: - return weechat.infolist_string(infolist, 'host') - finally: - weechat.infolist_free(infolist) - return '' - -def get_ip_from_userhost(user, host): - ip = get_ip_from_host(host) - if ip: - return ip - ip = get_ip_from_user(user) - if ip: - return ip - return host - -def get_ip_from_host(host): - if is_domain(host): - return host - else: - if host.startswith('gateway/web/freenode/ip.'): - ip = host.split('.', 1)[1] - return ip - -def get_ip_from_user(user): - user = user[-8:] # only interested in the last 8 chars - if len(user) == 8: - ip = hex_to_ip(user) - if ip and is_ip(ip): - return ip - -def sum_ip(ip): - """Converts the ip number from dot-decimal notation to decimal.""" - L = map(int, ip.split('.')) - return L[0]*16777216 + L[1]*65536 + L[2]*256 + L[3] - -unknown = ('--', 'unknown') -def search_in_database(ip): - """ - search_in_database(ip_number) => (code, country) - returns ('--', 'unknown') if nothing found - """ - import csv - global ip_database - if not ip or not ip_database: - return unknown - try: - # do a binary search. - n = sum_ip(ip) - fd = open(ip_database) - reader = csv.reader(fd) - max = os.path.getsize(ip_database) - last_high = last_low = min = 0 - while True: - mid = (max + min)/2 - fd.seek(mid) - fd.readline() # move cursor to next line - _, _, low, high, code, country = reader.next() - if low == last_low and high == last_high: - break - if n < long(low): - max = mid - elif n > long(high): - min = mid - elif n > long(low) and n < long(high): - return (code, country) - else: - break - last_low, last_high = low, high - except StopIteration: - pass - return unknown - -def print_country(host, buffer, quiet=False, broken=False, nick=''): - """ - Prints country and local time for a given host, if quiet is True prints only if there's a match, - if broken is True reply will be split in two messages. - """ - #debug('host: ' + host) - def reply_country(code, country): - if quiet and code == '--': - return - if pytz_module and get_config_boolean('show_localtime') and code != '--': - dt = get_country_datetime(code) - if broken: - whois(nick or host, string_country(country, code), buffer) - whois(nick or host, string_time(dt), buffer) - else: - s = '%s - %s' %(string_country(country, code), string_time(dt)) - whois(nick or host, s, buffer) - else: - whois(nick or host, string_country(country, code), buffer) - - if is_ip(host): - # good, got an ip - code, country = search_in_database(host) - elif is_domain(host): - # try to resolve uri - global reply_wrapper - reply_wrapper = reply_country - get_ip_process(host) - return - else: - # probably a cloak or ipv6 - code, country = unknown - reply_country(code, country) - -### timezone -def get_country_datetime(code): - """Get datetime object with country's timezone.""" - try: - tzname = pytz.country_timezones(code)[0] - tz = pytz.timezone(tzname) - return datetime.datetime.now(tz) - except: - return None - -### commands -def cmd_country(data, buffer, args): - """Shows country and local time for a given ip, uri or nick.""" - if not args: - weechat.command('', '/HELP %s' %SCRIPT_COMMAND) - return WEECHAT_RC_OK - if ' ' in args: - # picks the first argument only - args = args[:args.find(' ')] - if args == 'update': - update_database() - else: - if not check_database(): - error("IP database not found. You must download a database with '/country update' before " - "using this script.", buffer) - return WEECHAT_RC_OK - #check if is a nick - userhost = get_userhost_from_nick(buffer, args) - if userhost: - host = get_ip_from_userhost(*userhost.split('@')) - else: - host = get_ip_from_userhost(args, args) - print_country(host, buffer) - return WEECHAT_RC_OK - -def find_buffer(server, nick, message_type='whois'): - # See if there is a target msgbuffer set for this server - msgbuffer = weechat.config_string(weechat.config_get('irc.msgbuffer.%s.%s' % (server, message_type))) - # No whois msgbuffer for this server; use the global setting - if msgbuffer == '': - msgbuffer = weechat.config_string(weechat.config_get('irc.msgbuffer.%s' % message_type)) - - # Use the fallback msgbuffer setting if private buffer doesn't exist - if msgbuffer == 'private': - buffer = weechat.buffer_search('irc', '%s.%s' %(server, nick)) - if buffer != '': - return buffer - else: - msgbuffer = weechat.config_string(weechat.config_get('irc.look.msgbuffer_fallback')) - - # Find the appropriate buffer - if msgbuffer == "current": - return weechat.current_buffer() - elif msgbuffer == "weechat": - return weechat.buffer_search_main() - else: - return weechat.buffer_search('irc', 'server.%s' % server) - -### signal callbacks -def whois_cb(data, signal, signal_data): - """function for /WHOIS""" - if not get_config_boolean('show_in_whois') or not check_database(): - return WEECHAT_RC_OK - nick, user, host = signal_data.split()[3:6] - server = signal[:signal.find(',')] - #debug('%s | %s | %s' %(data, signal, signal_data)) - host = get_ip_from_userhost(user, host) - print_country(host, find_buffer(server, nick), quiet=True, broken=True, nick=nick) - return WEECHAT_RC_OK - -### main -if import_ok and weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, '', ''): - - # colors - color_delimiter = weechat.color('chat_delimiters') - color_chat_nick = weechat.color('chat_nick') - color_reset = weechat.color('reset') - - # pretty [SCRIPT_NAME] - script_nick = '%s[%s%s%s]%s' % (color_delimiter, - color_chat_nick, - SCRIPT_NAME, - color_delimiter, - color_reset) - - weechat.hook_signal('*,irc_in2_311', 'whois_cb', '') # /whois - weechat.hook_signal('*,irc_in2_314', 'whois_cb', '') # /whowas - weechat.hook_command('country', cmd_country.__doc__, 'update | (nick|ip|uri)', - " update: Downloads/updates ip database with country codes.\n" - "nick, ip, uri: Gets country and local time for a given ip, domain or nick.", - 'update||%(nick)', 'cmd_country', '') - - # settings - for opt, val in settings.iteritems(): - if not weechat.config_is_set_plugin(opt): - weechat.config_set_plugin(opt, val) - - if not check_database(): - say("IP database not found. You must download a database with '/country update' before " - "using this script.") - - if not pytz_module and get_config_boolean('show_localtime'): - error( - "pytz module isn't installed, local time information is DISABLED. " - "Get it from http://pytz.sourceforge.net or from your distro packages " - "(python-tz in Ubuntu/Debian).") - weechat.config_set_plugin('show_localtime', 'off') - - # ------------------------------------------------------------------------- - # Debug - - if weechat.config_get_plugin('debug'): - try: - # custom debug module I use, allows me to inspect script's objects. - import pybuffer - debug = pybuffer.debugBuffer(globals(), '%s_debug' % SCRIPT_NAME) - except: - def debug(s, *args): - if not isinstance(s, basestring): - s = str(s) - if args: - s = s %args - prnt('', '%s\t%s' % (script_nick, s)) - else: - def debug(*args): - pass - -# vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100: diff --git a/python/cron.py b/python/cron.py index fa71dc54..f8d4cfe8 100644 --- a/python/cron.py +++ b/python/cron.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2010-2012 Sebastien Helleu +# Copyright (C) 2010-2021 Sébastien Helleu # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,21 +22,26 @@ # # History: # -# 2012-01-03, Sebastien Helleu : +# 2021-11-07, Sébastien Helleu : +# version 0.6: replace calls to function hook_completion_list_add by +# completion_list_add +# 2021-05-02, Sébastien Helleu : +# version 0.5: add compatibility with WeeChat >= 3.2 (XDG directories) +# 2012-01-03, Sébastien Helleu : # version 0.4: make script compatible with Python 3.x -# 2011-02-13, Sebastien Helleu : +# 2011-02-13, Sébastien Helleu : # version 0.3: use new help format for command arguments -# 2010-07-31, Sebastien Helleu : +# 2010-07-31, Sébastien Helleu : # version 0.2: add keyword "commands" to run many commands -# 2010-07-26, Sebastien Helleu : +# 2010-07-26, Sébastien Helleu : # version 0.1: initial release -# 2010-07-20, Sebastien Helleu : +# 2010-07-20, Sébastien Helleu : # script creation # SCRIPT_NAME = "cron" -SCRIPT_AUTHOR = "Sebastien Helleu " -SCRIPT_VERSION = "0.4" +SCRIPT_AUTHOR = "Sébastien Helleu " +SCRIPT_VERSION = "0.6" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Time-based scheduler, like cron and at" @@ -46,7 +51,7 @@ import weechat except: print("This script must be run under WeeChat.") - print("Get WeeChat now at: http://www.weechat.org/") + print("Get WeeChat now at: https://weechat.org/") import_ok = False try: @@ -242,7 +247,11 @@ def exec_command(self, userExec=False): def cron_filename(): """ Get crontab filename. """ - return weechat.config_get_plugin("filename").replace("%h", weechat.info_get("weechat_dir", "")) + options = { + 'directory': 'config', + } + return weechat.string_eval_path_home( + weechat.config_get_plugin("filename"), {}, {}, options) def cron_str_job_count(number): """ Get string with "%d jobs". """ @@ -376,14 +385,14 @@ def cron_at_time(strtime): def cron_completion_time_cb(data, completion_item, buffer, completion): """ Complete with time, for command '/cron'. """ - weechat.hook_completion_list_add(completion, "*", - 0, weechat.WEECHAT_LIST_POS_BEGINNING) + weechat.completion_list_add(completion, "*", + 0, weechat.WEECHAT_LIST_POS_BEGINNING) return weechat.WEECHAT_RC_OK def cron_completion_repeat_cb(data, completion_item, buffer, completion): """ Complete with repeat, for command '/cron'. """ - weechat.hook_completion_list_add(completion, "*", - 0, weechat.WEECHAT_LIST_POS_BEGINNING) + weechat.completion_list_add(completion, "*", + 0, weechat.WEECHAT_LIST_POS_BEGINNING) return weechat.WEECHAT_RC_OK def cron_completion_buffer_cb(data, completion_item, buffer, completion): @@ -392,22 +401,22 @@ def cron_completion_buffer_cb(data, completion_item, buffer, completion): while weechat.infolist_next(infolist): plugin_name = weechat.infolist_string(infolist, "plugin_name") name = weechat.infolist_string(infolist, "name") - weechat.hook_completion_list_add(completion, - "%s.%s" % (plugin_name, name), - 0, weechat.WEECHAT_LIST_POS_SORT) + weechat.completion_list_add(completion, + "%s.%s" % (plugin_name, name), + 0, weechat.WEECHAT_LIST_POS_SORT) weechat.infolist_free(infolist) - weechat.hook_completion_list_add(completion, "current", - 0, weechat.WEECHAT_LIST_POS_BEGINNING) - weechat.hook_completion_list_add(completion, "core.weechat", - 0, weechat.WEECHAT_LIST_POS_BEGINNING) + weechat.completion_list_add(completion, "current", + 0, weechat.WEECHAT_LIST_POS_BEGINNING) + weechat.completion_list_add(completion, "core.weechat", + 0, weechat.WEECHAT_LIST_POS_BEGINNING) return weechat.WEECHAT_RC_OK def cron_completion_keyword_cb(data, completion_item, buffer, completion): """ Complete with cron keyword, for command '/cron'. """ global cron_commands for command in sorted(cron_commands.keys()): - weechat.hook_completion_list_add(completion, command, - 0, weechat.WEECHAT_LIST_POS_END) + weechat.completion_list_add(completion, command, + 0, weechat.WEECHAT_LIST_POS_END) return weechat.WEECHAT_RC_OK def cron_completion_commands_cb(data, completion_item, buffer, completion): @@ -418,8 +427,8 @@ def cron_completion_commands_cb(data, completion_item, buffer, completion): if command.startswith("/"): command = command[1:] if command: - weechat.hook_completion_list_add(completion, "/%s" % command, - 0, weechat.WEECHAT_LIST_POS_SORT) + weechat.completion_list_add(completion, "/%s" % command, + 0, weechat.WEECHAT_LIST_POS_SORT) weechat.infolist_free(infolist) return weechat.WEECHAT_RC_OK @@ -428,16 +437,16 @@ def cron_completion_number_cb(data, completion_item, buffer, completion): global crontab if len(crontab) > 0: for i in reversed(range(0, len(crontab))): - weechat.hook_completion_list_add(completion, "%d" % (i + 1), - 0, weechat.WEECHAT_LIST_POS_BEGINNING) + weechat.completion_list_add(completion, "%d" % (i + 1), + 0, weechat.WEECHAT_LIST_POS_BEGINNING) return weechat.WEECHAT_RC_OK def cron_completion_at_time_cb(data, completion_item, buffer, completion): """ Complete with time, for command '/at'. """ - weechat.hook_completion_list_add(completion, "+5m", - 0, weechat.WEECHAT_LIST_POS_END) - weechat.hook_completion_list_add(completion, "20:00", - 0, weechat.WEECHAT_LIST_POS_END) + weechat.completion_list_add(completion, "+5m", + 0, weechat.WEECHAT_LIST_POS_END) + weechat.completion_list_add(completion, "20:00", + 0, weechat.WEECHAT_LIST_POS_END) return weechat.WEECHAT_RC_OK def cron_cmd_cb(data, buffer, args): diff --git a/python/crypt.py b/python/crypt.py index 11e6819b..18275e9c 100644 --- a/python/crypt.py +++ b/python/crypt.py @@ -3,7 +3,7 @@ # =============================================================== SCRIPT_NAME = "crypt" SCRIPT_AUTHOR = "Nicolai Lissner " -SCRIPT_VERSION = "1.4.4" +SCRIPT_VERSION = "1.4.5" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "encrypt/decrypt PRIVMSGs using a pre-shared key and openssl" @@ -125,7 +125,8 @@ def encryption_statusbar(data, item, window): # register plugin if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "UTF-8"): - weechat_dir = weechat.info_get("weechat_dir","") + weechat_dir = weechat.info_get("weechat_data_dir", "") \ + or weechat.info_get("weechat_dir", "") version = weechat.info_get("version_number", "") or 0 if int(version) < 0x00030000: weechat.prnt("", "%s%s: WeeChat 0.3.0 is required for this script." diff --git a/python/customize_bar.py b/python/customize_bar.py index 5b410e90..87423bbe 100644 --- a/python/customize_bar.py +++ b/python/customize_bar.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2012 by nils_2 +# Copyright (c) 2012-2018 by nils_2 # # customize your title/status/input bar for each buffer # @@ -20,6 +20,9 @@ # This script deletes weechatlog-files by age or size # YOU ARE USING THIS SCRIPT AT YOUR OWN RISK! # +# 2017-11-11: nils_2, (freenode.#weechat) +# 0.2 : make script python3 compatible +# # 2012-01-20: nils_2, (freenode.#weechat) # 0.1 : initial release # @@ -30,13 +33,13 @@ import weechat,re except Exception: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org") quit() SCRIPT_NAME = "customize_bar" SCRIPT_AUTHOR = "nils_2 " -SCRIPT_VERSION = "0.1" +SCRIPT_VERSION = "0.2" SCRIPT_LICENSE = "GPL" SCRIPT_DESC = "customize your title/status/input bar for each buffer" @@ -50,7 +53,7 @@ def buffer_switch(data, signal, signal_data): if full_name == '': # upps, something totally wrong! return weechat.WEECHAT_RC_OK - for option in OPTIONS.keys(): + for option in list(OPTIONS.keys()): option = option.split('.') customize_plugin = weechat.config_get_plugin('%s.%s' % (option[1], full_name)) # for example: title.irc.freenode.#weechat if customize_plugin: # option exists @@ -89,14 +92,14 @@ def customize_cmd_cb(data, buffer, args): return weechat.WEECHAT_RC_OK def customize_bar_completion_cb(data, completion_item, buffer, completion): - for option in OPTIONS.keys(): + for option in list(OPTIONS.keys()): option = option.split('.') weechat.hook_completion_list_add(completion, option[1], 0, weechat.WEECHAT_LIST_POS_SORT) return weechat.WEECHAT_RC_OK def shutdown_cb(): # write back default options to original options, then quit... - for option in OPTIONS.keys(): + for option in list(OPTIONS.keys()): option = option.split('.') default_plugin = weechat.config_get_plugin('default.%s' % option[1]) config_pnt = weechat.config_get('weechat.bar.%s.items' % option[1]) @@ -105,7 +108,7 @@ def shutdown_cb(): # ================================[ config ]=============================== def init_options(): # check out if a default item bar exists - for option,value in OPTIONS.items(): + for option,value in list(OPTIONS.items()): if not weechat.config_get_plugin(option): default_bar = weechat.config_string(weechat.config_get(value))# get original option weechat.config_set_plugin(option, default_bar) @@ -129,10 +132,10 @@ def init_options(): ' plugins.var.python.customize_bar.default.status: stores the default items from weechat status bar.\n' ' plugins.var.python.customize_bar.default.input : stores the default items from weechat input bar.\n' ' plugins.var.python.customize_bar.(title|status|input). : stores the customize bar items for this buffer\n\n' - 'CAVE: Do not delete options \"plugins.var.python.customize_bar.default.*\" as long as script is running...\n', + 'HINT: Do not delete options \"plugins.var.python.customize_bar.default.*\" as long as script is running...\n', 'add %(plugin_customize_bar) %-|| del %(plugin_customize_bar) %-', 'customize_cmd_cb', '') init_options() weechat.hook_signal('buffer_switch','buffer_switch','') weechat.hook_completion('plugin_customize_bar', 'customize_bar_completion', 'customize_bar_completion_cb', '') - weechat.command('','/window refresh') \ No newline at end of file + weechat.command('','/window refresh') diff --git a/python/deadbeef_np.py b/python/deadbeef_np.py index 19c633fa..ac833425 100644 --- a/python/deadbeef_np.py +++ b/python/deadbeef_np.py @@ -18,13 +18,18 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import commands +import subprocess import weechat + def weechat_np(data, buffer, args): - read_track = commands.getoutput('deadbeef --nowplaying "%a - (%b) - %t [%@:BPS@bit / %@:BITRATE@kbps / %@:SAMPLERATE@Hz]"').split('\n') - weechat.command(buffer, '/me is currently listening to: ' + read_track[1]) + read_track = subprocess.getoutput('deadbeef --nowplaying "%a - (%b) - %t [%@:BPS@bit / %@:BITRATE@kbps / %@:SAMPLERATE@Hz]"').split('\n') + if len(read_track) > 1: + weechat.command(buffer, '/me is currently listening to: ' + read_track[1]) + else: + weechat.prnt(buffer, 'deadbeef_np: error: %s' % read_track[0]) return weechat.WEECHAT_RC_OK -weechat.register("deadbeef_np", "mwgg", "0.9", "MIT", "Show name of the song currently played by DeaDBeeF", "", "") + +weechat.register("deadbeef_np", "mwgg", "1.0", "MIT", "Show name of the song currently played by DeaDBeeF", "", "") weechat.hook_command("np", "Get/send now playing info.", "", "", "", "weechat_np", "") diff --git a/python/detach_away.py b/python/detach_away.py new file mode 100644 index 00000000..8d1cd2a3 --- /dev/null +++ b/python/detach_away.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 p3lim +# +# https://github.com/p3lim/weechat-detach-away +# +# Changelog: +# Ver: 0.1.1 Python3 support by Antonin Skala skala.antonin@gmail.com 3.2019 +# Ver: 0.1.2 Support Python 2 and 3 by Antonin Skala skala.antonin@gmail.com 3.2019 + +try: + import weechat +except ImportError: + from sys import exit + print('This script has to run under WeeChat (https://weechat.org/).') + exit(1) + +import sys + +if sys.version_info[0] > 2: + from urllib.parse import urlencode +else: + from urllib import urlencode + +SCRIPT_NAME = 'detach_away' +SCRIPT_AUTHOR = 'p3lim' +SCRIPT_VERSION = '0.1.2' +SCRIPT_LICENSE = 'MIT' +SCRIPT_DESC = 'Automatically sets away message based on number of relays connected' + +SETTINGS = { + 'message': ( + 'I am away', + 'away message'), + 'debugging': ( + 'off', + 'debug flag'), +} + +num_relays = 0 + +def DEBUG(): + return weechat.config_get_plugin('debug') == 'on' + +def set_away(is_away, message=''): + if is_away: + message = weechat.config_get_plugin('message') + + weechat.command('', '/away -all ' + message) + +def relay_connected(data, signal, signal_data): + global num_relays + + if DEBUG(): + weechat.prnt('', 'DETACH_AWAY: last #relays: ' + str(num_relays)) + + if int(num_relays) == 0: + set_away(False) + + num_relays = weechat.info_get('relay_client_count', 'connected') + return weechat.WEECHAT_RC_OK + +def relay_disconnected(data, signal, signal_data): + global num_relays + + if DEBUG(): + weechat.prnt('', 'DETACH_AWAY: last #relays: ' + str(num_relays)) + + if int(num_relays) > 0: + set_away(True) + + num_relays = weechat.info_get('relay_client_count', 'connected') + return weechat.WEECHAT_RC_OK + +# register plugin +weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', '') + +# register for relay status +weechat.hook_signal('relay_client_connected', 'relay_connected', '') +weechat.hook_signal('relay_client_disconnected', 'relay_disconnected', '') + +# register configuration defaults +for option, value in SETTINGS.items(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value[0]) + weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) diff --git a/python/digraph.py b/python/digraph.py new file mode 100644 index 00000000..3f3ca491 --- /dev/null +++ b/python/digraph.py @@ -0,0 +1,172 @@ +# coding=utf-8 +# WeeChat script for nvim style digraphs. Enables entering math symbols easily. + +# Copyright (C) 2023 narodnik +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Main repository, version history: https://github.com/narodnik/weechat-digraph-latex + +import weechat + +# Substrings surrounded by this will have replacement active +MODIFIER = "$" + +weechat.register("digraph", "narodnik", "1.0", "GPL3", + "Digraphs like nvim for inputting math symbols", + "", "") + +weechat.hook_modifier("input_text_display", "modifier_cb", "") +weechat.hook_modifier("input_text_for_buffer", "modifier_cb", "") + +sup_vals = "⁰¹²³⁴⁵⁶⁷⁸⁹ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻ⁺⁻⁼⁽⁾" +sup_keys = "0123456789abcdefghijklmnoprstuvwxyz+-=()" + +sub_vals = "₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎" +sub_keys = "0123456789+-=()" + +symbols = [ + ("ZZ", "ℤ"), + ("QQ", "ℚ"), + ("FF", "𝔽"), + ("a:", "𝔞"), + ("b:", "𝔟"), + ("c:", "𝔠"), + ("p:", "𝔭"), + ("in", "∈"), + ("ni", "∉"), + ("(_", "⊆"), + ("(<", "⊊"), + ("(!", "⊈"), + (":.", "·"), + (".,", "…"), + (".3", "⋯"), + ("**", "×"), + ("i8", "∞"), + ("", "⟩"), + ("ff", "ϕ"), + ("=>", "⇒"), + ("==", "⇔"), + ("->", "→"), + ("TE", "∃"), + ("!=", "≠"), + ("=3", "≡"), + ("=<", "≤"), + ("<=", "≤"), + (">=", "≥"), + ("=?", "≌"), + ("RT", "√"), + ("(U", "∩"), +] + +greek_key = "abcdefghiklmnopqrstuwxyz" +greek_cap = "ΑΒΞΔΕΦΓΘΙΚΛΜΝΟΠΨΡΣΤΥΩΧΗΖ" +greek_min = "αβξδεφγθικλμνοπψρστυωχηζ" + +START_EXPR = 1 +END_EXPR = 2 + +def build_replacement_table(): + table = [] + table.extend(symbols) + # Superscript + for key, val in zip(sup_keys, sup_vals): + table.append((f"^{key}", val)) + # Subscript + for key, val in zip(sub_keys, sub_vals): + table.append((f"_{key}", val)) + # Greek letters + assert greek_key == greek_key.lower() + for key, cap, min in zip(greek_key, greek_cap, greek_min): + table.append((f"{key.upper()}*", cap)) + table.append((f"{key}*", min)) + return table + +replacement_table = build_replacement_table() + +def find_all(a_str, sub): + start = 0 + while True: + start = a_str.find(sub, start) + if start == -1: return + yield start + start += len(sub) # use start += 1 to find overlapping matches + +def replace_symbols(a_str): + for key, value in replacement_table: + a_str = a_str.replace(key, value) + return a_str + +def lexer(string): + tokens = [] + last_pos = 0 + for pos in find_all(string, MODIFIER): + tokens.append(string[last_pos:pos - len(MODIFIER) + 1]) + tokens.append(MODIFIER) + last_pos = pos + len(MODIFIER) + if last_pos < len(string): + tokens.append(string[last_pos:]) + + active = False + for i, token in enumerate(tokens): + if token == MODIFIER: + if not active: + tokens[i] = START_EXPR + else: + tokens[i] = END_EXPR + active = not active + + return tokens + +def compile(tokens): + state = END_EXPR + result = "" + current_word = None + is_last = lambda i: i == len(tokens) - 1 + for i, token in enumerate(tokens): + if token == START_EXPR: + assert state == END_EXPR + result += current_word + current_word = None + # When at the very last token, keep the unclosed MODIFIER + if is_last(i): + result += MODIFIER + # Now change state to open expr + state = START_EXPR + elif token == END_EXPR: + assert state == START_EXPR + # Close the prev expr + result += replace_symbols(current_word) + current_word = None + # Re-open normal mode + state = END_EXPR + else: + assert state in (START_EXPR, END_EXPR) + assert current_word is None + current_word = token + if current_word is not None: + if state == START_EXPR: + result += MODIFIER + result += replace_symbols(current_word) + else: + assert state == END_EXPR + result += current_word + return result + +def modifier_cb(data, modifier, modifier_data, string): + tokens = lexer(string) + result = compile(tokens) + return result + diff --git a/python/emoji2alias.py b/python/emoji2alias.py index fd946b4e..7a21eaa6 100644 --- a/python/emoji2alias.py +++ b/python/emoji2alias.py @@ -25,1447 +25,1032 @@ # # History: # +# 2019-07-06, Sébastien Helleu : +# v0.2: Make script compatible with Python 3, fix PEP8 errors. +# # 2016-03-15, Wil Clouser : # v0.1: Initial release, based on Mike Reinhardt's BSD # licensed emoji_aliases.py -SCRIPT_NAME = "emoji2alias" -SCRIPT_AUTHOR = "Wil Clouser " -SCRIPT_VERSION = "0.1" +from __future__ import print_function + +import re +import sys + +SCRIPT_NAME = "emoji2alias" +SCRIPT_AUTHOR = "Wil Clouser " +SCRIPT_VERSION = "0.2" SCRIPT_LICENSE = "MIT" -SCRIPT_DESC = "Replaces emoji characters with their aliases" +SCRIPT_DESC = "Replaces emoji characters with their aliases" import_ok = True try: - import weechat as w -except: - print "Script must be run under weechat. http://www.weechat.org" - import_ok = False - -import re + import weechat as w +except ImportError: + print("Script must be run under weechat: https://weechat.org") + import_ok = False EMOJI_ALIASES = { - u'\U0001F44D': u':+1:', - u'\U0001F44E': u':-1:', - u'\U0001F4AF': u':100:', - u'\U0001F522': u':1234:', - u'\U0001F3B1': u':8ball:', - u'\U0001F170': u':a:', - u'\U0001F18E': u':ab:', - u'\U0001F524': u':abc:', - u'\U0001F521': u':abcd:', - u'\U0001F251': u':accept:', - u'\U0001F39F': u':admission_tickets:', - u'\U0001F6A1': u':aerial_tramway:', + u'\U0001f44d': u':+1:', + u'\U0001f44e': u':-1:', + u'\U0001f4af': u':100:', + u'\U0001f522': u':1234:', + u'\U0001f3b1': u':8ball:', + u'\U0001f170': u':a:', + u'\U0001f18e': u':ab:', + u'\U0001f524': u':abc:', + u'\U0001f521': u':abcd:', + u'\U0001f251': u':accept:', + u'\U0001f39f': u':admission_tickets:', + u'\U0001f6a1': u':aerial_tramway:', u'\U00002708': u':airplane:', - u'\U0001F6EC': u':airplane_arriving:', - u'\U0001F6EB': u':airplane_departure:', - u'\U000023F0': u':alarm_clock:', + u'\U0001f6ec': u':airplane_arriving:', + u'\U0001f6eb': u':airplane_departure:', + u'\U000023f0': u':alarm_clock:', u'\U00002697': u':alembic:', - u'\U0001F47D': u':alien:', - u'\U0001F47E': u':alien_monster:', - u'\U0001F691': u':ambulance:', - u'\U0001F3C8': u':american_football:', - u'\U0001F3FA': u':amphora:', + u'\U0001f47d': u':alien:', + u'\U0001f47e': u':alien_monster:', + u'\U0001f691': u':ambulance:', + u'\U0001f3c8': u':american_football:', + u'\U0001f3fa': u':amphora:', u'\U00002693': u':anchor:', - u'\U0001F47C': u':angel:', - u'\U0001F4A2': u':anger:', - u'\U0001F4A2': u':anger_symbol:', - u'\U0001F620': u':angry:', - u'\U0001F620': u':angry_face:', - u'\U0001F627': u':anguished:', - u'\U0001F627': u':anguished_face:', - u'\U0001F41C': u':ant:', - u'\U0001F4F6': u':antenna_with_bars:', - u'\U0001F34E': u':apple:', + u'\U0001f47c': u':angel:', + u'\U0001f4a2': u':anger:', + u'\U0001f620': u':angry:', + u'\U0001f627': u':anguished:', + u'\U0001f41c': u':ant:', + u'\U0001f4f6': u':antenna_with_bars:', + u'\U0001f34e': u':apple:', u'\U00002652': u':aquarius:', u'\U00002648': u':aries:', - u'\U000025C0': u':arrow_backward:', - u'\U000023EC': u':arrow_double_down:', - u'\U000023EB': u':arrow_double_up:', - u'\U00002B07': u':arrow_down:', - u'\U0001F53D': u':arrow_down_small:', - u'\U000025B6': u':arrow_forward:', + u'\U000025c0': u':arrow_backward:', + u'\U000023ec': u':arrow_double_down:', + u'\U000023eb': u':arrow_double_up:', + u'\U00002b07': u':arrow_down:', + u'\U0001f53d': u':arrow_down_small:', + u'\U000025b6': u':arrow_forward:', u'\U00002935': u':arrow_heading_down:', u'\U00002934': u':arrow_heading_up:', - u'\U00002B05': u':arrow_left:', + u'\U00002b05': u':arrow_left:', u'\U00002199': u':arrow_lower_left:', u'\U00002198': u':arrow_lower_right:', - u'\U000027A1': u':arrow_right:', - u'\U000021AA': u':arrow_right_hook:', - u'\U00002B06': u':arrow_up:', + u'\U000027a1': u':arrow_right:', + u'\U000021aa': u':arrow_right_hook:', + u'\U00002b06': u':arrow_up:', u'\U00002195': u':arrow_up_down:', - u'\U0001F53C': u':arrow_up_small:', + u'\U0001f53c': u':arrow_up_small:', u'\U00002196': u':arrow_upper_left:', u'\U00002197': u':arrow_upper_right:', - u'\U0001F503': u':arrows_clockwise:', - u'\U0001F504': u':arrows_counterclockwise:', - u'\U0001F3A8': u':art:', - u'\U0001F69B': u':articulated_lorry:', - u'\U0001F3A8': u':artist_palette:', - u'\U0001F632': u':astonished:', - u'\U0001F632': u':astonished_face:', - u'\U0001F45F': u':athletic_shoe:', - u'\U0001F3E7': u':atm:', - u'\U0000269B': u':atom_symbol:', - u'\U0001F346': u':aubergine:', - u'\U0001F3E7': u':automated_teller_machine:', - u'\U0001F697': u':automobile:', - u'\U0001F171': u':b:', - u'\U0001F476': u':baby:', - u'\U0001F47C': u':baby_angel:', - u'\U0001F37C': u':baby_bottle:', - u'\U0001F424': u':baby_chick:', - u'\U0001F6BC': u':baby_symbol:', - u'\U0001F519': u':back:', - u'\U0001F519': u':back_with_leftwards_arrow_above:', - u'\U0001F42B': u':bactrian_camel:', - u'\U0001F3F8': u':badminton_racquet_and_shuttlecock:', - u'\U0001F6C4': u':baggage_claim:', - u'\U0001F388': u':balloon:', - u'\U0001F5F3': u':ballot_box_with_ballot:', + u'\U0001f503': u':arrows_clockwise:', + u'\U0001f504': u':arrows_counterclockwise:', + u'\U0001f3a8': u':art:', + u'\U0001f69b': u':articulated_lorry:', + u'\U0001f632': u':astonished:', + u'\U0001f45f': u':athletic_shoe:', + u'\U0001f3e7': u':atm:', + u'\U0000269b': u':atom_symbol:', + u'\U0001f346': u':aubergine:', + u'\U0001f697': u':automobile:', + u'\U0001f171': u':b:', + u'\U0001f476': u':baby:', + u'\U0001f37c': u':baby_bottle:', + u'\U0001f424': u':baby_chick:', + u'\U0001f6bc': u':baby_symbol:', + u'\U0001f519': u':back:', + u'\U0001f42b': u':bactrian_camel:', + u'\U0001f3f8': u':badminton_racquet_and_shuttlecock:', + u'\U0001f6c4': u':baggage_claim:', + u'\U0001f388': u':balloon:', + u'\U0001f5f3': u':ballot_box_with_ballot:', u'\U00002611': u':ballot_box_with_check:', - u'\U0001F38D': u':bamboo:', - u'\U0001F34C': u':banana:', - u'\U0000203C': u':bangbang:', - u'\U0001F3E6': u':bank:', - u'\U0001F4B5': u':banknote_with_dollar_sign:', - u'\U0001F4B6': u':banknote_with_euro_sign:', - u'\U0001F4B7': u':banknote_with_pound_sign:', - u'\U0001F4B4': u':banknote_with_yen_sign:', - u'\U0001F4CA': u':bar_chart:', - u'\U0001F488': u':barber:', - u'\U0001F488': u':barber_pole:', - u'\U0001F325': u':barely_sunny:', - u'\U000026BE': u':baseball:', - u'\U0001F3C0': u':basketball:', - u'\U0001F3C0': u':basketball_and_hoop:', - u'\U0001F6C0': u':bath:', - u'\U0001F6C1': u':bathtub:', - u'\U0001F50B': u':battery:', - u'\U0001F3D6': u':beach_with_umbrella:', - u'\U0001F43B': u':bear:', - u'\U0001F43B': u':bear_face:', - u'\U0001F493': u':beating_heart:', - u'\U0001F6CF': u':bed:', - u'\U0001F41D': u':bee:', - u'\U0001F37A': u':beer:', - u'\U0001F37A': u':beer_mug:', - u'\U0001F37B': u':beers:', - u'\U0001F41E': u':beetle:', - u'\U0001F530': u':beginner:', - u'\U0001F514': u':bell:', - u'\U0001F515': u':bell_with_cancellation_stroke:', - u'\U0001F6CE': u':bellhop_bell:', - u'\U0001F371': u':bento:', - u'\U0001F371': u':bento_box:', - u'\U0001F6B2': u':bicycle:', - u'\U0001F6B4': u':bicyclist:', - u'\U0001F6B2': u':bike:', - u'\U0001F459': u':bikini:', - u'\U0001F3B1': u':billiards:', + u'\U0001f38d': u':bamboo:', + u'\U0001f34c': u':banana:', + u'\U0000203c': u':bangbang:', + u'\U0001f3e6': u':bank:', + u'\U0001f4b5': u':banknote_with_dollar_sign:', + u'\U0001f4b6': u':banknote_with_euro_sign:', + u'\U0001f4b7': u':banknote_with_pound_sign:', + u'\U0001f4b4': u':banknote_with_yen_sign:', + u'\U0001f4ca': u':bar_chart:', + u'\U0001f488': u':barber:', + u'\U0001f325': u':barely_sunny:', + u'\U000026be': u':baseball:', + u'\U0001f3c0': u':basketball:', + u'\U0001f6c0': u':bath:', + u'\U0001f6c1': u':bathtub:', + u'\U0001f50b': u':battery:', + u'\U0001f3d6': u':beach_with_umbrella:', + u'\U0001f43b': u':bear:', + u'\U0001f493': u':beating_heart:', + u'\U0001f6cf': u':bed:', + u'\U0001f41d': u':bee:', + u'\U0001f37a': u':beer:', + u'\U0001f37b': u':beers:', + u'\U0001f41e': u':beetle:', + u'\U0001f530': u':beginner:', + u'\U0001f514': u':bell:', + u'\U0001f515': u':bell_with_cancellation_stroke:', + u'\U0001f6ce': u':bellhop_bell:', + u'\U0001f371': u':bento:', + u'\U0001f6b2': u':bicycle:', + u'\U0001f6b4': u':bicyclist:', + u'\U0001f459': u':bikini:', u'\U00002623': u':biohazard_sign:', - u'\U0001F426': u':bird:', - u'\U0001F382': u':birthday:', - u'\U0001F382': u':birthday_cake:', - u'\U000026AB': u':black_circle:', - u'\U000023FA': u':black_circle_for_record:', + u'\U0001f426': u':bird:', + u'\U0001f382': u':birthday:', + u'\U000026ab': u':black_circle:', + u'\U000023fa': u':black_circle_for_record:', u'\U00002663': u':black_club_suit:', u'\U00002666': u':black_diamond_suit:', - u'\U000023EC': u':black_down-pointing_double_triangle:', u'\U00002665': u':black_heart_suit:', - u'\U0001F0CF': u':black_joker:', - u'\U00002B1B': u':black_large_square:', - u'\U000023EA': u':black_left-pointing_double_triangle:', - u'\U000025C0': u':black_left-pointing_triangle:', - u'\U000025FE': u':black_medium_small_square:', - u'\U000025FC': u':black_medium_square:', + u'\U0001f0cf': u':black_joker:', + u'\U00002b1b': u':black_large_square:', + u'\U000023ea': u':black_left-pointing_double_triangle:', + u'\U000025fe': u':black_medium_small_square:', + u'\U000025fc': u':black_medium_square:', u'\U00002712': u':black_nib:', u'\U00002753': u':black_question_mark_ornament:', - u'\U000023E9': u':black_right-pointing_double_triangle:', - u'\U000025B6': u':black_right-pointing_triangle:', - u'\U000027A1': u':black_rightwards_arrow:', + u'\U000023e9': u':black_right-pointing_double_triangle:', u'\U00002702': u':black_scissors:', - u'\U000025AA': u':black_small_square:', + u'\U000025aa': u':black_small_square:', u'\U00002660': u':black_spade_suit:', - u'\U0001F532': u':black_square_button:', - u'\U000023F9': u':black_square_for_stop:', + u'\U0001f532': u':black_square_button:', + u'\U000023f9': u':black_square_for_stop:', u'\U00002600': u':black_sun_with_rays:', - u'\U0000260E': u':black_telephone:', - u'\U0000267B': u':black_universal_recycling_symbol:', - u'\U000023EB': u':black_up-pointing_double_triangle:', - u'\U0001F33C': u':blossom:', - u'\U0001F421': u':blowfish:', - u'\U0001F4D8': u':blue_book:', - u'\U0001F699': u':blue_car:', - u'\U0001F499': u':blue_heart:', - u'\U0001F60A': u':blush:', - u'\U0001F417': u':boar:', - u'\U000026F5': u':boat:', - u'\U0001F4A3': u':bomb:', - u'\U0001F4D6': u':book:', - u'\U0001F516': u':bookmark:', - u'\U0001F4D1': u':bookmark_tabs:', - u'\U0001F4DA': u':books:', - u'\U0001F4A5': u':boom:', - u'\U0001F462': u':boot:', - u'\U0001F37E': u':bottle_with_popping_cork:', - u'\U0001F490': u':bouquet:', - u'\U0001F647': u':bow:', - u'\U0001F3F9': u':bow_and_arrow:', - u'\U0001F3B3': u':bowling:', - u'\U0001F466': u':boy:', - u'\U0001F35E': u':bread:', - u'\U0001F470': u':bride_with_veil:', - u'\U0001F309': u':bridge_at_night:', - u'\U0001F4BC': u':briefcase:', - u'\U0001F494': u':broken_heart:', - u'\U0001F41B': u':bug:', - u'\U0001F3D7': u':building_construction:', - u'\U0001F4A1': u':bulb:', - u'\U0001F685': u':bullettrain_front:', - u'\U0001F684': u':bullettrain_side:', - u'\U0001F32F': u':burrito:', - u'\U0001F68C': u':bus:', - u'\U0001F68F': u':bus_stop:', - u'\U0001F68F': u':busstop:', - u'\U0001F464': u':bust_in_silhouette:', - u'\U0001F465': u':busts_in_silhouette:', - u'\U0001F335': u':cactus:', - u'\U0001F370': u':cake:', - u'\U0001F4C5': u':calendar:', - u'\U0001F4C6': u':calendar:', - u'\U0001F4F2': u':calling:', - u'\U0001F42B': u':camel:', - u'\U0001F4F7': u':camera:', - u'\U0001F4F8': u':camera_with_flash:', - u'\U0001F3D5': u':camping:', - u'\U0000264B': u':cancer:', - u'\U0001F56F': u':candle:', - u'\U0001F36C': u':candy:', - u'\U0001F520': u':capital_abcd:', + u'\U0000260e': u':black_telephone:', + u'\U0000267b': u':black_universal_recycling_symbol:', + u'\U0001f33c': u':blossom:', + u'\U0001f421': u':blowfish:', + u'\U0001f4d8': u':blue_book:', + u'\U0001f699': u':blue_car:', + u'\U0001f499': u':blue_heart:', + u'\U0001f60a': u':blush:', + u'\U0001f417': u':boar:', + u'\U000026f5': u':boat:', + u'\U0001f4a3': u':bomb:', + u'\U0001f4d6': u':book:', + u'\U0001f516': u':bookmark:', + u'\U0001f4d1': u':bookmark_tabs:', + u'\U0001f4da': u':books:', + u'\U0001f4a5': u':boom:', + u'\U0001f462': u':boot:', + u'\U0001f37e': u':bottle_with_popping_cork:', + u'\U0001f490': u':bouquet:', + u'\U0001f647': u':bow:', + u'\U0001f3f9': u':bow_and_arrow:', + u'\U0001f3b3': u':bowling:', + u'\U0001f466': u':boy:', + u'\U0001f35e': u':bread:', + u'\U0001f470': u':bride_with_veil:', + u'\U0001f309': u':bridge_at_night:', + u'\U0001f4bc': u':briefcase:', + u'\U0001f494': u':broken_heart:', + u'\U0001f41b': u':bug:', + u'\U0001f3d7': u':building_construction:', + u'\U0001f4a1': u':bulb:', + u'\U0001f685': u':bullettrain_front:', + u'\U0001f684': u':bullettrain_side:', + u'\U0001f32f': u':burrito:', + u'\U0001f68c': u':bus:', + u'\U0001f68f': u':bus_stop:', + u'\U0001f464': u':bust_in_silhouette:', + u'\U0001f465': u':busts_in_silhouette:', + u'\U0001f335': u':cactus:', + u'\U0001f370': u':cake:', + u'\U0001f4c5': u':calendar:', + u'\U0001f4c6': u':calendar:', + u'\U0001f4f2': u':calling:', + u'\U0001f4f7': u':camera:', + u'\U0001f4f8': u':camera_with_flash:', + u'\U0001f3d5': u':camping:', + u'\U0000264b': u':cancer:', + u'\U0001f56f': u':candle:', + u'\U0001f36c': u':candy:', + u'\U0001f520': u':capital_abcd:', u'\U00002651': u':capricorn:', - u'\U0001F697': u':car:', - u'\U0001F5C3': u':card_file_box:', - u'\U0001F4C7': u':card_index:', - u'\U0001F5C2': u':card_index_dividers:', - u'\U0001F3A0': u':carousel_horse:', - u'\U0001F38F': u':carp_streamer:', - u'\U0001F408': u':cat2:', - u'\U0001F408': u':cat:', - u'\U0001F431': u':cat:', - u'\U0001F431': u':cat_face:', - u'\U0001F639': u':cat_face_with_tears_of_joy:', - u'\U0001F63C': u':cat_face_with_wry_smile:', - u'\U0001F4BF': u':cd:', - u'\U000026D3': u':chains:', - u'\U0001F37E': u':champagne:', - u'\U0001F4B9': u':chart:', - u'\U0001F4C9': u':chart_with_downwards_trend:', - u'\U0001F4C8': u':chart_with_upwards_trend:', - u'\U0001F4B9': u':chart_with_upwards_trend_and_yen_sign:', - u'\U0001F3C1': u':checkered_flag:', - u'\U0001F4E3': u':cheering_megaphone:', - u'\U0001F9C0': u':cheese_wedge:', - u'\U0001F3C1': u':chequered_flag:', - u'\U0001F352': u':cherries:', - u'\U0001F338': u':cherry_blossom:', - u'\U0001F330': u':chestnut:', - u'\U0001F414': u':chicken:', - u'\U0001F6B8': u':children_crossing:', - u'\U0001F43F': u':chipmunk:', - u'\U0001F36B': u':chocolate_bar:', - u'\U0001F384': u':christmas_tree:', - u'\U000026EA': u':church:', - u'\U0001F3A6': u':cinema:', - u'\U0001F251': u':circled_ideograph_accept:', - u'\U0001F250': u':circled_ideograph_advantage:', + u'\U0001f5c3': u':card_file_box:', + u'\U0001f4c7': u':card_index:', + u'\U0001f5c2': u':card_index_dividers:', + u'\U0001f3a0': u':carousel_horse:', + u'\U0001f38f': u':carp_streamer:', + u'\U0001f408': u':cat2:', + u'\U0001f431': u':cat:', + u'\U0001f639': u':cat_face_with_tears_of_joy:', + u'\U0001f63c': u':cat_face_with_wry_smile:', + u'\U0001f4bf': u':cd:', + u'\U000026d3': u':chains:', + u'\U0001f4b9': u':chart:', + u'\U0001f4c9': u':chart_with_downwards_trend:', + u'\U0001f4c8': u':chart_with_upwards_trend:', + u'\U0001f3c1': u':checkered_flag:', + u'\U0001f4e3': u':cheering_megaphone:', + u'\U0001f9c0': u':cheese_wedge:', + u'\U0001f352': u':cherries:', + u'\U0001f338': u':cherry_blossom:', + u'\U0001f330': u':chestnut:', + u'\U0001f414': u':chicken:', + u'\U0001f6b8': u':children_crossing:', + u'\U0001f43f': u':chipmunk:', + u'\U0001f36b': u':chocolate_bar:', + u'\U0001f384': u':christmas_tree:', + u'\U000026ea': u':church:', + u'\U0001f3a6': u':cinema:', + u'\U0001f250': u':circled_ideograph_advantage:', u'\U00003297': u':circled_ideograph_congratulation:', u'\U00003299': u':circled_ideograph_secret:', - u'\U000024C2': u':circled_latin_capital_letter_m:', - u'\U0001F3AA': u':circus_tent:', - u'\U0001F307': u':city_sunrise:', - u'\U0001F306': u':city_sunset:', - u'\U0001F3D9': u':cityscape:', - u'\U0001F306': u':cityscape_at_dusk:', - u'\U0001F191': u':cl:', - u'\U0001F44F': u':clap:', - u'\U0001F3AC': u':clapper:', - u'\U0001F3AC': u':clapper_board:', - u'\U0001F44F': u':clapping_hands_sign:', - u'\U0001F3DB': u':classical_building:', - u'\U0001F37B': u':clinking_beer_mugs:', - u'\U0001F4CB': u':clipboard:', - u'\U0001F565': u':clock1030:', - u'\U0001F559': u':clock10:', - u'\U0001F566': u':clock1130:', - u'\U0001F55A': u':clock11:', - u'\U0001F567': u':clock1230:', - u'\U0001F55B': u':clock12:', - u'\U0001F55C': u':clock130:', - u'\U0001F550': u':clock1:', - u'\U0001F55D': u':clock230:', - u'\U0001F551': u':clock2:', - u'\U0001F55E': u':clock330:', - u'\U0001F552': u':clock3:', - u'\U0001F55F': u':clock430:', - u'\U0001F553': u':clock4:', - u'\U0001F560': u':clock530:', - u'\U0001F554': u':clock5:', - u'\U0001F561': u':clock630:', - u'\U0001F555': u':clock6:', - u'\U0001F562': u':clock730:', - u'\U0001F556': u':clock7:', - u'\U0001F563': u':clock830:', - u'\U0001F557': u':clock8:', - u'\U0001F564': u':clock930:', - u'\U0001F558': u':clock9:', - u'\U0001F563': u':clock_face_eight-thirty:', - u'\U0001F557': u':clock_face_eight_oclock:', - u'\U0001F566': u':clock_face_eleven-thirty:', - u'\U0001F55A': u':clock_face_eleven_oclock:', - u'\U0001F560': u':clock_face_five-thirty:', - u'\U0001F554': u':clock_face_five_oclock:', - u'\U0001F55F': u':clock_face_four-thirty:', - u'\U0001F553': u':clock_face_four_oclock:', - u'\U0001F564': u':clock_face_nine-thirty:', - u'\U0001F558': u':clock_face_nine_oclock:', - u'\U0001F55C': u':clock_face_one-thirty:', - u'\U0001F550': u':clock_face_one_oclock:', - u'\U0001F562': u':clock_face_seven-thirty:', - u'\U0001F556': u':clock_face_seven_oclock:', - u'\U0001F561': u':clock_face_six-thirty:', - u'\U0001F555': u':clock_face_six_oclock:', - u'\U0001F565': u':clock_face_ten-thirty:', - u'\U0001F559': u':clock_face_ten_oclock:', - u'\U0001F55E': u':clock_face_three-thirty:', - u'\U0001F552': u':clock_face_three_oclock:', - u'\U0001F567': u':clock_face_twelve-thirty:', - u'\U0001F55B': u':clock_face_twelve_oclock:', - u'\U0001F55D': u':clock_face_two-thirty:', - u'\U0001F551': u':clock_face_two_oclock:', - u'\U0001F4D5': u':closed_book:', - u'\U0001F510': u':closed_lock_with_key:', - u'\U0001F4EA': u':closed_mailbox_with_lowered_flag:', - u'\U0001F4EB': u':closed_mailbox_with_raised_flag:', - u'\U0001F302': u':closed_umbrella:', + u'\U000024c2': u':circled_latin_capital_letter_m:', + u'\U0001f3aa': u':circus_tent:', + u'\U0001f307': u':city_sunrise:', + u'\U0001f306': u':city_sunset:', + u'\U0001f3d9': u':cityscape:', + u'\U0001f191': u':cl:', + u'\U0001f44f': u':clap:', + u'\U0001f3ac': u':clapper:', + u'\U0001f3db': u':classical_building:', + u'\U0001f4cb': u':clipboard:', + u'\U0001f565': u':clock1030:', + u'\U0001f559': u':clock10:', + u'\U0001f566': u':clock1130:', + u'\U0001f55a': u':clock11:', + u'\U0001f567': u':clock1230:', + u'\U0001f55b': u':clock12:', + u'\U0001f55c': u':clock130:', + u'\U0001f550': u':clock1:', + u'\U0001f55d': u':clock230:', + u'\U0001f551': u':clock2:', + u'\U0001f55e': u':clock330:', + u'\U0001f552': u':clock3:', + u'\U0001f55f': u':clock430:', + u'\U0001f553': u':clock4:', + u'\U0001f560': u':clock530:', + u'\U0001f554': u':clock5:', + u'\U0001f561': u':clock630:', + u'\U0001f555': u':clock6:', + u'\U0001f562': u':clock730:', + u'\U0001f556': u':clock7:', + u'\U0001f563': u':clock830:', + u'\U0001f557': u':clock8:', + u'\U0001f564': u':clock930:', + u'\U0001f558': u':clock9:', + u'\U0001f4d5': u':closed_book:', + u'\U0001f510': u':closed_lock_with_key:', + u'\U0001f4ea': u':closed_mailbox_with_lowered_flag:', + u'\U0001f4eb': u':closed_mailbox_with_raised_flag:', + u'\U0001f302': u':closed_umbrella:', u'\U00002601': u':cloud:', - u'\U0001F329': u':cloud_with_lightning:', - u'\U0001F327': u':cloud_with_rain:', - u'\U0001F328': u':cloud_with_snow:', - u'\U0001F32A': u':cloud_with_tornado:', - u'\U00002663': u':clubs:', - u'\U0001F378': u':cocktail:', - u'\U0001F378': u':cocktail_glass:', + u'\U0001f329': u':cloud_with_lightning:', + u'\U0001f327': u':cloud_with_rain:', + u'\U0001f328': u':cloud_with_snow:', + u'\U0001f32a': u':cloud_with_tornado:', + u'\U0001f378': u':cocktail:', u'\U00002615': u':coffee:', - u'\U000026B0': u':coffin:', - u'\U0001F630': u':cold_sweat:', - u'\U0001F4A5': u':collision:', - u'\U0001F4A5': u':collision_symbol:', + u'\U000026b0': u':coffin:', + u'\U0001f630': u':cold_sweat:', u'\U00002604': u':comet:', - u'\U0001F5DC': u':compression:', - u'\U0001F4BB': u':computer:', - u'\U0001F38A': u':confetti_ball:', - u'\U0001F616': u':confounded:', - u'\U0001F616': u':confounded_face:', - u'\U0001F615': u':confused:', - u'\U0001F615': u':confused_face:', - u'\U00003297': u':congratulations:', - u'\U0001F6A7': u':construction:', - u'\U0001F6A7': u':construction_sign:', - u'\U0001F477': u':construction_worker:', - u'\U0001F39B': u':control_knobs:', - u'\U0001F3EA': u':convenience_store:', - u'\U0001F35A': u':cooked_rice:', - u'\U0001F36A': u':cookie:', - u'\U0001F373': u':cooking:', - u'\U0001F192': u':cool:', - u'\U0001F46E': u':cop:', - u'\U000000A9': u':copyright:', - u'\U000000A9': u':copyright_sign:', - u'\U0001F33D': u':corn:', - u'\U0001F6CB': u':couch_and_lamp:', - u'\U0001F46B': u':couple:', - u'\U0001F491': u':couple_with_heart:', - u'\U0001F48F': u':couplekiss:', - u'\U0001F404': u':cow2:', - u'\U0001F404': u':cow:', - u'\U0001F42E': u':cow:', - u'\U0001F42E': u':cow_face:', - u'\U0001F980': u':crab:', - u'\U0001F4B3': u':credit_card:', - u'\U0001F319': u':crescent_moon:', - u'\U0001F3CF': u':cricket_bat_and_ball:', - u'\U0001F40A': u':crocodile:', - u'\U0000274C': u':cross_mark:', - u'\U0001F38C': u':crossed_flags:', + u'\U0001f5dc': u':compression:', + u'\U0001f4bb': u':computer:', + u'\U0001f38a': u':confetti_ball:', + u'\U0001f616': u':confounded:', + u'\U0001f615': u':confused:', + u'\U0001f6a7': u':construction:', + u'\U0001f477': u':construction_worker:', + u'\U0001f39b': u':control_knobs:', + u'\U0001f3ea': u':convenience_store:', + u'\U0001f35a': u':cooked_rice:', + u'\U0001f36a': u':cookie:', + u'\U0001f373': u':cooking:', + u'\U0001f192': u':cool:', + u'\U0001f46e': u':cop:', + u'\U000000a9': u':copyright:', + u'\U0001f33d': u':corn:', + u'\U0001f6cb': u':couch_and_lamp:', + u'\U0001f46b': u':couple:', + u'\U0001f491': u':couple_with_heart:', + u'\U0001f48f': u':couplekiss:', + u'\U0001f404': u':cow2:', + u'\U0001f42e': u':cow:', + u'\U0001f980': u':crab:', + u'\U0001f4b3': u':credit_card:', + u'\U0001f319': u':crescent_moon:', + u'\U0001f3cf': u':cricket_bat_and_ball:', + u'\U0001f40a': u':crocodile:', + u'\U0000274c': u':cross_mark:', + u'\U0001f38c': u':crossed_flags:', u'\U00002694': u':crossed_swords:', - u'\U0001F451': u':crown:', - u'\U0001F622': u':cry:', - u'\U0001F63F': u':crying_cat_face:', - u'\U0001F622': u':crying_face:', - u'\U0001F52E': u':crystal_ball:', - u'\U0001F498': u':cupid:', - u'\U000027B0': u':curly_loop:', - u'\U0001F4B1': u':currency_exchange:', - u'\U0001F35B': u':curry:', - u'\U0001F35B': u':curry_and_rice:', - u'\U0001F36E': u':custard:', - u'\U0001F6C3': u':customs:', - u'\U0001F300': u':cyclone:', - u'\U0001F5E1': u':dagger_knife:', - u'\U0001F483': u':dancer:', - u'\U0001F46F': u':dancers:', - u'\U0001F361': u':dango:', - u'\U0001F576': u':dark_sunglasses:', - u'\U0001F3AF': u':dart:', - u'\U0001F4A8': u':dash:', - u'\U0001F4A8': u':dash_symbol:', - u'\U0001F4C5': u':date:', - u'\U0001F333': u':deciduous_tree:', - u'\U0001F69A': u':delivery_truck:', - u'\U0001F3EC': u':department_store:', - u'\U0001F3DA': u':derelict_house_building:', - u'\U0001F3DC': u':desert:', - u'\U0001F3DD': u':desert_island:', - u'\U0001F5A5': u':desktop_computer:', - u'\U0001F4A0': u':diamond_shape_with_a_dot_inside:', - u'\U00002666': u':diamonds:', - u'\U0001F3AF': u':direct_hit:', - u'\U0001F61E': u':disappointed:', - u'\U0001F625': u':disappointed_but_relieved_face:', - u'\U0001F61E': u':disappointed_face:', - u'\U0001F625': u':disappointed_relieved:', - u'\U0001F4AB': u':dizzy:', - u'\U0001F635': u':dizzy_face:', - u'\U0001F4AB': u':dizzy_symbol:', - u'\U0001F6AF': u':do_not_litter:', - u'\U0001F6AF': u':do_not_litter_symbol:', - u'\U0001F415': u':dog2:', - u'\U0001F415': u':dog:', - u'\U0001F436': u':dog:', - u'\U0001F436': u':dog_face:', - u'\U0001F4B5': u':dollar:', - u'\U0001F38E': u':dolls:', - u'\U0001F42C': u':dolphin:', - u'\U0001F6AA': u':door:', - u'\U000027BF': u':double_curly_loop:', - u'\U0000203C': u':double_exclamation_mark:', - u'\U000023F8': u':double_vertical_bar:', - u'\U0001F369': u':doughnut:', - u'\U0001F54A': u':dove_of_peace:', - u'\U0001F53B': u':down-pointing_red_triangle:', - u'\U0001F53D': u':down-pointing_small_red_triangle:', - u'\U00002B07': u':downwards_black_arrow:', - u'\U0001F409': u':dragon:', - u'\U0001F432': u':dragon_face:', - u'\U0001F457': u':dress:', - u'\U0001F42A': u':dromedary_camel:', - u'\U0001F4A7': u':droplet:', - u'\U0001F4C0': u':dvd:', - u'\U0001F4E7': u':e-mail:', - u'\U0001F4E7': u':e-mail_symbol:', - u'\U0001F442': u':ear:', - u'\U0001F33D': u':ear_of_maize:', - u'\U0001F33E': u':ear_of_rice:', - u'\U0001F30D': u':earth_africa:', - u'\U0001F30E': u':earth_americas:', - u'\U0001F30F': u':earth_asia:', - u'\U0001F30E': u':earth_globe_americas:', - u'\U0001F30F': u':earth_globe_asia-australia:', - u'\U0001F30D': u':earth_globe_europe-africa:', - u'\U0001F373': u':egg:', - u'\U0001F346': u':eggplant:', + u'\U0001f451': u':crown:', + u'\U0001f622': u':cry:', + u'\U0001f63f': u':crying_cat_face:', + u'\U0001f52e': u':crystal_ball:', + u'\U0001f498': u':cupid:', + u'\U000027b0': u':curly_loop:', + u'\U0001f4b1': u':currency_exchange:', + u'\U0001f35b': u':curry:', + u'\U0001f36e': u':custard:', + u'\U0001f6c3': u':customs:', + u'\U0001f300': u':cyclone:', + u'\U0001f5e1': u':dagger_knife:', + u'\U0001f483': u':dancer:', + u'\U0001f46f': u':dancers:', + u'\U0001f361': u':dango:', + u'\U0001f576': u':dark_sunglasses:', + u'\U0001f3af': u':dart:', + u'\U0001f4a8': u':dash:', + u'\U0001f333': u':deciduous_tree:', + u'\U0001f69a': u':delivery_truck:', + u'\U0001f3ec': u':department_store:', + u'\U0001f3da': u':derelict_house_building:', + u'\U0001f3dc': u':desert:', + u'\U0001f3dd': u':desert_island:', + u'\U0001f5a5': u':desktop_computer:', + u'\U0001f4a0': u':diamond_shape_with_a_dot_inside:', + u'\U0001f61e': u':disappointed:', + u'\U0001f625': u':disappointed_but_relieved_face:', + u'\U0001f4ab': u':dizzy:', + u'\U0001f635': u':dizzy_face:', + u'\U0001f6af': u':do_not_litter:', + u'\U0001f415': u':dog2:', + u'\U0001f436': u':dog:', + u'\U0001f38e': u':dolls:', + u'\U0001f42c': u':dolphin:', + u'\U0001f6aa': u':door:', + u'\U000027bf': u':double_curly_loop:', + u'\U000023f8': u':double_vertical_bar:', + u'\U0001f369': u':doughnut:', + u'\U0001f54a': u':dove_of_peace:', + u'\U0001f53b': u':down-pointing_red_triangle:', + u'\U0001f409': u':dragon:', + u'\U0001f432': u':dragon_face:', + u'\U0001f457': u':dress:', + u'\U0001f42a': u':dromedary_camel:', + u'\U0001f4a7': u':droplet:', + u'\U0001f4c0': u':dvd:', + u'\U0001f4e7': u':e-mail:', + u'\U0001f442': u':ear:', + u'\U0001f33e': u':ear_of_rice:', + u'\U0001f30d': u':earth_africa:', + u'\U0001f30e': u':earth_americas:', + u'\U0001f30f': u':earth_asia:', u'\U00002734': u':eight_pointed_black_star:', u'\U00002733': u':eight_spoked_asterisk:', - u'\U000023CF': u':eject_symbol:', - u'\U0001F4A1': u':electric_light_bulb:', - u'\U0001F50C': u':electric_plug:', - u'\U0001F526': u':electric_torch:', - u'\U0001F418': u':elephant:', + u'\U000023cf': u':eject_symbol:', + u'\U0001f50c': u':electric_plug:', + u'\U0001f526': u':electric_torch:', + u'\U0001f418': u':elephant:', u'\U00002709': u':email:', - u'\U0001F3FB': u':emoji_modifier_fitzpatrick_type-1-2:', - u'\U0001F3FC': u':emoji_modifier_fitzpatrick_type-3:', - u'\U0001F3FD': u':emoji_modifier_fitzpatrick_type-4:', - u'\U0001F3FE': u':emoji_modifier_fitzpatrick_type-5:', - u'\U0001F3FF': u':emoji_modifier_fitzpatrick_type-6:', - u'\U0001F51A': u':end:', - u'\U0001F51A': u':end_with_leftwards_arrow_above:', - u'\U00002709': u':envelope:', - u'\U0001F4E9': u':envelope_with_arrow:', - u'\U0001F4E9': u':envelope_with_downwards_arrow_above:', - u'\U0001F4B6': u':euro:', - u'\U0001F3F0': u':european_castle:', - u'\U0001F3E4': u':european_post_office:', - u'\U0001F332': u':evergreen_tree:', + u'\U0001f3fb': u':emoji_modifier_fitzpatrick_type-1-2:', + u'\U0001f3fc': u':emoji_modifier_fitzpatrick_type-3:', + u'\U0001f3fd': u':emoji_modifier_fitzpatrick_type-4:', + u'\U0001f3fe': u':emoji_modifier_fitzpatrick_type-5:', + u'\U0001f3ff': u':emoji_modifier_fitzpatrick_type-6:', + u'\U0001f51a': u':end:', + u'\U0001f4e9': u':envelope_with_arrow:', + u'\U0001f3f0': u':european_castle:', + u'\U0001f3e4': u':european_post_office:', + u'\U0001f332': u':evergreen_tree:', u'\U00002757': u':exclamation:', u'\U00002049': u':exclamation_question_mark:', - u'\U0001F611': u':expressionless:', - u'\U0001F611': u':expressionless_face:', - u'\U0001F47D': u':extraterrestrial_alien:', - u'\U0001F441': u':eye:', - u'\U0001F453': u':eyeglasses:', - u'\U0001F440': u':eyes:', - u'\U0001F486': u':face_massage:', - u'\U0001F60B': u':face_savouring_delicious_food:', - u'\U0001F631': u':face_screaming_in_fear:', - u'\U0001F618': u':face_throwing_a_kiss:', - u'\U0001F613': u':face_with_cold_sweat:', - u'\U0001F915': u':face_with_head-bandage:', - u'\U0001F915': u':face_with_head_bandage:', - u'\U0001F624': u':face_with_look_of_triumph:', - u'\U0001F637': u':face_with_medical_mask:', - u'\U0001F645': u':face_with_no_good_gesture:', - u'\U0001F646': u':face_with_ok_gesture:', - u'\U0001F62E': u':face_with_open_mouth:', - u'\U0001F630': u':face_with_open_mouth_and_cold_sweat:', - u'\U0001F644': u':face_with_rolling_eyes:', - u'\U0001F61B': u':face_with_stuck-out_tongue:', - u'\U0001F602': u':face_with_tears_of_joy:', - u'\U0001F912': u':face_with_thermometer:', - u'\U0001F636': u':face_without_mouth:', - u'\U0001F44A': u':facepunch:', - u'\U0001F3ED': u':factory:', - u'\U0001F342': u':fallen_leaf:', - u'\U0001F46A': u':family:', - u'\U000023E9': u':fast_forward:', - u'\U0001F385': u':father_christmas:', - u'\U0001F4E0': u':fax:', - u'\U0001F4E0': u':fax_machine:', - u'\U0001F628': u':fearful:', - u'\U0001F628': u':fearful_face:', - u'\U0001F43E': u':feet:', - u'\U0001F3A1': u':ferris_wheel:', - u'\U000026F4': u':ferry:', - u'\U0001F3D1': u':field_hockey_stick_and_ball:', - u'\U0001F5C4': u':file_cabinet:', - u'\U0001F4C1': u':file_folder:', - u'\U0001F39E': u':film_frames:', - u'\U0001F4FD': u':film_projector:', - u'\U0001F525': u':fire:', - u'\U0001F692': u':fire_engine:', - u'\U0001F387': u':firework_sparkler:', - u'\U0001F386': u':fireworks:', - u'\U0001F313': u':first_quarter_moon:', - u'\U0001F313': u':first_quarter_moon_symbol:', - u'\U0001F31B': u':first_quarter_moon_with_face:', - u'\U0001F41F': u':fish:', - u'\U0001F365': u':fish_cake:', - u'\U0001F365': u':fish_cake_with_swirl_design:', - u'\U0001F3A3': u':fishing_pole_and_fish:', - u'\U0000270A': u':fist:', - u'\U0001F44A': u':fisted_hand_sign:', - u'\U000026F3': u':flag_in_hole:', - u'\U0001F38F': u':flags:', - u'\U0001F526': u':flashlight:', - u'\U0000269C': u':fleur-de-lis:', - u'\U0000269C': u':fleur_de_lis:', - u'\U0001F4AA': u':flexed_biceps:', - u'\U0001F42C': u':flipper:', - u'\U0001F4BE': u':floppy_disk:', - u'\U0001F3B4': u':flower_playing_cards:', - u'\U0001F633': u':flushed:', - u'\U0001F633': u':flushed_face:', - u'\U0001F32B': u':fog:', - u'\U0001F301': u':foggy:', - u'\U0001F3C8': u':football:', - u'\U0001F463': u':footprints:', - u'\U0001F374': u':fork_and_knife:', - u'\U0001F37D': u':fork_and_knife_with_plate:', - u'\U000026F2': u':fountain:', - u'\U0001F340': u':four_leaf_clover:', - u'\U0001F5BC': u':frame_with_picture:', - u'\U0001F193': u':free:', - u'\U0001F35F': u':french_fries:', - u'\U0001F364': u':fried_shrimp:', - u'\U0001F35F': u':fries:', - u'\U0001F438': u':frog:', - u'\U0001F438': u':frog_face:', - u'\U0001F425': u':front-facing_baby_chick:', - u'\U0001F626': u':frowning:', - u'\U0001F626': u':frowning_face_with_open_mouth:', - u'\U000026FD': u':fuel_pump:', - u'\U000026FD': u':fuelpump:', - u'\U0001F315': u':full_moon:', - u'\U0001F315': u':full_moon_symbol:', - u'\U0001F31D': u':full_moon_with_face:', - u'\U000026B1': u':funeral_urn:', - u'\U0001F3B2': u':game_die:', + u'\U0001f611': u':expressionless:', + u'\U0001f441': u':eye:', + u'\U0001f453': u':eyeglasses:', + u'\U0001f440': u':eyes:', + u'\U0001f486': u':face_massage:', + u'\U0001f60b': u':face_savouring_delicious_food:', + u'\U0001f631': u':face_screaming_in_fear:', + u'\U0001f618': u':face_throwing_a_kiss:', + u'\U0001f613': u':face_with_cold_sweat:', + u'\U0001f915': u':face_with_head-bandage:', + u'\U0001f624': u':face_with_look_of_triumph:', + u'\U0001f637': u':face_with_medical_mask:', + u'\U0001f645': u':face_with_no_good_gesture:', + u'\U0001f646': u':face_with_ok_gesture:', + u'\U0001f62e': u':face_with_open_mouth:', + u'\U0001f644': u':face_with_rolling_eyes:', + u'\U0001f61b': u':face_with_stuck-out_tongue:', + u'\U0001f602': u':face_with_tears_of_joy:', + u'\U0001f912': u':face_with_thermometer:', + u'\U0001f636': u':face_without_mouth:', + u'\U0001f44a': u':facepunch:', + u'\U0001f3ed': u':factory:', + u'\U0001f342': u':fallen_leaf:', + u'\U0001f46a': u':family:', + u'\U0001f385': u':father_christmas:', + u'\U0001f4e0': u':fax:', + u'\U0001f628': u':fearful:', + u'\U0001f43e': u':feet:', + u'\U0001f3a1': u':ferris_wheel:', + u'\U000026f4': u':ferry:', + u'\U0001f3d1': u':field_hockey_stick_and_ball:', + u'\U0001f5c4': u':file_cabinet:', + u'\U0001f4c1': u':file_folder:', + u'\U0001f39e': u':film_frames:', + u'\U0001f4fd': u':film_projector:', + u'\U0001f525': u':fire:', + u'\U0001f692': u':fire_engine:', + u'\U0001f387': u':firework_sparkler:', + u'\U0001f386': u':fireworks:', + u'\U0001f313': u':first_quarter_moon:', + u'\U0001f31b': u':first_quarter_moon_with_face:', + u'\U0001f41f': u':fish:', + u'\U0001f365': u':fish_cake:', + u'\U0001f3a3': u':fishing_pole_and_fish:', + u'\U0000270a': u':fist:', + u'\U000026f3': u':flag_in_hole:', + u'\U0000269c': u':fleur-de-lis:', + u'\U0001f4aa': u':flexed_biceps:', + u'\U0001f4be': u':floppy_disk:', + u'\U0001f3b4': u':flower_playing_cards:', + u'\U0001f633': u':flushed:', + u'\U0001f32b': u':fog:', + u'\U0001f301': u':foggy:', + u'\U0001f463': u':footprints:', + u'\U0001f374': u':fork_and_knife:', + u'\U0001f37d': u':fork_and_knife_with_plate:', + u'\U000026f2': u':fountain:', + u'\U0001f340': u':four_leaf_clover:', + u'\U0001f5bc': u':frame_with_picture:', + u'\U0001f193': u':free:', + u'\U0001f35f': u':french_fries:', + u'\U0001f364': u':fried_shrimp:', + u'\U0001f438': u':frog:', + u'\U0001f425': u':front-facing_baby_chick:', + u'\U0001f626': u':frowning:', + u'\U000026fd': u':fuel_pump:', + u'\U0001f315': u':full_moon:', + u'\U0001f31d': u':full_moon_with_face:', + u'\U000026b1': u':funeral_urn:', + u'\U0001f3b2': u':game_die:', u'\U00002699': u':gear:', - u'\U0001F48E': u':gem:', - u'\U0001F48E': u':gem_stone:', - u'\U0000264A': u':gemini:', - u'\U0001F47B': u':ghost:', - u'\U0001F381': u':gift:', - u'\U0001F49D': u':gift_heart:', - u'\U0001F467': u':girl:', - u'\U0001F310': u':globe_with_meridians:', - u'\U0001F31F': u':glowing_star:', - u'\U0001F410': u':goat:', - u'\U000026F3': u':golf:', - u'\U0001F3CC': u':golfer:', - u'\U0001F393': u':graduation_cap:', - u'\U0001F347': u':grapes:', - u'\U0001F34F': u':green_apple:', - u'\U0001F4D7': u':green_book:', - u'\U0001F49A': u':green_heart:', + u'\U0001f48e': u':gem:', + u'\U0000264a': u':gemini:', + u'\U0001f47b': u':ghost:', + u'\U0001f381': u':gift:', + u'\U0001f49d': u':gift_heart:', + u'\U0001f467': u':girl:', + u'\U0001f310': u':globe_with_meridians:', + u'\U0001f31f': u':glowing_star:', + u'\U0001f410': u':goat:', + u'\U0001f3cc': u':golfer:', + u'\U0001f393': u':graduation_cap:', + u'\U0001f347': u':grapes:', + u'\U0001f34f': u':green_apple:', + u'\U0001f4d7': u':green_book:', + u'\U0001f49a': u':green_heart:', u'\U00002755': u':grey_exclamation:', u'\U00002754': u':grey_question:', - u'\U0001F62C': u':grimacing:', - u'\U0001F62C': u':grimacing_face:', - u'\U0001F601': u':grin:', - u'\U0001F600': u':grinning:', - u'\U0001F638': u':grinning_cat_face_with_smiling_eyes:', - u'\U0001F600': u':grinning_face:', - u'\U0001F601': u':grinning_face_with_smiling_eyes:', - u'\U0001F497': u':growing_heart:', - u'\U0001F482': u':guardsman:', - u'\U0001F3B8': u':guitar:', - u'\U0001F52B': u':gun:', - u'\U0001F487': u':haircut:', - u'\U0001F354': u':hamburger:', - u'\U0001F528': u':hammer:', + u'\U0001f62c': u':grimacing:', + u'\U0001f601': u':grin:', + u'\U0001f600': u':grinning:', + u'\U0001f638': u':grinning_cat_face_with_smiling_eyes:', + u'\U0001f497': u':growing_heart:', + u'\U0001f482': u':guardsman:', + u'\U0001f3b8': u':guitar:', + u'\U0001f52b': u':gun:', + u'\U0001f487': u':haircut:', + u'\U0001f354': u':hamburger:', + u'\U0001f528': u':hammer:', u'\U00002692': u':hammer_and_pick:', - u'\U0001F6E0': u':hammer_and_wrench:', - u'\U0001F439': u':hamster:', - u'\U0001F439': u':hamster_face:', - u'\U0000270B': u':hand:', - u'\U0001F45C': u':handbag:', - u'\U0001F4A9': u':hankey:', - u'\U0001F64B': u':happy_person_raising_one_hand:', - u'\U0001F425': u':hatched_chick:', - u'\U0001F423': u':hatching_chick:', - u'\U0001F3A7': u':headphone:', - u'\U0001F3A7': u':headphones:', - u'\U0001F649': u':hear-no-evil_monkey:', - u'\U0001F649': u':hear_no_evil:', + u'\U0001f6e0': u':hammer_and_wrench:', + u'\U0001f439': u':hamster:', + u'\U0000270b': u':hand:', + u'\U0001f45c': u':handbag:', + u'\U0001f4a9': u':hankey:', + u'\U0001f64b': u':happy_person_raising_one_hand:', + u'\U0001f423': u':hatching_chick:', + u'\U0001f3a7': u':headphone:', + u'\U0001f649': u':hear-no-evil_monkey:', u'\U00002764': u':heart:', - u'\U0001F49F': u':heart_decoration:', - u'\U0001F60D': u':heart_eyes:', - u'\U0001F63B': u':heart_eyes_cat:', - u'\U0001F498': u':heart_with_arrow:', - u'\U0001F49D': u':heart_with_ribbon:', - u'\U0001F493': u':heartbeat:', - u'\U0001F497': u':heartpulse:', - u'\U00002665': u':hearts:', - u'\U00002764': u':heavy_black_heart:', + u'\U0001f49f': u':heart_decoration:', + u'\U0001f60d': u':heart_eyes:', + u'\U0001f63b': u':heart_eyes_cat:', u'\U00002714': u':heavy_check_mark:', u'\U00002797': u':heavy_division_sign:', - u'\U0001F4B2': u':heavy_dollar_sign:', - u'\U00002757': u':heavy_exclamation_mark:', - u'\U00002757': u':heavy_exclamation_mark_symbol:', + u'\U0001f4b2': u':heavy_dollar_sign:', u'\U00002763': u':heavy_heart_exclamation_mark_ornament:', - u'\U00002B55': u':heavy_large_circle:', + u'\U00002b55': u':heavy_large_circle:', u'\U00002796': u':heavy_minus_sign:', u'\U00002716': u':heavy_multiplication_x:', u'\U00002795': u':heavy_plus_sign:', - u'\U0001F681': u':helicopter:', + u'\U0001f681': u':helicopter:', u'\U00002388': u':helm_symbol:', - u'\U000026D1': u':helmet_with_white_cross:', - u'\U0001F33F': u':herb:', - u'\U0001F33A': u':hibiscus:', - u'\U0001F460': u':high-heeled_shoe:', - u'\U0001F684': u':high-speed_train:', - u'\U0001F685': u':high-speed_train_with_bullet_nose:', - u'\U0001F506': u':high_brightness:', - u'\U0001F506': u':high_brightness_symbol:', - u'\U0001F460': u':high_heel:', - u'\U000026A1': u':high_voltage_sign:', - u'\U0001F52A': u':hocho:', - u'\U0001F573': u':hole:', - u'\U0001F36F': u':honey_pot:', - u'\U0001F41D': u':honeybee:', - u'\U0001F6A5': u':horizontal_traffic_light:', - u'\U0001F40E': u':horse:', - u'\U0001F434': u':horse:', - u'\U0001F434': u':horse_face:', - u'\U0001F3C7': u':horse_racing:', - u'\U0001F3E5': u':hospital:', - u'\U00002615': u':hot_beverage:', - u'\U0001F32D': u':hot_dog:', - u'\U0001F336': u':hot_pepper:', + u'\U000026d1': u':helmet_with_white_cross:', + u'\U0001f33f': u':herb:', + u'\U0001f33a': u':hibiscus:', + u'\U0001f460': u':high-heeled_shoe:', + u'\U0001f506': u':high_brightness:', + u'\U000026a1': u':high_voltage_sign:', + u'\U0001f52a': u':hocho:', + u'\U0001f573': u':hole:', + u'\U0001f36f': u':honey_pot:', + u'\U0001f6a5': u':horizontal_traffic_light:', + u'\U0001f40e': u':horse:', + u'\U0001f434': u':horse:', + u'\U0001f3c7': u':horse_racing:', + u'\U0001f3e5': u':hospital:', + u'\U0001f32d': u':hot_dog:', + u'\U0001f336': u':hot_pepper:', u'\U00002668': u':hot_springs:', - u'\U0001F32D': u':hotdog:', - u'\U0001F3E8': u':hotel:', - u'\U00002668': u':hotsprings:', - u'\U0000231B': u':hourglass:', - u'\U000023F3': u':hourglass_flowing_sand:', - u'\U000023F3': u':hourglass_with_flowing_sand:', - u'\U0001F3E0': u':house:', - u'\U0001F3E0': u':house_building:', - u'\U0001F3D8': u':house_buildings:', - u'\U0001F3E1': u':house_with_garden:', - u'\U0001F917': u':hugging_face:', - u'\U0001F4AF': u':hundred_points_symbol:', - u'\U0001F62F': u':hushed:', - u'\U0001F62F': u':hushed_face:', - u'\U0001F368': u':ice_cream:', - u'\U0001F3D2': u':ice_hockey_stick_and_puck:', - u'\U000026F8': u':ice_skate:', - u'\U0001F366': u':icecream:', - u'\U0001F194': u':id:', - u'\U0001F250': u':ideograph_advantage:', - u'\U0001F47F': u':imp:', - u'\U0001F4E5': u':inbox_tray:', - u'\U0001F4E8': u':incoming_envelope:', - u'\U0001F481': u':information_desk_person:', + u'\U0001f3e8': u':hotel:', + u'\U0000231b': u':hourglass:', + u'\U000023f3': u':hourglass_flowing_sand:', + u'\U0001f3e0': u':house:', + u'\U0001f3d8': u':house_buildings:', + u'\U0001f3e1': u':house_with_garden:', + u'\U0001f917': u':hugging_face:', + u'\U0001f62f': u':hushed:', + u'\U0001f368': u':ice_cream:', + u'\U0001f3d2': u':ice_hockey_stick_and_puck:', + u'\U000026f8': u':ice_skate:', + u'\U0001f366': u':icecream:', + u'\U0001f194': u':id:', + u'\U0001f47f': u':imp:', + u'\U0001f4e5': u':inbox_tray:', + u'\U0001f4e8': u':incoming_envelope:', + u'\U0001f481': u':information_desk_person:', u'\U00002139': u':information_source:', - u'\U0001F607': u':innocent:', - u'\U0001F520': u':input_symbol_for_latin_capital_letters:', - u'\U0001F524': u':input_symbol_for_latin_letters:', - u'\U0001F521': u':input_symbol_for_latin_small_letters:', - u'\U0001F522': u':input_symbol_for_numbers:', - u'\U0001F523': u':input_symbol_for_symbols:', - u'\U00002049': u':interrobang:', - u'\U0001F4F1': u':iphone:', - u'\U0001F3EE': u':izakaya_lantern:', - u'\U0001F383': u':jack-o-lantern:', - u'\U0001F383': u':jack_o_lantern:', - u'\U0001F5FE': u':japan:', - u'\U0001F3EF': u':japanese_castle:', - u'\U0001F38E': u':japanese_dolls:', - u'\U0001F47A': u':japanese_goblin:', - u'\U0001F479': u':japanese_ogre:', - u'\U0001F3E3': u':japanese_post_office:', - u'\U0001F530': u':japanese_symbol_for_beginner:', - u'\U0001F456': u':jeans:', - u'\U0001F602': u':joy:', - u'\U0001F639': u':joy_cat:', - u'\U0001F579': u':joystick:', - u'\U0001F54B': u':kaaba:', - u'\U0001F511': u':key:', + u'\U0001f607': u':innocent:', + u'\U0001f523': u':input_symbol_for_symbols:', + u'\U0001f4f1': u':iphone:', + u'\U0001f3ee': u':izakaya_lantern:', + u'\U0001f383': u':jack-o-lantern:', + u'\U0001f5fe': u':japan:', + u'\U0001f3ef': u':japanese_castle:', + u'\U0001f47a': u':japanese_goblin:', + u'\U0001f479': u':japanese_ogre:', + u'\U0001f3e3': u':japanese_post_office:', + u'\U0001f456': u':jeans:', + u'\U0001f579': u':joystick:', + u'\U0001f54b': u':kaaba:', + u'\U0001f511': u':key:', u'\U00002328': u':keyboard:', - u'\U0001F51F': u':keycap_ten:', - u'\U0001F458': u':kimono:', - u'\U0001F48B': u':kiss:', - u'\U0001F48F': u':kiss:', - u'\U0001F48B': u':kiss_mark:', - u'\U0001F617': u':kissing:', - u'\U0001F63D': u':kissing_cat:', - u'\U0001F63D': u':kissing_cat_face_with_closed_eyes:', - u'\U0001F61A': u':kissing_closed_eyes:', - u'\U0001F617': u':kissing_face:', - u'\U0001F61A': u':kissing_face_with_closed_eyes:', - u'\U0001F619': u':kissing_face_with_smiling_eyes:', - u'\U0001F618': u':kissing_heart:', - u'\U0001F619': u':kissing_smiling_eyes:', - u'\U0001F52A': u':knife:', - u'\U0001F37D': u':knife_fork_plate:', - u'\U0001F428': u':koala:', - u'\U0001F201': u':koko:', - u'\U0001F3F7': u':label:', - u'\U0001F41E': u':lady_beetle:', - u'\U0001F3EE': u':lantern:', - u'\U0001F535': u':large_blue_circle:', - u'\U0001F537': u':large_blue_diamond:', - u'\U0001F536': u':large_orange_diamond:', - u'\U0001F534': u':large_red_circle:', - u'\U0001F317': u':last_quarter_moon:', - u'\U0001F317': u':last_quarter_moon_symbol:', - u'\U0001F31C': u':last_quarter_moon_with_face:', - u'\U0000271D': u':latin_cross:', - u'\U0001F606': u':laughing:', - u'\U0001F343': u':leaf_fluttering_in_wind:', - u'\U0001F343': u':leaves:', - u'\U0001F4D2': u':ledger:', - u'\U0001F50D': u':left-pointing_magnifying_glass:', - u'\U0001F6C5': u':left_luggage:', + u'\U0001f51f': u':keycap_ten:', + u'\U0001f458': u':kimono:', + u'\U0001f48b': u':kiss:', + u'\U0001f617': u':kissing:', + u'\U0001f63d': u':kissing_cat:', + u'\U0001f61a': u':kissing_closed_eyes:', + u'\U0001f619': u':kissing_face_with_smiling_eyes:', + u'\U0001f428': u':koala:', + u'\U0001f201': u':koko:', + u'\U0001f3f7': u':label:', + u'\U0001f535': u':large_blue_circle:', + u'\U0001f537': u':large_blue_diamond:', + u'\U0001f536': u':large_orange_diamond:', + u'\U0001f534': u':large_red_circle:', + u'\U0001f317': u':last_quarter_moon:', + u'\U0001f31c': u':last_quarter_moon_with_face:', + u'\U0000271d': u':latin_cross:', + u'\U0001f606': u':laughing:', + u'\U0001f343': u':leaf_fluttering_in_wind:', + u'\U0001f4d2': u':ledger:', + u'\U0001f50d': u':left-pointing_magnifying_glass:', + u'\U0001f6c5': u':left_luggage:', u'\U00002194': u':left_right_arrow:', - u'\U0001F4AC': u':left_speech_bubble:', - u'\U000021A9': u':leftwards_arrow_with_hook:', - u'\U00002B05': u':leftwards_black_arrow:', - u'\U0001F34B': u':lemon:', - u'\U0000264C': u':leo:', - u'\U0001F406': u':leopard:', - u'\U0001F39A': u':level_slider:', - u'\U0000264E': u':libra:', - u'\U0001F688': u':light_rail:', - u'\U0001F329': u':lightning:', - u'\U0001F517': u':link:', - u'\U0001F517': u':link_symbol:', - u'\U0001F587': u':linked_paperclips:', - u'\U0001F981': u':lion_face:', - u'\U0001F444': u':lips:', - u'\U0001F484': u':lipstick:', - u'\U0001F512': u':lock:', - u'\U0001F50F': u':lock_with_ink_pen:', - u'\U0001F36D': u':lollipop:', - u'\U000027BF': u':loop:', - u'\U0001F50A': u':loud_sound:', - u'\U0001F62D': u':loudly_crying_face:', - u'\U0001F4E2': u':loudspeaker:', - u'\U0001F3E9': u':love_hotel:', - u'\U0001F48C': u':love_letter:', - u'\U0001F505': u':low_brightness:', - u'\U0001F505': u':low_brightness_symbol:', - u'\U0001F58A': u':lower_left_ballpoint_pen:', - u'\U0001F58D': u':lower_left_crayon:', - u'\U0001F58B': u':lower_left_fountain_pen:', - u'\U0001F58C': u':lower_left_paintbrush:', - u'\U000024C2': u':m:', - u'\U0001F50D': u':mag:', - u'\U0001F50E': u':mag_right:', - u'\U0001F004': u':mahjong:', - u'\U0001F004': u':mahjong_tile_red_dragon:', - u'\U0001F4EB': u':mailbox:', - u'\U0001F4EA': u':mailbox_closed:', - u'\U0001F4EC': u':mailbox_with_mail:', - u'\U0001F4ED': u':mailbox_with_no_mail:', - u'\U0001F468': u':man:', - u'\U0001F46B': u':man_and_woman_holding_hands:', - u'\U0001F574': u':man_in_business_suit_levitating:', - u'\U0001F472': u':man_with_gua_pi_mao:', - u'\U0001F473': u':man_with_turban:', - u'\U0001F45E': u':mans_shoe:', - u'\U0001F570': u':mantelpiece_clock:', - u'\U0001F341': u':maple_leaf:', - u'\U0001F637': u':mask:', - u'\U0001F486': u':massage:', - u'\U0001F356': u':meat_on_bone:', - u'\U0001F3C5': u':medal:', - u'\U000026AB': u':medium_black_circle:', - u'\U000026AA': u':medium_white_circle:', - u'\U0001F4E3': u':mega:', - u'\U0001F348': u':melon:', - u'\U0001F4DD': u':memo:', - u'\U0001F54E': u':menorah_with_nine_branches:', - u'\U0001F6B9': u':mens:', - u'\U0001F6B9': u':mens_symbol:', - u'\U0001F687': u':metro:', - u'\U0001F3A4': u':microphone:', - u'\U0001F52C': u':microscope:', - u'\U0001F595': u':middle_finger:', - u'\U0001F396': u':military_medal:', - u'\U0001F30C': u':milky_way:', - u'\U0001F690': u':minibus:', - u'\U0001F4BD': u':minidisc:', - u'\U0001F4F1': u':mobile_phone:', - u'\U0001F4F4': u':mobile_phone_off:', - u'\U0001F911': u':money-mouth_face:', - u'\U0001F4B0': u':money_bag:', - u'\U0001F911': u':money_mouth_face:', - u'\U0001F4B8': u':money_with_wings:', - u'\U0001F4B0': u':moneybag:', - u'\U0001F412': u':monkey:', - u'\U0001F435': u':monkey_face:', - u'\U0001F69D': u':monorail:', - u'\U0001F314': u':moon:', - u'\U0001F391': u':moon_viewing_ceremony:', - u'\U0001F393': u':mortar_board:', - u'\U0001F54C': u':mosque:', - u'\U0001F324': u':mostly_sunny:', - u'\U0001F6E5': u':motor_boat:', - u'\U0001F6E3': u':motorway:', - u'\U0001F5FB': u':mount_fuji:', - u'\U000026F0': u':mountain:', - u'\U0001F6B5': u':mountain_bicyclist:', - u'\U0001F6A0': u':mountain_cableway:', - u'\U0001F69E': u':mountain_railway:', - u'\U0001F401': u':mouse2:', - u'\U0001F401': u':mouse:', - u'\U0001F42D': u':mouse:', - u'\U0001F42D': u':mouse_face:', - u'\U0001F444': u':mouth:', - u'\U0001F3A5': u':movie_camera:', - u'\U0001F5FF': u':moyai:', - u'\U0001F3B6': u':multiple_musical_notes:', - u'\U0001F4AA': u':muscle:', - u'\U0001F344': u':mushroom:', - u'\U0001F3B9': u':musical_keyboard:', - u'\U0001F3B5': u':musical_note:', - u'\U0001F3BC': u':musical_score:', - u'\U0001F507': u':mute:', - u'\U0001F485': u':nail_care:', - u'\U0001F485': u':nail_polish:', - u'\U0001F4DB': u':name_badge:', - u'\U0001F3DE': u':national_park:', - u'\U0001F454': u':necktie:', - u'\U0001F18E': u':negative_squared_ab:', - u'\U0000274E': u':negative_squared_cross_mark:', - u'\U0001F913': u':nerd_face:', - u'\U0001F610': u':neutral_face:', - u'\U0001F195': u':new:', - u'\U0001F311': u':new_moon:', - u'\U0001F311': u':new_moon_symbol:', - u'\U0001F31A': u':new_moon_with_face:', - u'\U0001F4F0': u':newspaper:', - u'\U0001F196': u':ng:', - u'\U0001F303': u':night_with_stars:', - u'\U0001F515': u':no_bell:', - u'\U0001F6B3': u':no_bicycles:', - u'\U000026D4': u':no_entry:', - u'\U0001F6AB': u':no_entry_sign:', - u'\U0001F645': u':no_good:', - u'\U0001F4F5': u':no_mobile_phones:', - u'\U0001F636': u':no_mouth:', - u'\U0001F51E': u':no_one_under_eighteen_symbol:', - u'\U0001F6B7': u':no_pedestrians:', - u'\U0001F6AD': u':no_smoking:', - u'\U0001F6AD': u':no_smoking_symbol:', - u'\U0001F6B1': u':non-potable_water:', - u'\U0001F6B1': u':non-potable_water_symbol:', - u'\U00002197': u':north_east_arrow:', - u'\U00002196': u':north_west_arrow:', - u'\U0001F443': u':nose:', - u'\U0001F4D3': u':notebook:', - u'\U0001F4D4': u':notebook_with_decorative_cover:', - u'\U0001F3B6': u':notes:', - u'\U0001F529': u':nut_and_bolt:', - u'\U0001F17E': u':o2:', - u'\U00002B55': u':o:', - u'\U0001F30A': u':ocean:', - u'\U0001F419': u':octopus:', - u'\U0001F362': u':oden:', - u'\U0001F3E2': u':office:', - u'\U0001F3E2': u':office_building:', - u'\U0001F6E2': u':oil_drum:', - u'\U0001F197': u':ok:', - u'\U0001F44C': u':ok_hand:', - u'\U0001F44C': u':ok_hand_sign:', - u'\U0001F646': u':ok_woman:', - u'\U0001F5DD': u':old_key:', - u'\U0001F474': u':older_man:', - u'\U0001F475': u':older_woman:', - u'\U0001F549': u':om_symbol:', - u'\U0001F51B': u':on:', - u'\U0001F698': u':oncoming_automobile:', - u'\U0001F68D': u':oncoming_bus:', - u'\U0001F694': u':oncoming_police_car:', - u'\U0001F696': u':oncoming_taxi:', - u'\U0001F4D6': u':open_book:', - u'\U0001F4C2': u':open_file_folder:', - u'\U0001F450': u':open_hands:', - u'\U0001F450': u':open_hands_sign:', - u'\U0001F513': u':open_lock:', - u'\U0001F4ED': u':open_mailbox_with_lowered_flag:', - u'\U0001F4EC': u':open_mailbox_with_raised_flag:', - u'\U0001F62E': u':open_mouth:', - u'\U000026CE': u':ophiuchus:', - u'\U0001F4BF': u':optical_disc:', - u'\U0001F4D9': u':orange_book:', + u'\U0001f4ac': u':left_speech_bubble:', + u'\U000021a9': u':leftwards_arrow_with_hook:', + u'\U0001f34b': u':lemon:', + u'\U0000264c': u':leo:', + u'\U0001f406': u':leopard:', + u'\U0001f39a': u':level_slider:', + u'\U0000264e': u':libra:', + u'\U0001f688': u':light_rail:', + u'\U0001f517': u':link:', + u'\U0001f587': u':linked_paperclips:', + u'\U0001f981': u':lion_face:', + u'\U0001f444': u':lips:', + u'\U0001f484': u':lipstick:', + u'\U0001f512': u':lock:', + u'\U0001f50f': u':lock_with_ink_pen:', + u'\U0001f36d': u':lollipop:', + u'\U0001f50a': u':loud_sound:', + u'\U0001f62d': u':loudly_crying_face:', + u'\U0001f4e2': u':loudspeaker:', + u'\U0001f3e9': u':love_hotel:', + u'\U0001f48c': u':love_letter:', + u'\U0001f505': u':low_brightness:', + u'\U0001f58a': u':lower_left_ballpoint_pen:', + u'\U0001f58d': u':lower_left_crayon:', + u'\U0001f58b': u':lower_left_fountain_pen:', + u'\U0001f58c': u':lower_left_paintbrush:', + u'\U0001f50e': u':mag_right:', + u'\U0001f004': u':mahjong:', + u'\U0001f4ec': u':mailbox_with_mail:', + u'\U0001f4ed': u':mailbox_with_no_mail:', + u'\U0001f468': u':man:', + u'\U0001f574': u':man_in_business_suit_levitating:', + u'\U0001f472': u':man_with_gua_pi_mao:', + u'\U0001f473': u':man_with_turban:', + u'\U0001f45e': u':mans_shoe:', + u'\U0001f570': u':mantelpiece_clock:', + u'\U0001f341': u':maple_leaf:', + u'\U0001f356': u':meat_on_bone:', + u'\U0001f3c5': u':medal:', + u'\U000026aa': u':medium_white_circle:', + u'\U0001f348': u':melon:', + u'\U0001f4dd': u':memo:', + u'\U0001f54e': u':menorah_with_nine_branches:', + u'\U0001f6b9': u':mens:', + u'\U0001f687': u':metro:', + u'\U0001f3a4': u':microphone:', + u'\U0001f52c': u':microscope:', + u'\U0001f595': u':middle_finger:', + u'\U0001f396': u':military_medal:', + u'\U0001f30c': u':milky_way:', + u'\U0001f690': u':minibus:', + u'\U0001f4bd': u':minidisc:', + u'\U0001f4f4': u':mobile_phone_off:', + u'\U0001f911': u':money-mouth_face:', + u'\U0001f4b0': u':money_bag:', + u'\U0001f4b8': u':money_with_wings:', + u'\U0001f412': u':monkey:', + u'\U0001f435': u':monkey_face:', + u'\U0001f69d': u':monorail:', + u'\U0001f314': u':moon:', + u'\U0001f391': u':moon_viewing_ceremony:', + u'\U0001f54c': u':mosque:', + u'\U0001f324': u':mostly_sunny:', + u'\U0001f6e5': u':motor_boat:', + u'\U0001f6e3': u':motorway:', + u'\U0001f5fb': u':mount_fuji:', + u'\U000026f0': u':mountain:', + u'\U0001f6b5': u':mountain_bicyclist:', + u'\U0001f6a0': u':mountain_cableway:', + u'\U0001f69e': u':mountain_railway:', + u'\U0001f401': u':mouse2:', + u'\U0001f42d': u':mouse:', + u'\U0001f3a5': u':movie_camera:', + u'\U0001f5ff': u':moyai:', + u'\U0001f3b6': u':multiple_musical_notes:', + u'\U0001f344': u':mushroom:', + u'\U0001f3b9': u':musical_keyboard:', + u'\U0001f3b5': u':musical_note:', + u'\U0001f3bc': u':musical_score:', + u'\U0001f507': u':mute:', + u'\U0001f485': u':nail_care:', + u'\U0001f4db': u':name_badge:', + u'\U0001f3de': u':national_park:', + u'\U0001f454': u':necktie:', + u'\U0000274e': u':negative_squared_cross_mark:', + u'\U0001f913': u':nerd_face:', + u'\U0001f610': u':neutral_face:', + u'\U0001f195': u':new:', + u'\U0001f311': u':new_moon:', + u'\U0001f31a': u':new_moon_with_face:', + u'\U0001f4f0': u':newspaper:', + u'\U0001f196': u':ng:', + u'\U0001f303': u':night_with_stars:', + u'\U0001f6b3': u':no_bicycles:', + u'\U000026d4': u':no_entry:', + u'\U0001f6ab': u':no_entry_sign:', + u'\U0001f4f5': u':no_mobile_phones:', + u'\U0001f51e': u':no_one_under_eighteen_symbol:', + u'\U0001f6b7': u':no_pedestrians:', + u'\U0001f6ad': u':no_smoking:', + u'\U0001f6b1': u':non-potable_water:', + u'\U0001f443': u':nose:', + u'\U0001f4d3': u':notebook:', + u'\U0001f4d4': u':notebook_with_decorative_cover:', + u'\U0001f529': u':nut_and_bolt:', + u'\U0001f17e': u':o2:', + u'\U0001f30a': u':ocean:', + u'\U0001f419': u':octopus:', + u'\U0001f362': u':oden:', + u'\U0001f3e2': u':office:', + u'\U0001f6e2': u':oil_drum:', + u'\U0001f197': u':ok:', + u'\U0001f44c': u':ok_hand:', + u'\U0001f5dd': u':old_key:', + u'\U0001f474': u':older_man:', + u'\U0001f475': u':older_woman:', + u'\U0001f549': u':om_symbol:', + u'\U0001f51b': u':on:', + u'\U0001f698': u':oncoming_automobile:', + u'\U0001f68d': u':oncoming_bus:', + u'\U0001f694': u':oncoming_police_car:', + u'\U0001f696': u':oncoming_taxi:', + u'\U0001f4c2': u':open_file_folder:', + u'\U0001f450': u':open_hands:', + u'\U0001f513': u':open_lock:', + u'\U000026ce': u':ophiuchus:', + u'\U0001f4d9': u':orange_book:', u'\U00002626': u':orthodox_cross:', - u'\U0001F4E4': u':outbox_tray:', - u'\U0001F402': u':ox:', - u'\U0001F4E6': u':package:', - u'\U0001F4C4': u':page_facing_up:', - u'\U0001F4C3': u':page_with_curl:', - u'\U0001F4DF': u':pager:', - u'\U0001F334': u':palm_tree:', - u'\U0001F43C': u':panda_face:', - u'\U0001F4CE': u':paperclip:', - u'\U0001F17F': u':parking:', - u'\U0000303D': u':part_alternation_mark:', - u'\U000026C5': u':partly_sunny:', - u'\U0001F326': u':partly_sunny_rain:', - u'\U0001F389': u':party_popper:', - u'\U0001F6F3': u':passenger_ship:', - u'\U0001F6C2': u':passport_control:', - u'\U0001F43E': u':paw_prints:', - u'\U0000262E': u':peace_symbol:', - u'\U0001F351': u':peach:', - u'\U0001F350': u':pear:', - u'\U0001F6B6': u':pedestrian:', - u'\U0000270F': u':pencil2:', - u'\U0000270F': u':pencil:', - u'\U0001F4DD': u':pencil:', - u'\U0001F427': u':penguin:', - u'\U0001F614': u':pensive:', - u'\U0001F614': u':pensive_face:', - u'\U0001F3AD': u':performing_arts:', - u'\U0001F623': u':persevere:', - u'\U0001F623': u':persevering_face:', - u'\U0001F647': u':person_bowing_deeply:', - u'\U0001F64D': u':person_frowning:', - u'\U000026F9': u':person_with_ball:', - u'\U0001F471': u':person_with_blond_hair:', - u'\U0001F64F': u':person_with_folded_hands:', - u'\U0001F64E': u':person_with_pouting_face:', - u'\U0001F4BB': u':personal_computer:', - u'\U0000260E': u':phone:', - u'\U000026CF': u':pick:', - u'\U0001F416': u':pig2:', - u'\U0001F416': u':pig:', - u'\U0001F437': u':pig:', - u'\U0001F437': u':pig_face:', - u'\U0001F43D': u':pig_nose:', - u'\U0001F4A9': u':pile_of_poo:', - u'\U0001F48A': u':pill:', - u'\U0001F38D': u':pine_decoration:', - u'\U0001F34D': u':pineapple:', + u'\U0001f4e4': u':outbox_tray:', + u'\U0001f402': u':ox:', + u'\U0001f4e6': u':package:', + u'\U0001f4c4': u':page_facing_up:', + u'\U0001f4c3': u':page_with_curl:', + u'\U0001f4df': u':pager:', + u'\U0001f334': u':palm_tree:', + u'\U0001f43c': u':panda_face:', + u'\U0001f4ce': u':paperclip:', + u'\U0001f17f': u':parking:', + u'\U0000303d': u':part_alternation_mark:', + u'\U000026c5': u':partly_sunny:', + u'\U0001f326': u':partly_sunny_rain:', + u'\U0001f389': u':party_popper:', + u'\U0001f6f3': u':passenger_ship:', + u'\U0001f6c2': u':passport_control:', + u'\U0000262e': u':peace_symbol:', + u'\U0001f351': u':peach:', + u'\U0001f350': u':pear:', + u'\U0001f6b6': u':pedestrian:', + u'\U0000270f': u':pencil2:', + u'\U0001f427': u':penguin:', + u'\U0001f614': u':pensive:', + u'\U0001f3ad': u':performing_arts:', + u'\U0001f623': u':persevere:', + u'\U0001f64d': u':person_frowning:', + u'\U000026f9': u':person_with_ball:', + u'\U0001f471': u':person_with_blond_hair:', + u'\U0001f64f': u':person_with_folded_hands:', + u'\U0001f64e': u':person_with_pouting_face:', + u'\U000026cf': u':pick:', + u'\U0001f416': u':pig2:', + u'\U0001f437': u':pig:', + u'\U0001f43d': u':pig_nose:', + u'\U0001f48a': u':pill:', + u'\U0001f34d': u':pineapple:', u'\U00002653': u':pisces:', - u'\U0001F52B': u':pistol:', - u'\U0001F355': u':pizza:', - u'\U0001F6D0': u':place_of_worship:', - u'\U0001F0CF': u':playing_card_black_joker:', - u'\U0001F447': u':point_down:', - u'\U0001F448': u':point_left:', - u'\U0001F449': u':point_right:', - u'\U0000261D': u':point_up:', - u'\U0001F446': u':point_up_2:', - u'\U0001F693': u':police_car:', - u'\U0001F6A8': u':police_cars_revolving_light:', - u'\U0001F46E': u':police_officer:', - u'\U0001F429': u':poodle:', - u'\U0001F4A9': u':poop:', - u'\U0001F37F': u':popcorn:', - u'\U0001F3E3': u':post_office:', - u'\U0001F4EF': u':postal_horn:', - u'\U0001F4EE': u':postbox:', - u'\U0001F372': u':pot_of_food:', - u'\U0001F6B0': u':potable_water:', - u'\U0001F6B0': u':potable_water_symbol:', - u'\U0001F45D': u':pouch:', - u'\U0001F357': u':poultry_leg:', - u'\U0001F4B7': u':pound:', - u'\U0001F63E': u':pouting_cat:', - u'\U0001F63E': u':pouting_cat_face:', - u'\U0001F621': u':pouting_face:', - u'\U0001F64F': u':pray:', - u'\U0001F4FF': u':prayer_beads:', - u'\U0001F478': u':princess:', - u'\U0001F5A8': u':printer:', - u'\U0001F4E2': u':public_address_loudspeaker:', - u'\U0001F44A': u':punch:', - u'\U0001F49C': u':purple_heart:', - u'\U0001F45B': u':purse:', - u'\U0001F4CC': u':pushpin:', - u'\U0001F6AE': u':put_litter_in_its_place:', - u'\U0001F6AE': u':put_litter_in_its_place_symbol:', - u'\U00002753': u':question:', - u'\U0001F407': u':rabbit2:', - u'\U0001F407': u':rabbit:', - u'\U0001F430': u':rabbit:', - u'\U0001F430': u':rabbit_face:', - u'\U0001F40E': u':racehorse:', - u'\U0001F3CE': u':racing_car:', - u'\U0001F3CD': u':racing_motorcycle:', - u'\U0001F4FB': u':radio:', - u'\U0001F518': u':radio_button:', + u'\U0001f355': u':pizza:', + u'\U0001f6d0': u':place_of_worship:', + u'\U0001f447': u':point_down:', + u'\U0001f448': u':point_left:', + u'\U0001f449': u':point_right:', + u'\U0000261d': u':point_up:', + u'\U0001f446': u':point_up_2:', + u'\U0001f693': u':police_car:', + u'\U0001f6a8': u':police_cars_revolving_light:', + u'\U0001f429': u':poodle:', + u'\U0001f37f': u':popcorn:', + u'\U0001f4ef': u':postal_horn:', + u'\U0001f4ee': u':postbox:', + u'\U0001f372': u':pot_of_food:', + u'\U0001f6b0': u':potable_water:', + u'\U0001f45d': u':pouch:', + u'\U0001f357': u':poultry_leg:', + u'\U0001f63e': u':pouting_cat:', + u'\U0001f621': u':pouting_face:', + u'\U0001f4ff': u':prayer_beads:', + u'\U0001f478': u':princess:', + u'\U0001f5a8': u':printer:', + u'\U0001f49c': u':purple_heart:', + u'\U0001f45b': u':purse:', + u'\U0001f4cc': u':pushpin:', + u'\U0001f6ae': u':put_litter_in_its_place:', + u'\U0001f407': u':rabbit2:', + u'\U0001f430': u':rabbit:', + u'\U0001f3ce': u':racing_car:', + u'\U0001f3cd': u':racing_motorcycle:', + u'\U0001f4fb': u':radio:', + u'\U0001f518': u':radio_button:', u'\U00002622': u':radioactive_sign:', - u'\U0001F621': u':rage:', - u'\U0001F683': u':railway_car:', - u'\U0001F6E4': u':railway_track:', - u'\U0001F327': u':rain_cloud:', - u'\U0001F308': u':rainbow:', - u'\U0000270A': u':raised_fist:', - u'\U0000270B': u':raised_hand:', - u'\U0001F590': u':raised_hand_with_fingers_splayed:', - u'\U0001F64C': u':raised_hands:', - u'\U0001F64B': u':raising_hand:', - u'\U0001F40F': u':ram:', - u'\U0001F35C': u':ramen:', - u'\U0001F400': u':rat:', - u'\U0001F699': u':recreational_vehicle:', - u'\U0000267B': u':recycle:', - u'\U0001F34E': u':red_apple:', - u'\U0001F697': u':red_car:', - u'\U0001F534': u':red_circle:', - u'\U000000AE': u':registered:', - u'\U000000AE': u':registered_sign:', - u'\U0000263A': u':relaxed:', - u'\U0001F60C': u':relieved:', - u'\U0001F60C': u':relieved_face:', - u'\U0001F397': u':reminder_ribbon:', - u'\U0001F501': u':repeat:', - u'\U0001F502': u':repeat_one:', - u'\U0001F6BB': u':restroom:', - u'\U0001F49E': u':revolving_hearts:', - u'\U000023EA': u':rewind:', - u'\U0001F380': u':ribbon:', - u'\U0001F35A': u':rice:', - u'\U0001F359': u':rice_ball:', - u'\U0001F358': u':rice_cracker:', - u'\U0001F391': u':rice_scene:', - u'\U0001F50E': u':right-pointing_magnifying_glass:', - u'\U0001F5EF': u':right_anger_bubble:', - u'\U000021AA': u':rightwards_arrow_with_hook:', - u'\U0001F48D': u':ring:', - u'\U0001F360': u':roasted_sweet_potato:', - u'\U0001F916': u':robot_face:', - u'\U0001F680': u':rocket:', - u'\U0001F5DE': u':rolled-up_newspaper:', - u'\U0001F5DE': u':rolled_up_newspaper:', - u'\U0001F3A2': u':roller_coaster:', - u'\U0001F413': u':rooster:', - u'\U0001F339': u':rose:', - u'\U0001F3F5': u':rosette:', - u'\U0001F6A8': u':rotating_light:', - u'\U0001F4CD': u':round_pushpin:', - u'\U0001F6A3': u':rowboat:', - u'\U0001F3C9': u':rugby_football:', - u'\U0001F3C3': u':runner:', - u'\U0001F3C3': u':running:', - u'\U0001F3BD': u':running_shirt_with_sash:', - u'\U0001F202': u':sa:', + u'\U0001f683': u':railway_car:', + u'\U0001f6e4': u':railway_track:', + u'\U0001f308': u':rainbow:', + u'\U0001f590': u':raised_hand_with_fingers_splayed:', + u'\U0001f64c': u':raised_hands:', + u'\U0001f40f': u':ram:', + u'\U0001f35c': u':ramen:', + u'\U0001f400': u':rat:', + u'\U000000ae': u':registered:', + u'\U0000263a': u':relaxed:', + u'\U0001f60c': u':relieved:', + u'\U0001f397': u':reminder_ribbon:', + u'\U0001f501': u':repeat:', + u'\U0001f502': u':repeat_one:', + u'\U0001f6bb': u':restroom:', + u'\U0001f49e': u':revolving_hearts:', + u'\U0001f380': u':ribbon:', + u'\U0001f359': u':rice_ball:', + u'\U0001f358': u':rice_cracker:', + u'\U0001f5ef': u':right_anger_bubble:', + u'\U0001f48d': u':ring:', + u'\U0001f360': u':roasted_sweet_potato:', + u'\U0001f916': u':robot_face:', + u'\U0001f680': u':rocket:', + u'\U0001f5de': u':rolled-up_newspaper:', + u'\U0001f3a2': u':roller_coaster:', + u'\U0001f413': u':rooster:', + u'\U0001f339': u':rose:', + u'\U0001f3f5': u':rosette:', + u'\U0001f4cd': u':round_pushpin:', + u'\U0001f6a3': u':rowboat:', + u'\U0001f3c9': u':rugby_football:', + u'\U0001f3c3': u':runner:', + u'\U0001f3bd': u':running_shirt_with_sash:', + u'\U0001f202': u':sa:', u'\U00002650': u':sagittarius:', - u'\U000026F5': u':sailboat:', - u'\U0001F376': u':sake:', - u'\U0001F376': u':sake_bottle_and_cup:', - u'\U0001F461': u':sandal:', - u'\U0001F385': u':santa:', - u'\U0001F4E1': u':satellite:', - u'\U0001F6F0': u':satellite:', - u'\U0001F4E1': u':satellite_antenna:', - u'\U0001F606': u':satisfied:', - u'\U0001F3B7': u':saxophone:', + u'\U0001f376': u':sake:', + u'\U0001f461': u':sandal:', + u'\U0001f4e1': u':satellite:', + u'\U0001f6f0': u':satellite:', + u'\U0001f3b7': u':saxophone:', u'\U00002696': u':scales:', - u'\U0001F3EB': u':school:', - u'\U0001F392': u':school_satchel:', - u'\U00002702': u':scissors:', - u'\U0001F982': u':scorpion:', - u'\U0000264F': u':scorpius:', - u'\U0001F631': u':scream:', - u'\U0001F640': u':scream_cat:', - u'\U0001F4DC': u':scroll:', - u'\U0001F4BA': u':seat:', - u'\U00003299': u':secret:', - u'\U0001F648': u':see-no-evil_monkey:', - u'\U0001F648': u':see_no_evil:', - u'\U0001F331': u':seedling:', + u'\U0001f3eb': u':school:', + u'\U0001f392': u':school_satchel:', + u'\U0001f982': u':scorpion:', + u'\U0000264f': u':scorpius:', + u'\U0001f640': u':scream_cat:', + u'\U0001f4dc': u':scroll:', + u'\U0001f4ba': u':seat:', + u'\U0001f648': u':see-no-evil_monkey:', + u'\U0001f331': u':seedling:', u'\U00002618': u':shamrock:', - u'\U0001F367': u':shaved_ice:', - u'\U0001F411': u':sheep:', - u'\U0001F41A': u':shell:', - u'\U0001F6E1': u':shield:', - u'\U000026E9': u':shinto_shrine:', - u'\U0001F6A2': u':ship:', - u'\U0001F455': u':shirt:', - u'\U0001F4A9': u':shit:', - u'\U0001F45E': u':shoe:', - u'\U0001F320': u':shooting_star:', - u'\U0001F6CD': u':shopping_bags:', - u'\U0001F370': u':shortcake:', - u'\U0001F6BF': u':shower:', - u'\U0001F918': u':sign_of_the_horns:', - u'\U0001F4F6': u':signal_strength:', - u'\U0001F5FE': u':silhouette_of_japan:', - u'\U0001F642': u':simple_smile:', - u'\U0001F52F': u':six_pointed_star:', - u'\U0001F3BF': u':ski:', - u'\U0001F3BF': u':ski_and_ski_boot:', - u'\U000026F7': u':skier:', - u'\U0001F480': u':skull:', + u'\U0001f367': u':shaved_ice:', + u'\U0001f411': u':sheep:', + u'\U0001f41a': u':shell:', + u'\U0001f6e1': u':shield:', + u'\U000026e9': u':shinto_shrine:', + u'\U0001f6a2': u':ship:', + u'\U0001f455': u':shirt:', + u'\U0001f320': u':shooting_star:', + u'\U0001f6cd': u':shopping_bags:', + u'\U0001f6bf': u':shower:', + u'\U0001f918': u':sign_of_the_horns:', + u'\U0001f642': u':simple_smile:', + u'\U0001f52f': u':six_pointed_star:', + u'\U0001f3bf': u':ski:', + u'\U000026f7': u':skier:', + u'\U0001f480': u':skull:', u'\U00002620': u':skull_and_crossbones:', - u'\U0001F634': u':sleeping:', - u'\U0001F6CC': u':sleeping_accommodation:', - u'\U0001F634': u':sleeping_face:', - u'\U0001F4A4': u':sleeping_symbol:', - u'\U0001F62A': u':sleepy:', - u'\U0001F62A': u':sleepy_face:', - u'\U0001F575': u':sleuth_or_spy:', - u'\U0001F355': u':slice_of_pizza:', - u'\U0001F641': u':slightly_frowning_face:', - u'\U0001F642': u':slightly_smiling_face:', - u'\U0001F3B0': u':slot_machine:', - u'\U0001F6E9': u':small_airplane:', - u'\U0001F539': u':small_blue_diamond:', - u'\U0001F538': u':small_orange_diamond:', - u'\U0001F53A': u':small_red_triangle:', - u'\U0001F53B': u':small_red_triangle_down:', - u'\U0001F604': u':smile:', - u'\U0001F638': u':smile_cat:', - u'\U0001F603': u':smiley:', - u'\U0001F63A': u':smiley_cat:', - u'\U0001F63B': u':smiling_cat_face_with_heart-shaped_eyes:', - u'\U0001F63A': u':smiling_cat_face_with_open_mouth:', - u'\U0001F607': u':smiling_face_with_halo:', - u'\U0001F60D': u':smiling_face_with_heart-shaped_eyes:', - u'\U0001F608': u':smiling_face_with_horns:', - u'\U0001F603': u':smiling_face_with_open_mouth:', - u'\U0001F605': u':smiling_face_with_open_mouth_and_cold_sweat:', - u'\U0001F604': u':smiling_face_with_open_mouth_and_smiling_eyes:', - u'\U0001F606': u':smiling_face_with_open_mouth_and_tightly-closed_eyes:', - u'\U0001F60A': u':smiling_face_with_smiling_eyes:', - u'\U0001F60E': u':smiling_face_with_sunglasses:', - u'\U0001F608': u':smiling_imp:', - u'\U0001F60F': u':smirk:', - u'\U0001F63C': u':smirk_cat:', - u'\U0001F60F': u':smirking_face:', - u'\U0001F6AC': u':smoking:', - u'\U0001F6AC': u':smoking_symbol:', - u'\U0001F40C': u':snail:', - u'\U0001F40D': u':snake:', - u'\U0001F3D4': u':snow_capped_mountain:', - u'\U0001F328': u':snow_cloud:', - u'\U0001F3C2': u':snowboarder:', + u'\U0001f634': u':sleeping:', + u'\U0001f6cc': u':sleeping_accommodation:', + u'\U0001f4a4': u':sleeping_symbol:', + u'\U0001f62a': u':sleepy:', + u'\U0001f575': u':sleuth_or_spy:', + u'\U0001f641': u':slightly_frowning_face:', + u'\U0001f3b0': u':slot_machine:', + u'\U0001f6e9': u':small_airplane:', + u'\U0001f539': u':small_blue_diamond:', + u'\U0001f538': u':small_orange_diamond:', + u'\U0001f53a': u':small_red_triangle:', + u'\U0001f604': u':smile:', + u'\U0001f603': u':smiley:', + u'\U0001f63a': u':smiley_cat:', + u'\U0001f608': u':smiling_face_with_horns:', + u'\U0001f605': u':smiling_face_with_open_mouth_and_cold_sweat:', + u'\U0001f60e': u':smiling_face_with_sunglasses:', + u'\U0001f60f': u':smirk:', + u'\U0001f6ac': u':smoking:', + u'\U0001f40c': u':snail:', + u'\U0001f40d': u':snake:', + u'\U0001f3d4': u':snow_capped_mountain:', + u'\U0001f3c2': u':snowboarder:', u'\U00002744': u':snowflake:', u'\U00002603': u':snowman:', - u'\U000026C4': u':snowman_without_snow:', - u'\U0001F62D': u':sob:', - u'\U000026BD': u':soccer:', - u'\U000026BD': u':soccer_ball:', - u'\U0001F366': u':soft_ice_cream:', - u'\U0001F51C': u':soon:', - u'\U0001F51C': u':soon_with_rightwards_arrow_above:', - u'\U0001F198': u':sos:', - u'\U0001F509': u':sound:', - u'\U00002198': u':south_east_arrow:', - u'\U00002199': u':south_west_arrow:', - u'\U0001F47E': u':space_invader:', - u'\U00002660': u':spades:', - u'\U0001F35D': u':spaghetti:', + u'\U000026c4': u':snowman_without_snow:', + u'\U000026bd': u':soccer:', + u'\U0001f51c': u':soon:', + u'\U0001f198': u':sos:', + u'\U0001f509': u':sound:', + u'\U0001f35d': u':spaghetti:', u'\U00002747': u':sparkle:', - u'\U0001F387': u':sparkler:', u'\U00002728': u':sparkles:', - u'\U0001F496': u':sparkling_heart:', - u'\U0001F64A': u':speak-no-evil_monkey:', - u'\U0001F64A': u':speak_no_evil:', - u'\U0001F508': u':speaker:', - u'\U0001F507': u':speaker_with_cancellation_stroke:', - u'\U0001F509': u':speaker_with_one_sound_wave:', - u'\U0001F50A': u':speaker_with_three_sound_waves:', - u'\U0001F5E3': u':speaking_head_in_silhouette:', - u'\U0001F4AC': u':speech_balloon:', - u'\U0001F6A4': u':speedboat:', - u'\U0001F577': u':spider:', - u'\U0001F578': u':spider_web:', - u'\U0001F5D3': u':spiral_calendar_pad:', - u'\U0001F5D2': u':spiral_note_pad:', - u'\U0001F41A': u':spiral_shell:', - u'\U0001F4A6': u':splashing_sweat_symbol:', - u'\U0001F596': u':spock-hand:', - u'\U0001F596': u':spock_hand:', - u'\U0001F3C5': u':sports_medal:', - u'\U0001F433': u':spouting_whale:', - u'\U0001F191': u':squared_cl:', - u'\U0001F192': u':squared_cool:', - u'\U0001F193': u':squared_free:', - u'\U0001F194': u':squared_id:', - u'\U0001F201': u':squared_katakana_koko:', - u'\U0001F202': u':squared_katakana_sa:', - u'\U0001F195': u':squared_new:', - u'\U0001F196': u':squared_ng:', - u'\U0001F197': u':squared_ok:', - u'\U0001F198': u':squared_sos:', - u'\U0001F199': u':squared_up_with_exclamation_mark:', - u'\U0001F19A': u':squared_vs:', - u'\U0001F3DF': u':stadium:', - u'\U0001F31F': u':star2:', - u'\U00002B50': u':star:', - u'\U0000262A': u':star_and_crescent:', + u'\U0001f496': u':sparkling_heart:', + u'\U0001f64a': u':speak-no-evil_monkey:', + u'\U0001f508': u':speaker:', + u'\U0001f5e3': u':speaking_head_in_silhouette:', + u'\U0001f6a4': u':speedboat:', + u'\U0001f577': u':spider:', + u'\U0001f578': u':spider_web:', + u'\U0001f5d3': u':spiral_calendar_pad:', + u'\U0001f5d2': u':spiral_note_pad:', + u'\U0001f4a6': u':splashing_sweat_symbol:', + u'\U0001f596': u':spock-hand:', + u'\U0001f433': u':spouting_whale:', + u'\U0001f199': u':squared_up_with_exclamation_mark:', + u'\U0001f19a': u':squared_vs:', + u'\U0001f3df': u':stadium:', + u'\U00002b50': u':star:', + u'\U0000262a': u':star_and_crescent:', u'\U00002721': u':star_of_david:', - u'\U0001F320': u':stars:', - u'\U0001F689': u':station:', - u'\U0001F5FD': u':statue_of_liberty:', - u'\U0001F682': u':steam_locomotive:', - u'\U0001F35C': u':steaming_bowl:', - u'\U0001F372': u':stew:', - u'\U000023F1': u':stopwatch:', - u'\U0001F4CF': u':straight_ruler:', - u'\U0001F353': u':strawberry:', - u'\U0001F61B': u':stuck_out_tongue:', - u'\U0001F61D': u':stuck_out_tongue_closed_eyes:', - u'\U0001F61C': u':stuck_out_tongue_winking_eye:', - u'\U0001F399': u':studio_microphone:', - u'\U000026C5': u':sun_behind_cloud:', - u'\U0001F31E': u':sun_with_face:', - u'\U0001F33B': u':sunflower:', - u'\U0001F60E': u':sunglasses:', - u'\U00002600': u':sunny:', - u'\U0001F305': u':sunrise:', - u'\U0001F304': u':sunrise_over_mountains:', - u'\U0001F307': u':sunset_over_buildings:', - u'\U0001F3C4': u':surfer:', - u'\U0001F363': u':sushi:', - u'\U0001F69F': u':suspension_railway:', - u'\U0001F613': u':sweat:', - u'\U0001F4A6': u':sweat_drops:', - u'\U0001F605': u':sweat_smile:', - u'\U0001F360': u':sweet_potato:', - u'\U0001F3CA': u':swimmer:', - u'\U0001F523': u':symbols:', - u'\U0001F54D': u':synagogue:', - u'\U0001F489': u':syringe:', - u'\U0001F455': u':t-shirt:', - u'\U0001F3D3': u':table_tennis_paddle_and_ball:', - u'\U0001F32E': u':taco:', - u'\U0001F389': u':tada:', - u'\U0001F38B': u':tanabata_tree:', - u'\U0001F34A': u':tangerine:', + u'\U0001f689': u':station:', + u'\U0001f5fd': u':statue_of_liberty:', + u'\U0001f682': u':steam_locomotive:', + u'\U000023f1': u':stopwatch:', + u'\U0001f4cf': u':straight_ruler:', + u'\U0001f353': u':strawberry:', + u'\U0001f61d': u':stuck_out_tongue_closed_eyes:', + u'\U0001f61c': u':stuck_out_tongue_winking_eye:', + u'\U0001f399': u':studio_microphone:', + u'\U0001f31e': u':sun_with_face:', + u'\U0001f33b': u':sunflower:', + u'\U0001f305': u':sunrise:', + u'\U0001f304': u':sunrise_over_mountains:', + u'\U0001f3c4': u':surfer:', + u'\U0001f363': u':sushi:', + u'\U0001f69f': u':suspension_railway:', + u'\U0001f3ca': u':swimmer:', + u'\U0001f54d': u':synagogue:', + u'\U0001f489': u':syringe:', + u'\U0001f3d3': u':table_tennis_paddle_and_ball:', + u'\U0001f32e': u':taco:', + u'\U0001f38b': u':tanabata_tree:', + u'\U0001f34a': u':tangerine:', u'\U00002649': u':taurus:', - u'\U0001F695': u':taxi:', - u'\U0001F375': u':tea:', - u'\U0001F375': u':teacup_without_handle:', - u'\U0001F4C6': u':tear-off_calendar:', - u'\U0000260E': u':telephone:', - u'\U0001F4DE': u':telephone_receiver:', - u'\U0001F52D': u':telescope:', - u'\U0001F4FA': u':television:', - u'\U0001F51F': u':ten:', - u'\U0001F3BE': u':tennis:', - u'\U0001F3BE': u':tennis_racquet_and_ball:', - u'\U000026FA': u':tent:', - u'\U0001F918': u':the_horns:', - u'\U0001F321': u':thermometer:', - u'\U0001F914': u':thinking_face:', - u'\U0001F4AD': u':thought_balloon:', - u'\U0001F5B1': u':three_button_mouse:', - u'\U0001F44E': u':thumbs_down_sign:', - u'\U0001F44D': u':thumbs_up_sign:', - u'\U0001F44E': u':thumbsdown:', - u'\U0001F44D': u':thumbsup:', - u'\U000026C8': u':thunder_cloud_and_rain:', - u'\U0001F3AB': u':ticket:', - u'\U0001F405': u':tiger2:', - u'\U0001F405': u':tiger:', - u'\U0001F42F': u':tiger:', - u'\U0001F42F': u':tiger_face:', - u'\U000023F2': u':timer_clock:', - u'\U0001F62B': u':tired_face:', + u'\U0001f695': u':taxi:', + u'\U0001f375': u':tea:', + u'\U0001f4de': u':telephone_receiver:', + u'\U0001f52d': u':telescope:', + u'\U0001f4fa': u':television:', + u'\U0001f3be': u':tennis:', + u'\U000026fa': u':tent:', + u'\U0001f321': u':thermometer:', + u'\U0001f914': u':thinking_face:', + u'\U0001f4ad': u':thought_balloon:', + u'\U0001f5b1': u':three_button_mouse:', + u'\U000026c8': u':thunder_cloud_and_rain:', + u'\U0001f3ab': u':ticket:', + u'\U0001f405': u':tiger2:', + u'\U0001f42f': u':tiger:', + u'\U000023f2': u':timer_clock:', + u'\U0001f62b': u':tired_face:', u'\U00002122': u':tm:', - u'\U0001F6BD': u':toilet:', - u'\U0001F5FC': u':tokyo_tower:', - u'\U0001F345': u':tomato:', - u'\U0001F445': u':tongue:', - u'\U0001F51D': u':top:', - u'\U0001F3A9': u':top_hat:', - u'\U0001F51D': u':top_with_upwards_arrow_above:', - u'\U0001F3A9': u':tophat:', - u'\U0001F32A': u':tornado:', - u'\U0001F5B2': u':trackball:', - u'\U0001F69C': u':tractor:', - u'\U00002122': u':trade_mark_sign:', - u'\U0001F6A5': u':traffic_light:', - u'\U0001F686': u':train2:', - u'\U0001F686': u':train:', - u'\U0001F68B': u':train:', - u'\U0001F68A': u':tram:', - u'\U0001F68B': u':tram_car:', - u'\U0001F6A9': u':triangular_flag_on_post:', - u'\U0001F4D0': u':triangular_ruler:', - u'\U0001F531': u':trident:', - u'\U0001F531': u':trident_emblem:', - u'\U0001F624': u':triumph:', - u'\U0001F68E': u':trolleybus:', - u'\U0001F3C6': u':trophy:', - u'\U0001F379': u':tropical_drink:', - u'\U0001F420': u':tropical_fish:', - u'\U0001F69A': u':truck:', - u'\U0001F3BA': u':trumpet:', - u'\U0001F455': u':tshirt:', - u'\U0001F337': u':tulip:', - u'\U0001F983': u':turkey:', - u'\U0001F422': u':turtle:', - u'\U0001F4FA': u':tv:', - u'\U0001F500': u':twisted_rightwards_arrows:', - u'\U0001F495': u':two_hearts:', - u'\U0001F46C': u':two_men_holding_hands:', - u'\U0001F46D': u':two_women_holding_hands:', + u'\U0001f6bd': u':toilet:', + u'\U0001f5fc': u':tokyo_tower:', + u'\U0001f345': u':tomato:', + u'\U0001f445': u':tongue:', + u'\U0001f51d': u':top:', + u'\U0001f3a9': u':top_hat:', + u'\U0001f5b2': u':trackball:', + u'\U0001f69c': u':tractor:', + u'\U0001f686': u':train2:', + u'\U0001f68b': u':train:', + u'\U0001f68a': u':tram:', + u'\U0001f6a9': u':triangular_flag_on_post:', + u'\U0001f4d0': u':triangular_ruler:', + u'\U0001f531': u':trident:', + u'\U0001f68e': u':trolleybus:', + u'\U0001f3c6': u':trophy:', + u'\U0001f379': u':tropical_drink:', + u'\U0001f420': u':tropical_fish:', + u'\U0001f3ba': u':trumpet:', + u'\U0001f337': u':tulip:', + u'\U0001f983': u':turkey:', + u'\U0001f422': u':turtle:', + u'\U0001f500': u':twisted_rightwards_arrows:', + u'\U0001f495': u':two_hearts:', + u'\U0001f46c': u':two_men_holding_hands:', + u'\U0001f46d': u':two_women_holding_hands:', u'\U00002602': u':umbrella:', - u'\U000026F1': u':umbrella_on_ground:', + u'\U000026f1': u':umbrella_on_ground:', u'\U00002614': u':umbrella_with_rain_drops:', - u'\U0001F612': u':unamused:', - u'\U0001F612': u':unamused_face:', - u'\U0001F51E': u':underage:', - u'\U0001F984': u':unicorn_face:', - u'\U0001F513': u':unlock:', - u'\U0001F53A': u':up-pointing_red_triangle:', - u'\U0001F53C': u':up-pointing_small_red_triangle:', - u'\U0001F199': u':up:', - u'\U00002195': u':up_down_arrow:', - u'\U0001F643': u':upside-down_face:', - u'\U0001F643': u':upside_down_face:', - u'\U00002B06': u':upwards_black_arrow:', - u'\U0000270C': u':v:', - u'\U0001F6A6': u':vertical_traffic_light:', - u'\U0001F4FC': u':vhs:', - u'\U0001F4F3': u':vibration_mode:', - u'\U0000270C': u':victory_hand:', - u'\U0001F4F9': u':video_camera:', - u'\U0001F3AE': u':video_game:', - u'\U0001F4FC': u':videocassette:', - u'\U0001F3BB': u':violin:', - u'\U0000264D': u':virgo:', - u'\U0001F30B': u':volcano:', - u'\U0001F3D0': u':volleyball:', - u'\U0001F19A': u':vs:', - u'\U0001F6B6': u':walking:', - u'\U0001F318': u':waning_crescent_moon:', - u'\U0001F318': u':waning_crescent_moon_symbol:', - u'\U0001F316': u':waning_gibbous_moon:', - u'\U0001F316': u':waning_gibbous_moon_symbol:', - u'\U000026A0': u':warning:', - u'\U000026A0': u':warning_sign:', - u'\U0001F5D1': u':wastebasket:', - u'\U0000231A': u':watch:', - u'\U0001F403': u':water_buffalo:', - u'\U0001F6BE': u':water_closet:', - u'\U0001F30A': u':water_wave:', - u'\U0001F349': u':watermelon:', - u'\U0001F44B': u':wave:', - u'\U0001F3F4': u':waving_black_flag:', - u'\U0001F44B': u':waving_hand_sign:', - u'\U0001F3F3': u':waving_white_flag:', + u'\U0001f612': u':unamused:', + u'\U0001f984': u':unicorn_face:', + u'\U0001f643': u':upside-down_face:', + u'\U0000270c': u':v:', + u'\U0001f6a6': u':vertical_traffic_light:', + u'\U0001f4fc': u':vhs:', + u'\U0001f4f3': u':vibration_mode:', + u'\U0001f4f9': u':video_camera:', + u'\U0001f3ae': u':video_game:', + u'\U0001f3bb': u':violin:', + u'\U0000264d': u':virgo:', + u'\U0001f30b': u':volcano:', + u'\U0001f3d0': u':volleyball:', + u'\U0001f318': u':waning_crescent_moon:', + u'\U0001f316': u':waning_gibbous_moon:', + u'\U000026a0': u':warning:', + u'\U0001f5d1': u':wastebasket:', + u'\U0000231a': u':watch:', + u'\U0001f403': u':water_buffalo:', + u'\U0001f6be': u':water_closet:', + u'\U0001f349': u':watermelon:', + u'\U0001f44b': u':wave:', + u'\U0001f3f4': u':waving_black_flag:', + u'\U0001f3f3': u':waving_white_flag:', u'\U00003030': u':wavy_dash:', - u'\U0001F312': u':waxing_crescent_moon:', - u'\U0001F312': u':waxing_crescent_moon_symbol:', - u'\U0001F314': u':waxing_gibbous_moon:', - u'\U0001F314': u':waxing_gibbous_moon_symbol:', - u'\U0001F6BE': u':wc:', - u'\U0001F629': u':weary:', - u'\U0001F640': u':weary_cat_face:', - u'\U0001F629': u':weary_face:', - u'\U0001F492': u':wedding:', - u'\U0001F3CB': u':weight_lifter:', - u'\U0001F40B': u':whale2:', - u'\U0001F40B': u':whale:', - u'\U0001F433': u':whale:', + u'\U0001f312': u':waxing_crescent_moon:', + u'\U0001f629': u':weary:', + u'\U0001f492': u':wedding:', + u'\U0001f3cb': u':weight_lifter:', + u'\U0001f40b': u':whale2:', u'\U00002638': u':wheel_of_dharma:', - u'\U0000267F': u':wheelchair:', - u'\U0000267F': u':wheelchair_symbol:', + u'\U0000267f': u':wheelchair:', u'\U00002705': u':white_check_mark:', - u'\U000026AA': u':white_circle:', - u'\U0001F447': u':white_down_pointing_backhand_index:', - u'\U00002755': u':white_exclamation_mark_ornament:', - u'\U0001F4AE': u':white_flower:', + u'\U0001f4ae': u':white_flower:', u'\U00002639': u':white_frowning_face:', - u'\U00002705': u':white_heavy_check_mark:', - u'\U00002B1C': u':white_large_square:', - u'\U0001F448': u':white_left_pointing_backhand_index:', - u'\U000025FD': u':white_medium_small_square:', - u'\U000025FB': u':white_medium_square:', - u'\U00002B50': u':white_medium_star:', - u'\U00002754': u':white_question_mark_ornament:', - u'\U0001F449': u':white_right_pointing_backhand_index:', - u'\U000025AB': u':white_small_square:', - u'\U0000263A': u':white_smiling_face:', - u'\U0001F533': u':white_square_button:', - u'\U0001F325': u':white_sun_behind_cloud:', - u'\U0001F326': u':white_sun_behind_cloud_with_rain:', - u'\U0001F324': u':white_sun_with_small_cloud:', - u'\U0001F446': u':white_up_pointing_backhand_index:', - u'\U0000261D': u':white_up_pointing_index:', - u'\U0001F32C': u':wind_blowing_face:', - u'\U0001F390': u':wind_chime:', - u'\U0001F377': u':wine_glass:', - u'\U0001F609': u':wink:', - u'\U0001F609': u':winking_face:', - u'\U0001F43A': u':wolf:', - u'\U0001F43A': u':wolf_face:', - u'\U0001F469': u':woman:', - u'\U0001F46F': u':woman_with_bunny_ears:', - u'\U0001F462': u':womans_boots:', - u'\U0001F45A': u':womans_clothes:', - u'\U0001F452': u':womans_hat:', - u'\U0001F461': u':womans_sandal:', - u'\U0001F6BA': u':womens:', - u'\U0001F6BA': u':womens_symbol:', - u'\U0001F5FA': u':world_map:', - u'\U0001F61F': u':worried:', - u'\U0001F61F': u':worried_face:', - u'\U0001F381': u':wrapped_present:', - u'\U0001F527': u':wrench:', - u'\U0000270D': u':writing_hand:', - u'\U0000274C': u':x:', - u'\U0001F49B': u':yellow_heart:', - u'\U0001F4B4': u':yen:', - u'\U0000262F': u':yin_yang:', - u'\U0001F60B': u':yum:', - u'\U000026A1': u':zap:', - u'\U0001F910': u':zipper-mouth_face:', - u'\U0001F910': u':zipper_mouth_face:', - u'\U0001F4A4': u':zzz:' + u'\U00002b1c': u':white_large_square:', + u'\U000025fd': u':white_medium_small_square:', + u'\U000025fb': u':white_medium_square:', + u'\U000025ab': u':white_small_square:', + u'\U0001f533': u':white_square_button:', + u'\U0001f32c': u':wind_blowing_face:', + u'\U0001f390': u':wind_chime:', + u'\U0001f377': u':wine_glass:', + u'\U0001f609': u':wink:', + u'\U0001f43a': u':wolf:', + u'\U0001f469': u':woman:', + u'\U0001f45a': u':womans_clothes:', + u'\U0001f452': u':womans_hat:', + u'\U0001f6ba': u':womens:', + u'\U0001f5fa': u':world_map:', + u'\U0001f61f': u':worried:', + u'\U0001f527': u':wrench:', + u'\U0000270d': u':writing_hand:', + u'\U0001f49b': u':yellow_heart:', + u'\U0000262f': u':yin_yang:', + u'\U0001f910': u':zipper-mouth_face:', } # Expressions taken from Martijn Pieters code at @@ -1483,14 +1068,17 @@ u'[\u2600-\u26FF\u2700-\u27BF])+', re.DOTALL | re.UNICODE) + def convert_emoji_to_aliases(data, modifier, modifier_data, string): - string = unicode(string, "utf-8") + if sys.version_info < (3, ): + string = string.decode('utf-8') emoji_found = ALIAS_RE.findall(string) for emoji in emoji_found: if emoji in EMOJI_ALIASES: string = string.replace(emoji, EMOJI_ALIASES[emoji]) return string + if __name__ == "__main__" and import_ok: if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, @@ -1508,4 +1096,5 @@ def convert_emoji_to_aliases(data, modifier, modifier_data, string): w.hook_modifier("irc_in_wallops", "convert_emoji_to_aliases", "") # This intercepts outgoing emoji also - #w.hook_modifier("input_text_for_buffer", "convert_emoji_to_aliases", "") + # w.hook_modifier("input_text_for_buffer", + # "convert_emoji_to_aliases", "") diff --git a/python/emoji2ascii.py b/python/emoji2ascii.py new file mode 100644 index 00000000..1ababaa3 --- /dev/null +++ b/python/emoji2ascii.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019-2021 by eyJhb (eyjhbb@gmail.com) +# +# replaces incoming emojis with ascii text, and can replace outgoing +# messages with emojis, if the same syntax is followed. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# This script deletes weechatlog-files by age or size +# YOU ARE USING THIS SCRIPT AT YOUR OWN RISK! +# +# 2021-04-15: eyJhb +# 0.2 : added this text + error if unable to import emoji package +# +# 2019-09-16: eyJhb +# 0.1 : initial release +# +# Development is currently hosted at +# https://github.com/eyJhb/weechat-emoji2ascii + +SCRIPT_NAME = "emoji2ascii" +SCRIPT_AUTHOR = "eyJhb " +SCRIPT_VERSION = "0.2" +SCRIPT_LICENSE = "GPLv3" +SCRIPT_DESC = "Replaces emoji characters with ascii text and vice versa" + +import_ok = True + +try: + import weechat as w +except: + print("Script must be run under weechat. http://www.weechat.org") + import_ok = False + +try: + import emoji +except ImportError: + print("Failed to import emoji, please install it") + import_ok = False + +import re + +def convert_emoji_to_aliases(data, modifier, modifier_data, string): + return emoji.demojize(string) + +def convert_aliases_to_emoji(data, modifier, modifier_data, string): + return emoji.emojize(string) + +if __name__ == "__main__" and import_ok: + if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, + SCRIPT_DESC, "", "utf-8"): + + w.hook_modifier("irc_in2_away", "convert_emoji_to_aliases", "") + w.hook_modifier("irc_in2_cnotice", "convert_emoji_to_aliases", "") + w.hook_modifier("irc_in2_cprivmsg", "convert_emoji_to_aliases", "") + w.hook_modifier("irc_in2_kick", "convert_emoji_to_aliases", "") + w.hook_modifier("irc_in2_knock", "convert_emoji_to_aliases", "") + w.hook_modifier("irc_in2_notice", "convert_emoji_to_aliases", "") + w.hook_modifier("irc_in2_part", "convert_emoji_to_aliases", "") + w.hook_modifier("irc_in2_privmsg", "convert_emoji_to_aliases", "") + w.hook_modifier("irc_in2_quit", "convert_emoji_to_aliases", "") + w.hook_modifier("irc_in2_wallops", "convert_emoji_to_aliases", "") + + w.hook_modifier("irc_out1_cprivmsg", "convert_aliases_to_emoji", "") + w.hook_modifier("irc_out1_privmsg", "convert_aliases_to_emoji", "") diff --git a/python/emoji_aliases.py b/python/emoji_aliases.py index 51959ba8..446369cf 100644 --- a/python/emoji_aliases.py +++ b/python/emoji_aliases.py @@ -18,7 +18,7 @@ weechat.register( "emoji_aliases", # name "Mike Reinhardt", # author - "1.0.2", # version + "1.0.4", # version "BSD", # license "Convert emoji aliases to unicode emoji.", # description "", # shutdown function @@ -1465,15 +1465,47 @@ def convert_aliases_to_emoji(data, modifier, modifier_data, string): + # `unmodified` is text not to have replacements done on it + unmodified, modifiable = "", string + if modifier in NEEDSPLIT: - aliases_found = ALIAS_RE.findall(string.split(':', 1)[1]) - else: - aliases_found = ALIAS_RE.findall(string) - for alias in aliases_found: + # if " :" exists in a raw IRC string (once tags have been removed) it + # will be the start of the final (trailing) parameter + + # optionally put IRCv3 tags (and space) in to `unmodified` + if string[0] == "@": + tags, sep, string = string.partition(" ") + unmodified += tags+sep + + # optionally put :source (and space) in to `unmodified` + if string[0] == ":": + source, sep, string = string.partition(" ") + unmodified += source+sep + + # split at the first instance of " :" + # (`trailing` will be empty string if not found) + string, trailing_sep, trailing = string.partition(" :") + + # put COMMAND (and space) in to `unmodified` + command, sep, string = string.partition(" ") + unmodified += command+sep + + if not trailing and string: + # we've not got a :trailing param; let's use the last arg instead + string, sep, modifiable = string.rpartition(" ") + # put all other args (and space) in to `unmodified` + unmodified += string+sep + else: + # we've got a :trailing param. + # put all the other args (and " :") in to `unmodified` + unmodified += string+trailing_sep + modifiable = trailing + + for alias in ALIAS_RE.findall(modifiable): if alias in EMOJI_ALIASES: - string = string.replace(alias, '{} '.format(EMOJI_ALIASES[alias].encode('utf-8'))) - return string + modifiable = modifiable.replace(alias, '{} '.format(EMOJI_ALIASES[alias])) + return unmodified+modifiable for hook in HOOKS: weechat.hook_modifier( diff --git a/python/emojis.py b/python/emojis.py new file mode 100644 index 00000000..16603950 --- /dev/null +++ b/python/emojis.py @@ -0,0 +1,474 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 jmui +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# +# +# A simple script to print a random emoji chosen from a list +# +# Usage: /emojis +# +# +# History: +# 2017-11-17 +# 0.1 First version +# +# + +SCRIPT_NAME = "emojis" +SCRIPT_AUTHOR = "jmui " +SCRIPT_VERSION = "0.1" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = "Send a random emoji to the current buffer" + + +import_ok = True + +try: + import weechat +except ImportError: + print("Script must be used in WeeChat.") + import_ok = False + +from random import choice + + +emojiList = [ + '¢‿¢', + '©¿© o', + 'ª{•̃̾_•̃̾}ª', + '¬_¬', + '¯\(º_o)/¯', + '¯\(º o)/¯', + '¯\_(⊙︿⊙)_/¯', + '¯\_(ツ)_/¯', + '°ω°', + '°Д°', + '°‿‿°', + '´ ▽ ` )ノ', + '¿ⓧ_ⓧﮌ', + 'Ò,ó', + 'ó‿ó', + 'ô⌐ô', + 'ôヮô', + 'ŎםŎ', + 'ŏﺡó', + 'ʕ•̫͡•ʔ', + 'ʕ•ᴥ•ʔ', + 'ʘ‿ʘ', + '˚•_•˚', + '˚⌇˚', + '˚▱˚', + 'Σ ◕ ◡ ◕', + 'Σ (゚Д゚;)', + 'Σ(゚Д゚;≡;゚д゚)', + 'Σ(゚Д゚ )', + 'Σ(||゚Д゚)', + 'Φ,Φ', + 'δﺡό', + 'σ_σ', + 'д_д', + 'ф_ф', + 'щ(゚Д゚щ)', + 'щ(ಠ益ಠщ)', + 'щ(ಥДಥщ)', + 'Ծ_Ծ', + '٩๏̯͡๏۶', + '٩๏̯͡๏)۶', + '٩◔̯◔۶', + '٩(×̯×)۶', + '٩(̾●̮̮̃̾•̃̾)۶', + '٩(͡๏̯͡๏)۶', + '٩(͡๏̯ ͡๏)۶', + '٩(ಥ_ಥ)۶', + '٩(•̮̮̃•̃)۶', + '٩(●̮̮̃•̃)۶', + '٩(●̮̮̃●̃)۶', + '٩(。͡•‿•。)۶', + '٩(-̮̮̃•̃)۶', + '٩(-̮̮̃-̃)۶', + '۞_۞', + '۞_۟۞', + '۹ↁﮌↁ', + '۹⌤_⌤۹', + '॓_॔', + '१✌◡✌५', + '१|˚–˚|५', + 'ਉ_ਉ', + 'ଘ_ଘ', + 'இ_இ', + 'ఠ_ఠ', + 'రృర', + 'ಠ¿ಠi', + 'ಠ‿ಠ', + 'ಠ⌣ಠ', + 'ಠ╭╮ಠ', + 'ಠ▃ಠ', + 'ಠ◡ಠ', + 'ಠ益ಠ', + 'ಠ益ಠ', + 'ಠ︵ಠ凸', + 'ಠ , ಥ', + 'ಠ.ಠ', + 'ಠoಠ', + 'ಠ_ృ', + 'ಠ_ಠ', + 'ಠ_๏', + 'ಠ~ಠ', + 'ಡ_ಡ', + 'ತಎತ', + 'ತ_ತ', + 'ಥдಥ', + 'ಥ‿ಥ', + 'ಥ⌣ಥ', + 'ಥ◡ಥ', + 'ಥ﹏ಥ', + 'ಥ_ಥ', + 'ಭ_ಭ', + 'ರ_ರ', + 'ಸ , ໖', + 'ಸ_ಸ', + 'ക_ക', + 'อ้_อ้', + 'อ_อ', + 'โ๏௰๏ใ ื', + '๏̯͡๏﴿', + '๏̯͡๏', + '๏̯͡๏﴿', + '๏[-ิิ_•ิ]๏', + '๏_๏', + '໖_໖', + '༺‿༻', + 'ლ(´ڡ`ლ)', + 'ლ(́◉◞౪◟◉‵ლ)', + 'ლ(ಠ益ಠლ)', + 'ლ(╹◡╹ლ)', + 'ლ(◉◞౪◟◉‵ლ)', + 'ლ,ᔑ•ﺪ͟͠•ᔐ.ლ', + 'ᄽὁȍ ̪ őὀᄿ', + 'ᕕ( ᐛ )ᕗ', + 'ᕙ(⇀‸↼‶)ᕗ', + 'ᕦ(ò_óˇ)ᕤ', + 'ᶘ ᵒᴥᵒᶅ', + '‘︿’', + '•▱•', + '•✞_✞•', + '•(⌚_⌚)•', + '•_•)', + '‷̗ↂ凸ↂ‴̖', + '‹•.•›', + '‹› ‹(•¿•)› ‹›', + '‹(ᵒᴥᵒ­­­­­)›', + '‹(•¿•)›', + 'ↁ_ↁ', + '⇎_⇎', + '∩(︶▽︶)∩', + '∩( ・ω・)∩', + '≖‿≖', + '≧ヮ≦', + '⊂•⊃_⊂•⊃', + '⊂⌒~⊃。Д。)⊃', + '⊂(◉‿◉)つ', + '⊂(゚Д゚,,⊂⌒`つ', + '⊙ω⊙', + '⊙▂⊙', + '⊙▃⊙', + '⊙△⊙', + '⊙︿⊙', + '⊙﹏⊙', + '⊙0⊙', + '⊛ठ̯⊛', + '⋋ō_ō`', + '━━━ヽ(ヽ(゚ヽ(゚∀ヽ(゚∀゚ヽ(゚∀゚)ノ゚∀゚)ノ∀゚)ノ゚)ノ)ノ━━━', + '┌∩┐(◕_◕)┌∩┐', + '┌( ಠ_ಠ)┘', + '┌( ಥ_ಥ)┘', + '╚(•⌂•)╝', + '╭╮╭╮☜{•̃̾_•̃̾}☞╭╮╭╮', + '╭✬⌢✬╮', + '╮(─▽─)╭', + '╯‵Д′)╯彡┻━┻', + '╰☆╮', + '□_□', + '►_◄', + '◃┆◉◡◉┆▷', + '◉△◉', + '◉︵◉', + '◉_◉', + '○_○', + '●¿●\ ~', + '●_●', + '◔̯◔', + '◔ᴗ◔', + '◔ ⌣ ◔', + '◔_◔', + '◕ω◕', + '◕‿◕', + '◕◡◕', + '◕ ◡ ◕', + '◖♪_♪|◗', + '◖|◔◡◉|◗', + '◘_◘', + '◙‿◙', + '◜㍕◝', + '◪_◪', + '◮_◮', + '☁ ☝ˆ~ˆ☂', + '☆¸☆', + '☉‿⊙', + '☉_☉', + '☐_☐', + '☜(⌒▽⌒)☞', + '☜(゚ヮ゚☜)', + '☜-(ΘLΘ)-☞', + '☝☞✌', + '☮▁▂▃▄☾ ♛ ◡ ♛ ☽▄▃▂▁☮', + '☹_☹', + '☻_☻', + '☼.☼', + '☾˙❀‿❀˙☽', + '♥‿♥', + '♥╣[-_-]╠♥', + '♥╭╮♥', + '♥◡♥', + '✌♫♪˙❤‿❤˙♫♪✌', + '✌.ʕʘ‿ʘʔ.✌', + '✌.|•͡˘‿•͡˘|.✌', + '✖‿✖', + '✖_✖', + '❐‿❑', + '⨀_⨀', + '⨀_Ꙩ', + '⨂_⨂', + '〆(・∀・@)', + '《〠_〠》', + '【•】_【•】', + '〠_〠', + '〴⋋_⋌〵', + 'の� �の', + 'ニガー? ━━━━━━(゚∀゚)━━━━━━ ニガー?', + 'ヽ(´ー` )ノ', + 'ヽ(๏∀๏ )ノ', + 'ヽ(`Д´)ノ', + 'ヽ(o`皿′o)ノ', + 'ヽ(`Д´)ノ', + 'ㅎ_ㅎ', + '乂◜◬◝乂', + '凸ಠ益ಠ)凸', + '句_句', + 'Ꙩ⌵Ꙩ', + 'Ꙩ_Ꙩ', + 'ꙩ_ꙩ', + 'Ꙫ_Ꙫ', + 'ꙫ_ꙫ', + 'ꙮ_ꙮ', + '흫_흫', + '句_句', + '﴾͡๏̯͡๏﴿', + '¯\(ºдಠ)/¯', + '(·×·)', + '(⌒Д⌒)', + '(╹ェ╹)', + '(♯・∀・)⊃', + '( ´∀`)☆', + '( ´∀`)', + '(゜Д゜)', + '(・∀・)', + '(・A・)', + '(゚∀゚)', + '( ̄へ ̄)', + '( ´☣///_ゝ///☣`)', + '( つ Д `)', + '_☆( ´_⊃`)☆_', + '。◕‿‿◕。', + '。◕ ‿ ◕。', + '!⑈ˆ~ˆ!⑈', + '!(`・ω・。)', + '(¬‿¬)', + '(¬▂¬)', + '(¬_¬)', + '(°ℇ °)', + '(°∀°)', + '(´ω`)', + '(´◉◞౪◟◉)', + '(´ヘ`;)', + '(´・ω・`)', + '(´ー`)', + '(ʘ‿ʘ)', + '(ʘ_ʘ)', + '(˚இ˚)', + '(͡๏̯͡๏)', + '(ΘεΘ;)', + '(ι´Д`)ノ', + '(Ծ‸ Ծ)', + '(॓_॔)', + '(० ्०)', + '(ு८ு_ .:)', + '(ಠ‾ಠ)', + '(ಠ‿ʘ)', + '(ಠ‿ಠ)', + '(ಠ⌣ಠ)', + '(ಠ益ಠ ╬)', + '(ಠ益ಠ)', + '(ಠ_ృ)', + '(ಠ_ಠ)', + '(ಥ﹏ಥ)', + '(ಥ_ಥ)', + '(๏̯͡๏ )', + '(ღ˘⌣˘ღ) ♫・*:.。. .。.:*・', + '(ღ˘⌣˘ღ)', + '(ᵔᴥᵔ)', + '(•ω•)', + '(•‿•)', + '(•⊙ω⊙•)', + '(• ε •)', + '(∩▂∩)', + '(∩︵∩)', + '(∪ ◡ ∪)', + '(≧ω≦)', + '(≧◡≦)', + '(≧ロ≦)', + '(⊙ヮ⊙)', + '(⊙_◎)', + '(⋋▂⋌)', + '(⌐■_■)', + '(─‿‿─)', + '(┛◉Д◉)┛┻━┻', + '(╥_╥)', + '(╬ಠ益ಠ)', + '(╬◣д◢)', + '(╬ ಠ益ಠ)', + '(╯°□°)╯︵ ┻━┻', + '(╯ಊ╰)', + '(╯◕_◕)╯', + '(╯︵╰,)', + '(╯3╰)', + '(╯_╰)', + '(╹◡╹)凸', + '(▰˘◡˘▰)', + '(●´ω`●)', + '(●´∀`●)', + '(◑‿◐)', + '(◑◡◑)', + '(◕‿◕✿)', + '(◕‿◕)', + '(◕‿-)', + '(◕︵◕)', + '(◕ ^ ◕)', + '(◕_◕)', + '(◜௰◝)', + '(◡‿◡✿)', + '(◣_◢)', + '(☞゚∀゚)☞', + '(☞゚ヮ゚)☞', + '(☞゚ ∀゚ )☞', + '(☼◡☼)', + '(☼_☼)', + '(✌゚∀゚)☞', + '(✖╭╮✖)', + '(✪㉨✪)', + '(✿◠‿◠)', + '(✿ ♥‿♥)', + '( ・∀・)', + '( ・ัω・ั)?', + '( ゚∀゚)o彡゜', + '(。・_・。)', + '(つд`)', + '(づ。◕‿‿◕。)づ', + '(ノಠ益ಠ)ノ彡┻━┻', + '(ノ ◑‿◑)ノ', + '(ノ_・。)', + '(・∀・ )', + '(屮゚Д゚)屮', + '(︶ω︶)', + '(︶︹︺)', + '(;一_一)', + '(`・ω・´)”', + '(。◕‿‿◕。)', + '(。◕‿◕。)', + '(。◕ ‿ ◕。)', + '(。♥‿♥。)', + '(。・ω..・)っ', + '(・ェ-)', + '(ノ◕ヮ◕)ノ*:・゚✧', + '(゚Д゚)', + '(゚Д゚)y─┛~~', + '(゚∀゚)', + '(゚ヮ゚)', + '( ̄□ ̄)', + '( ̄。 ̄)', + '( ̄ー ̄)', + '( ̄(エ) ̄)', + '( °٢° )', + '( ´_ゝ`)', + '( ͡° ͜ʖ ͡°)', + '( ͡~ ͜ʖ ͡°)', + '( ಠ◡ಠ )', + '( •_•)>⌐■-■', + '(  ゚,_ゝ゚)', + '( ・ิз・ิ)', + '( ゚д゚)、', + '( ^▽^)σ)~O~)', + '((((゜д゜;))))', + '(*´д`*)', + '(*..Д`)', + '(*..д`*)', + '(*~▽~)', + '(-’๏_๏’-)', + '(-_- )ノ', + '(/◔ ◡ ◔)/', + '(///_ಥ)', + '(;´Д`)', + '(=ω=;)', + '(=゜ω゜)', + '(>\'o\')> ♥ <(\'o\'<)', + '(n˘v˘•)¬', + '(o´ω`o)', + '(V)(°,,°)(V)', + '(\/) (°,,°) (\/)', + '(^▽^)', + '(`・ω・´)', + '(~ ̄▽ ̄)~', + '\= (゚д゚)ウ', + '@_@', + 'd(*⌒▽⌒*)b', + 'o(≧∀≦)o', + 'o(≧o≦)o', + 'q(❂‿❂)p', + 'y=ー( ゚д゚)・∵.', + '\˚ㄥ˚\ ', + '\ᇂ_ᇂ\ ', + '\(ಠ ὡ ಠ )/', + '\(◕ ◡ ◕\)', + '^̮^', + '^ㅂ^', + '_(͡๏̯͡๏)_', + '{´◕ ◡ ◕`}', + '\{ಠ_ಠ\}__,,|,', + '{◕ ◡ ◕}', +] + + +def print_face(data, buf, args): + weechat.command(buf, choice(emojiList)) + return weechat.WEECHAT_RC_OK + + +if __name__ == "__main__" and import_ok: + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, + SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + weechat.hook_command(SCRIPT_NAME, SCRIPT_DESC, "", "", "", + "print_face", "") diff --git a/python/emojize.py b/python/emojize.py new file mode 100644 index 00000000..47ee5129 --- /dev/null +++ b/python/emojize.py @@ -0,0 +1,83 @@ +""" +Weechat plugin to convert emoji shortcodes to unicode emoji. + +This plugin is a thin wrapper around the emoji package for python. +It converts emoji shortcodes to Unicode emoji. + +This package is based on the emoji_aliases.py script by Mike Reinhardt. + +License: CC0 +Author: Thom Wiggers +Repository: https://github.com/thomwiggers/weechat-emojize + +This plugin supports python 3 and requires the 'emoji' python package. +Requires at least weechat 1.3 + +Changelog: + 1.0.1 - 2023-08-06: mva + Adaptation to modern version of `emoji` package + (use_aliases => language="alias") + +""" + + +def register(): + weechat.register( + "emojize", + "Thom Wiggers", + "1.0.1", + "CC0", + "Convert emoji shortcodes to unicode emoji", + "", # shutdown function + "utf-8", + ) + + +import_ok = True + +try: + import emoji +except ImportError: + print("Failed to import emoji package, try installing 'emoji'") + import_ok = False + +import weechat + + +HOOKS = ( + "away", + "cnotice", + "cprivmsg", + "kick", + "knock", + "notice", + "part", + "privmsg", + "quit", + "wallops", +) + + +def convert_emoji(_data, modifier, _modifier_data, string): + """Convert the emoji in event messages""" + # Check if this message has a segment we shouldn't touch. + msg = weechat.info_get_hashtable("irc_message_parse", {"message": string}) + pos_text = int(msg["pos_text"]) + if msg["text"] != "" and pos_text > 0: + return ( + string[:pos_text] + + emoji.emojize(msg["text"], language="alias") + + string[(pos_text + len(msg["text"])):] + ) + + if modifier == "input_text_for_buffer": + return emoji.emojize(string, language="alias") + + return string + + +if __name__ == "__main__" and import_ok: + register() + weechat.hook_modifier("input_text_for_buffer", "convert_emoji", "") + for hook in HOOKS: + weechat.hook_modifier("irc_in2_{}".format(hook), "convert_emoji", "") diff --git a/python/execbot.py b/python/execbot.py new file mode 100644 index 00000000..281df5e4 --- /dev/null +++ b/python/execbot.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +# execbot.py +# ExecBot - Executing bot +# ============================== +# +# Copyright (C) 2018 Giap Tran +# https://github.com/txgvnn/weechat-execbot +# +# == About ===================== +# Execbot is a bot can execute the command from IRC message +# (You maybe want to call it is a kind of backdoor) +# +# With server is installed execbot script, the client can execute the +# command remote by send privmsg to server's nick in IRC network +# +# == Usage ===================== +# /excebot list List server.nicknames are allowed +# /execbot add -server nicknames.. Add server.nicknames are allowed to execute +# /execbot del -server nicknames.. Remove server.nicknames are allowed +# +# == NOTICE ==================== +# This is a POC that I want to execute a remote command via IRC +# network. It's really not security, although you can set who can +# talk with the bot. +# +# PLEASE CONSIDER CAREFULLY WHEN USING THE PLUGIN + +import weechat + +SCRIPT_NAME = 'execbot' +SCRIPT_AUTHOR = 'Giap Tran ' +SCRIPT_VERSION = '1.1' +SCRIPT_LICENSE = 'GPL3' +SCRIPT_DESC = 'Executing bot' +SCRIPT_HELP_TEXT = ''' +%(bold)sExecbot command options: %(normal)s +list List nicknames +add -server Add server.nicknames are allowed to execute +del -server Del server.nicknames are allowed + +%(bold)sExamples: %(normal)s +Allow john and jack in oftc server to talk with bot + /execbot add -server oftc john jack +''' % {'bold':weechat.color('bold'), 'normal':weechat.color('-bold')} + +execbot_config_file = None +execbot_config_section = {} +execbot_allows = {} + +def execbot_config_init(): + '''Init configuration file''' + global execbot_config_file + execbot_config_file = weechat.config_new('execbot', 'execbot_config_reload_cb', '') + if not execbot_config_file: + return + + execbot_config_section['allows'] = weechat.config_new_section( + execbot_config_file, 'allows', 0, 0, 'execbot_config_allows_read_cb', '', + 'execbot_config_allows_write_cb', '', '', '', '', '', '', '') + if not execbot_config_section['allows']: + weechat.config_free(execbot_config_file) + +def execbot_config_reload_cb(data, config_file): + '''Handle a reload of the configuration file.''' + global execbot_allows + execbot_allows = {} + return weechat.config_reload(config_file) + +def execbot_config_allows_read_cb(data, config_file, section_name, option_name, value): + '''Read elements of the allows section from the configuration file.''' + execbot_allows[option_name.lower()] = value + return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED + +def execbot_config_allows_write_cb(data, config_file, section_name): + '''Write infomation to the allows section of the configuration file.''' + weechat.config_write_line(config_file, section_name, '') + for username, right in sorted(list(execbot_allows.items())): + weechat.config_write_line(config_file, username.lower(), right) + return weechat.WEECHAT_RC_OK + +def execbot_config_read(): + ''' Read Execbot configuration file (execbot.conf).''' + return weechat.config_read(execbot_config_file) + +def execbot_config_write(): + ''' Write Execbot configuration file (execbot.conf) to disk.''' + return weechat.config_write(execbot_config_file) + +def execbot_command_list(): + '''List server.nicknames are allowed.''' + nicknames = '\n'.join([' %s' % x for x in execbot_allows.keys()]) + # for nickname,_ in sorted(list(execbot_allows.items())): + weechat.prnt(weechat.current_buffer(), 'Nicknames are allowed:\n' +nicknames) + return weechat.WEECHAT_RC_OK + +def execbot_command_add(server,nicknames): + '''Add nicknames.''' + for x in nicknames: + execbot_allows['%s.%s'%(server,x.lower())] = 'allow' + weechat.prnt(weechat.current_buffer(),'Add permission for %s' % '%s.%s'%(server,x.lower())) + return weechat.WEECHAT_RC_OK + +def execbot_command_del(server,nicknames): + '''Remove nicknames.''' + for x in nicknames: + try: + del execbot_allows['%s.%s'%(server,x.lower())] + weechat.prnt(weechat.current_buffer(),'Deleted permission of %s' % '%s.%s'%(server,x.lower())) + except KeyError: + weechat.prnt(weechat.current_buffer(),'No existing %s.%s'%(server,x.lower())) + return weechat.WEECHAT_RC_OK + +def execbot_command(data, buffer, args): + '''Hook to handle the /execbot weechat command.''' + argv = args.split() + + # list + if not argv or argv == ['list']: + return execbot_command_list() + + # check if a server was set + if (len(argv) > 2 and argv[1] == '-server'): + server = argv[2] + del argv[1:3] + args = (args.split(' ', 2)+[''])[2] + else: + server = weechat.buffer_get_string(buffer, 'localvar_server') + if not server: + weechat.prnt(weechat.current_buffer(), 'Required -server option') + return weechat.WEECHAT_RC_ERROR + + # add + if argv[:1] == ['add']: + if len(argv) < 2: + return weechat.WEECHAT_RC_ERROR + return execbot_command_add(server,argv[1:]) + + # del + if argv[:1] == ['del']: + if len(argv) < 2: + return weechat.WEECHAT_RC_ERROR + return execbot_command_del(server,argv[1:]) + + execbot_error('Unknown command. Try /help execbot', buffer) + return weechat.WEECHAT_RC_OK + +def execbot_process(buffer, command, return_code, out, err): + '''Execute the command and return to buffer.''' + message = "%s ... | $? = %d\n" % (command.split()[0], return_code) + if out != "": + message += out + if err != "": + message += err + + weechat.command(buffer, message) + return weechat.WEECHAT_RC_OK + +def execbot_hook_signal(data, signal, signal_data): + server = signal.split(",")[0] + info = weechat.info_get_hashtable("irc_message_parse", { "message": signal_data }) + username = '.'.join([server,info['nick']]) + + # Check the permission + allowed = execbot_allows.get(username.lower()) + if not allowed: + return weechat.WEECHAT_RC_OK + # Prevent public channel + if info['channel'].startswith('#'): + return weechat.WEECHAT_RC_OK + + # buffer output + buffer = weechat.buffer_search("irc", username) + # command + _, command = info['arguments'].split(':', 1) + + # timeout = 5 mins + weechat.hook_process(command, 300000, "execbot_process", buffer) + + return weechat.WEECHAT_RC_OK + +def execbot_unload_script(): + execbot_config_write() + return weechat.WEECHAT_RC_OK + +if __name__ == '__main__' and weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, + SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, 'execbot_unload_script','UTF-8'): + execbot_config_init() + execbot_config_read() + weechat.hook_command('execbot', 'Commands to manage Execbot options and execute Execbot commands', + '', + SCRIPT_HELP_TEXT, + 'list %- || add -server %(irc_servers) %(nicks) %-' + '|| del -server %(irc_servers) %(nicks) %-', + 'execbot_command', '') + weechat.hook_signal("*,irc_in2_privmsg", "execbot_hook_signal", "") diff --git a/python/fileaway.py b/python/fileaway.py index 2e8fc958..88d07e10 100644 --- a/python/fileaway.py +++ b/python/fileaway.py @@ -4,7 +4,7 @@ # # fileaway.py - A simple autoaway script for Weechat which monitors a file, # allowing it to easily connect to external things (such as xscreensaver) -# +# # The code from screen_away.py and auto_away.py were heavily consulted in the # writing of this script # --------------------------------------------------------------------------- @@ -20,7 +20,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# along with this program. If not, see . # # ------------------------------------------------------------------------- # Purpose - @@ -35,7 +35,7 @@ # While this only one way this script can be used, this is why I wrote it # # #!/bin/sh -# +# # # Read xscreensaver's state # xscreensaver-command -watch| # while read STATUS; do @@ -72,19 +72,23 @@ # -Handles improper commands # Version 1.0.2 release - Jun 15, 2011 # -Added alternative for xset users (credit: sherpa9 at irc.freenode.net) +# Version 1.0.3 release - Dec 19, 2019 +# -Adapt for python3 + +from __future__ import print_function try: import weechat as w import os.path except Exception: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.wwchat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.wwchat.org/") quit() SCRIPT_NAME = "fileaway" SCRIPT_AUTHOR = "javagamer" -SCRIPT_VERSION = "1.0.2" +SCRIPT_VERSION = "1.0.3" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Set away status based on presence of a file" debug = 0 @@ -150,8 +154,8 @@ def fileaway_cb(data, buffer, args): response[words[0]](words[2]) else: w.prnt('', "Fileaway error: %s not a recognized command. Try /help fileaway" % words[0]) - w.prnt('', "fileaway: enabled: %s interval: %s away message: \"%s\" filepath: %s" % - (w.config_get_plugin('status'), w.config_get_plugin('interval'), + w.prnt('', "fileaway: enabled: %s interval: %s away message: \"%s\" filepath: %s" % + (w.config_get_plugin('status'), w.config_get_plugin('interval'), w.config_get_plugin('awaymessage'), w.config_get_plugin('filepath'))) return w.WEECHAT_RC_OK @@ -178,11 +182,11 @@ def check_timer(): if __name__ == "__main__": if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - for option, default_value in settings.iteritems(): + for option, default_value in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_value) - w.hook_command("fileaway", + w.hook_command("fileaway", "Set away status based on presense or absense of a file.", "check, msg [status], interval [time], file [filepath], or enable|disable", "check - manually checks for file rather than waiting for interval.\n" diff --git a/python/fish.py b/python/fish.py index 426560c1..17506076 100644 --- a/python/fish.py +++ b/python/fish.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # +# Copyright (C) 2011-2022 David Flatz +# Copyright (C) 2017-2020 Marcin Kurczewski +# Copyright (C) 2017 Ricardo Ferreira # Copyright (C) 2014 Charles Franklin # Copyright (C) 2012 Markus Näsman -# Copyright (C) 2011 David Flatz # Copyright (C) 2009 Bjorn Edstrom # # This program is free software; you can redistribute it and/or modify @@ -50,32 +52,37 @@ # IRC utterly broken in terms of security. # +import re +import struct +import hashlib +import base64 +import sys +from os import urandom + SCRIPT_NAME = "fish" SCRIPT_AUTHOR = "David Flatz " -SCRIPT_VERSION = "0.9" +SCRIPT_VERSION = "0.15" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "FiSH for weechat" CONFIG_FILE_NAME = SCRIPT_NAME import_ok = True -import re -import struct -import hashlib -from os import urandom - try: import weechat except ImportError: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org/") import_ok = False try: - import Crypto.Cipher.Blowfish -except: - print "Python Cryptography Toolkit must be installed to use fish" - import_ok = False + import Crypto.Cipher.Blowfish as CryptoBlowfish +except ImportError: + try: + import Cryptodome.Cipher.Blowfish as CryptoBlowfish + except ImportError: + print("Pycryptodome must be installed to use fish") + import_ok = False # @@ -93,6 +100,7 @@ fish_secure_key = "" fish_secure_cipher = None + # # CONFIG # @@ -102,11 +110,12 @@ def fish_config_reload_cb(data, config_file): def fish_config_keys_read_cb(data, config_file, section_name, option_name, - value): + value): global fish_keys - option = weechat.config_new_option(config_file, section_name, option_name, - "string", "key", "", 0, 0, "", value, 0, "", "", "", "", "", "") + option = weechat.config_new_option( + config_file, section_name, option_name, "string", "key", "", 0, 0, + "", value, 0, "", "", "", "", "", "") if not option: return weechat.WEECHAT_CONFIG_OPTION_SET_ERROR @@ -119,14 +128,11 @@ def fish_config_keys_write_cb(data, config_file, section_name): global fish_keys, fish_secure_cipher weechat.config_write_line(config_file, section_name, "") - for target, key in sorted(fish_keys.iteritems()): - - if fish_secure_cipher != None: - ### ENCRYPT Targets/Keys ### - weechat.config_write_line(config_file, - blowcrypt_pack(target, fish_secure_cipher), - blowcrypt_pack(key, fish_secure_cipher)) - + for target, key in sorted(fish_keys.items()): + if fish_secure_cipher is not None: + weechat.config_write_line( + config_file, blowcrypt_pack(target.encode(), fish_secure_cipher), + blowcrypt_pack(key.encode(), fish_secure_cipher)) else: weechat.config_write_line(config_file, target, key) @@ -137,69 +143,69 @@ def fish_config_init(): global fish_config_file, fish_config_section, fish_config_option global fish_secure_cipher - fish_config_file = weechat.config_new(CONFIG_FILE_NAME, - "fish_config_reload_cb", "") + fish_config_file = weechat.config_new( + CONFIG_FILE_NAME, "fish_config_reload_cb", "") if not fish_config_file: return # look - fish_config_section["look"] = weechat.config_new_section(fish_config_file, - "look", 0, 0, "", "", "", "", "", "", "", "", "", "") + fish_config_section["look"] = weechat.config_new_section( + fish_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "", + "") if not fish_config_section["look"]: weechat.config_free(fish_config_file) return fish_config_option["announce"] = weechat.config_new_option( - fish_config_file, fish_config_section["look"], "announce", - "boolean", "annouce if messages are being encrypted or not", "", 0, - 0, "on", "on", 0, "", "", "", "", "", "") - + fish_config_file, fish_config_section["look"], "announce", + "boolean", "announce if messages are being encrypted or not", "", + 0, 0, "on", "on", 0, "", "", "", "", "", "") fish_config_option["marker"] = weechat.config_new_option( - fish_config_file, fish_config_section["look"], "marker", - "string", "marker for important FiSH messages", "", 0, 0, - "O<", "O<", 0, "", "", "", "", "", "") + fish_config_file, fish_config_section["look"], "marker", + "string", "marker for important FiSH messages", "", 0, 0, + "O<", "O<", 0, "", "", "", "", "", "") fish_config_option["mark_position"] = weechat.config_new_option( - fish_config_file, fish_config_section["look"], "mark_position", - "integer", "put marker for encrypted INCOMING messages at start or end", - "off|begin|end", - 0,2, "off", "off", 0, "", "", "", "", "", "") + fish_config_file, fish_config_section["look"], "mark_position", + "integer", + "put marker for encrypted INCOMING messages at start or end", + "off|begin|end", 0, 2, "off", "off", 0, "", "", "", "", "", "") fish_config_option["mark_encrypted"] = weechat.config_new_option( - fish_config_file, fish_config_section["look"], "mark_encrypted", - "string", "marker for encrypted INCOMING messages", "", 0, 0, - "*", "*", 0, "", "", "", "", "", "") + fish_config_file, fish_config_section["look"], "mark_encrypted", + "string", "marker for encrypted INCOMING messages", "", 0, 0, + "*", "*", 0, "", "", "", "", "", "") # color - fish_config_section["color"] = weechat.config_new_section(fish_config_file, - "color", 0, 0, "", "", "", "", "", "", "", "", "", "") + fish_config_section["color"] = weechat.config_new_section( + fish_config_file, "color", 0, 0, "", "", "", "", "", "", "", "", + "", "") if not fish_config_section["color"]: weechat.config_free(fish_config_file) return fish_config_option["alert"] = weechat.config_new_option( - fish_config_file, fish_config_section["color"], "alert", - "color", "color for important FiSH message markers", "", 0, 0, - "lightblue", "lightblue", 0, "", "", "", "", "", "") + fish_config_file, fish_config_section["color"], "alert", + "color", "color for important FiSH message markers", "", 0, 0, + "lightblue", "lightblue", 0, "", "", "", "", "", "") # secure - fish_config_section["secure"] = weechat.config_new_section(fish_config_file, - "secure", 0, 0, "", "", "", "", "", "", "", "", "", "") + fish_config_section["secure"] = weechat.config_new_section( + fish_config_file, "secure", 0, 0, "", "", "", "", "", "", "", "", + "", "") if not fish_config_section["secure"]: weechat.config_free(fish_config_file) return fish_config_option["key"] = weechat.config_new_option( - fish_config_file, fish_config_section["secure"], "key", - "string", "key for securing blowfish keys", "", 0, 0, "", "", - 0, "", "", "", "", "", "") + fish_config_file, fish_config_section["secure"], "key", + "string", "key for securing blowfish keys", "", 0, 0, "", "", + 0, "", "", "", "", "", "") # keys - fish_config_section["keys"] = weechat.config_new_section(fish_config_file, - "keys", 0, 0, - "fish_config_keys_read_cb", "", - "fish_config_keys_write_cb", "", "", - "", "", "", "", "") + fish_config_section["keys"] = weechat.config_new_section( + fish_config_file, "keys", 0, 0, "fish_config_keys_read_cb", "", + "fish_config_keys_write_cb", "", "", "", "", "", "", "") if not fish_config_section["keys"]: weechat.config_free(fish_config_file) return @@ -218,7 +224,7 @@ def fish_config_write(): ## -## Blowfish and DH1080 Code: +# Blowfish and DH1080 Code: ## # # BLOWFISH @@ -230,7 +236,8 @@ def __init__(self, key=None): if key: if len(key) > 72: key = key[:72] - self.blowfish = Crypto.Cipher.Blowfish.new(key) + self.blowfish = CryptoBlowfish.new( + key.encode('utf-8'), CryptoBlowfish.MODE_ECB) def decrypt(self, data): return self.blowfish.decrypt(data) @@ -246,10 +253,10 @@ def blowcrypt_b64encode(s): res = '' while s: left, right = struct.unpack('>LL', s[:8]) - for i in xrange(6): + for i in range(6): res += B64[right & 0x3f] right >>= 6 - for i in xrange(6): + for i in range(6): res += B64[left & 0x3f] left >>= 6 s = s[8:] @@ -259,19 +266,19 @@ def blowcrypt_b64encode(s): def blowcrypt_b64decode(s): """A non-standard base64-decode.""" B64 = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - res = '' + res = [] while s: left, right = 0, 0 for i, p in enumerate(s[0:6]): right |= B64.index(p) << (i * 6) for i, p in enumerate(s[6:12]): left |= B64.index(p) << (i * 6) - for i in range(0,4): - res +=chr(((left & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8))) - for i in range(0,4): - res +=chr(((right & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8))) + for i in range(0, 4): + res.append((left & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8)) + for i in range(0, 4): + res.append((right & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8)) s = s[12:] - return res + return bytes(res) def padto(msg, length): @@ -279,7 +286,7 @@ def padto(msg, length): If the length of msg is already a multiple of 'length', does nothing.""" L = len(msg) if L % length: - msg += '\x00' * (length - L % length) + msg += b'\x00' * (length - L % length) assert len(msg) % length == 0 return msg @@ -289,30 +296,44 @@ def blowcrypt_pack(msg, cipher): return '+OK ' + blowcrypt_b64encode(cipher.encrypt(padto(msg, 8))) -def blowcrypt_unpack(msg, cipher): +def blowcrypt_unpack(msg, cipher, key): """.""" if not (msg.startswith('+OK ') or msg.startswith('mcps ')): raise ValueError _, rest = msg.split(' ', 1) - if len(rest) < 12: - raise MalformedError - if not (len(rest) %12) == 0: - rest = rest[:-(len(rest) % 12)] + if rest.startswith('*'): # CBC mode + rest = rest[1:] + if len(rest) % 4: + rest += '=' * (4 - len(rest) % 4) + raw = base64.b64decode(rest) - try: - raw = blowcrypt_b64decode(padto(rest, 12)) - except TypeError: - raise MalformedError - if not raw: - raise MalformedError + iv = raw[:8] + raw = raw[8:] + + cbcCipher = CryptoBlowfish.new( + key.encode('utf-8'), CryptoBlowfish.MODE_CBC, iv) + + plain = cbcCipher.decrypt(padto(raw, 8)) + + else: + + if len(rest) < 12: + raise ValueError + + if not (len(rest) % 12) == 0: + rest = rest[:-(len(rest) % 12)] + + try: + raw = blowcrypt_b64decode(padto(rest, 12)) + except TypeError: + raise ValueError + if not raw: + raise ValueError - try: plain = cipher.decrypt(raw) - except ValueError: - raise MalformedError - return plain.strip('\x00').replace('\n','') + return plain.strip(b'\x00').replace(b'\n', b'') # @@ -329,7 +350,7 @@ def blowcrypt_unpack(msg, cipher): '83EB68FA07A77AB6AD7BEB618ACF9C' 'A2897EB28A6189EFA07AB99A8A7FA9' 'AE299EFA7BA66DEAFEFBEFBF0B7D8B', 16) -q_dh1080 = (p_dh1080 - 1) / 2 +q_dh1080 = (p_dh1080 - 1) // 2 def dh1080_b64encode(s): @@ -341,7 +362,7 @@ def dh1080_b64encode(s): m = 0x80 i, j, k, t = 0, 0, 0, 0 while i < L: - if ord(s[i >> 3]) & m: + if s[i >> 3] & m: t |= 1 j += 1 m >>= 1 @@ -380,7 +401,7 @@ def dh1080_b64decode(s): L = len(s) if L < 2: raise ValueError - for i in reversed(range(L - 1)): + for i in reversed(list(range(L - 1))): if buf[ord(s[i])] == 0: L -= 1 else: @@ -425,7 +446,7 @@ def dh1080_b64decode(s): else: break k += 1 - return ''.join(map(chr, d[0:i - 1])) + return bytes(d[0:i - 1]) def dh_validate_public(public, q, p): @@ -443,7 +464,7 @@ def __init__(self): bits = 1080 while True: - self.private = bytes2int(urandom(bits / 8)) + self.private = bytes2int(urandom(bits // 8)) self.public = pow(g_dh1080, self.private, p_dh1080) if 2 <= self.public <= p_dh1080 - 1 and \ dh_validate_public(self.public, q_dh1080, p_dh1080) == 1: @@ -466,47 +487,35 @@ def dh1080_unpack(msg, ctx): if not msg.startswith("DH1080_"): raise ValueError - invalidmsg = "Key does not validate per RFC 2631. This check is not " \ - "performed by any DH1080 implementation, so we use the key " \ - "anyway. See RFC 2785 for more details." - if ctx.state == 0: if not msg.startswith("DH1080_INIT "): - raise MalformedError + raise ValueError ctx.state = 1 try: cmd, public_raw = msg.split(' ', 1) public = bytes2int(dh1080_b64decode(public_raw)) if not 1 < public < p_dh1080: - raise MalformedError - - if not dh_validate_public(public, q_dh1080, p_dh1080): - #print invalidmsg - pass + raise ValueError ctx.secret = pow(public, ctx.private, p_dh1080) - except: - raise MalformedError + except Exception: + raise ValueError elif ctx.state == 1: if not msg.startswith("DH1080_FINISH "): - raise MalformedError + raise ValueError ctx.state = 1 try: cmd, public_raw = msg.split(' ', 1) public = bytes2int(dh1080_b64decode(public_raw)) if not 1 < public < p_dh1080: - raise MalformedError - - if not dh_validate_public(public, q_dh1080, p_dh1080): - #print invalidmsg - pass + raise ValueError ctx.secret = pow(public, ctx.private, p_dh1080) - except: - raise MalformedError + except Exception: + raise ValueError return True @@ -520,22 +529,12 @@ def dh1080_secret(ctx): def bytes2int(b): """Variable length big endian to integer.""" - n = 0 - for p in b: - n *= 256 - n += ord(p) - return n + return int.from_bytes(b, byteorder='big') def int2bytes(n): """Integer to variable length big endian.""" - if n == 0: - return '\x00' - b = '' - while n: - b = chr(n % 256) + b - n /= 256 - return b + return n.to_bytes((n.bit_length() + 7) // 8, byteorder='big') def sha256(s): @@ -544,7 +543,7 @@ def sha256(s): ## -## END Blowfish and DH1080 Code +# END Blowfish and DH1080 Code ## # # HOOKS @@ -554,17 +553,14 @@ def fish_secure_key_cb(data, option, value): global fish_secure_key, fish_secure_cipher fish_secure_key = weechat.config_string( - weechat.config_get("fish.secure.key") - ) + weechat.config_get("fish.secure.key")) if fish_secure_key == "": fish_secure_cipher = None return weechat.WEECHAT_RC_OK if fish_secure_key[:6] == "${sec.": - decrypted = weechat.string_eval_expression( - fish_secure_key, {}, {}, {} - ) + decrypted = weechat.string_eval_expression(fish_secure_key, {}, {}, {}) if decrypted: fish_secure_cipher = Blowfish(decrypted) return weechat.WEECHAT_RC_OK @@ -582,15 +578,19 @@ def fish_secure_key_cb(data, option, value): def fish_modifier_in_notice_cb(data, modifier, server_name, string): global fish_DH1080ctx, fish_keys, fish_cyphers + if type(string) is bytes: + return string + match = re.match( - r"^(:(.*?)!.*? NOTICE (.*?) :)((DH1080_INIT |DH1080_FINISH |\+OK |mcps )?.*)$", + r"^((?:@[^ ]* )?:(.*?)!.*? NOTICE (.*?) :)" + r"((DH1080_INIT |DH1080_FINISH |\+OK |mcps )?.*)$", string) - #match.group(0): message - #match.group(1): msg without payload - #match.group(2): source - #match.group(3): target - #match.group(4): msg - #match.group(5): DH1080_INIT |DH1080_FINISH + # match.group(0): message + # match.group(1): msg without payload + # match.group(2): source + # match.group(3): target + # match.group(4): msg + # match.group(5): "DH1080_INIT "|"DH1080_FINISH "|"+OK "|"mcps " if not match or not match.group(5): return string @@ -607,7 +607,7 @@ def fish_modifier_in_notice_cb(data, modifier, server_name, string): fish_announce_unencrypted(buffer, target) return string - fish_alert(buffer, "Key exchange for %s sucessful" % target) + fish_alert(buffer, "Key exchange for %s successful" % target) fish_keys[targetl] = dh1080_secret(fish_DH1080ctx[targetl]) if targetl in fish_cyphers: @@ -644,17 +644,25 @@ def fish_modifier_in_notice_cb(data, modifier, server_name, string): fish_announce_unencrypted(buffer, target) return string - if targetl not in fish_cyphers: - b = Blowfish(fish_keys[targetl]) - fish_cyphers[targetl] = b - else: - b = fish_cyphers[targetl] + key = fish_keys[targetl] - clean = blowcrypt_unpack(match.group(4), b) + try: + if targetl not in fish_cyphers: + b = Blowfish(key) + fish_cyphers[targetl] = b + else: + b = fish_cyphers[targetl] - fish_announce_encrypted(buffer, target) + clean = blowcrypt_unpack(match.group(4), b, key) - return "%s%s" % (match.group(1), fish_msg_w_marker(clean)) + fish_announce_encrypted(buffer, target) + + return b"%s%s" % ( + match.group(1).encode(), fish_msg_w_marker(clean)) + except Exception as e: + fish_announce_unencrypted(buffer, target) + + raise e fish_announce_unencrypted(buffer, target) @@ -664,16 +672,20 @@ def fish_modifier_in_notice_cb(data, modifier, server_name, string): def fish_modifier_in_privmsg_cb(data, modifier, server_name, string): global fish_keys, fish_cyphers + if type(string) is bytes: + return string + match = re.match( - r"^(:(.*?)!.*? PRIVMSG (.*?) :)(\x01ACTION )?((\+OK |mcps )?.*?)(\x01)?$", + r"^((?:@[^ ]* )?:(.*?)!.*? PRIVMSG (.*?) :)(\x01ACTION )?" + r"((\+OK |mcps )?.*?)(\x01)?$", string) - #match.group(0): message - #match.group(1): msg without payload - #match.group(2): source - #match.group(3): target - #match.group(4): action - #match.group(5): msg - #match.group(6): +OK |mcps + # match.group(0): message + # match.group(1): msg without payload + # match.group(2): source + # match.group(3): target + # match.group(4): action + # match.group(5): msg + # match.group(6): "+OK "|"mcps " if not match: return string @@ -695,96 +707,132 @@ def fish_modifier_in_privmsg_cb(data, modifier, server_name, string): return string - fish_announce_encrypted(buffer, target) + key = fish_keys[targetl] - if targetl not in fish_cyphers: - b = Blowfish(fish_keys[targetl]) - fish_cyphers[targetl] = b - else: - b = fish_cyphers[targetl] - clean = blowcrypt_unpack(match.group(5), b) + try: + if targetl not in fish_cyphers: + b = Blowfish(key) + fish_cyphers[targetl] = b + else: + b = fish_cyphers[targetl] + + clean = blowcrypt_unpack(match.group(5), b, key) + + fish_announce_encrypted(buffer, target) + + if not match.group(4): + return b'%s%s' % ( + match.group(1).encode(), fish_msg_w_marker(clean)) - if not match.group(4): - return "%s%s" % (match.group(1), fish_msg_w_marker(clean)) + return b"%s%s%s\x01" % ( + match.group(1).encode(), match.group(4).encode(), + fish_msg_w_marker(clean)) + + except Exception as e: + fish_announce_unencrypted(buffer, target) - return "%s%s%s\x01" % (match.group(1), match.group(4), fish_msg_w_marker(clean)) + raise e def fish_modifier_in_topic_cb(data, modifier, server_name, string): global fish_keys, fish_cyphers - match = re.match(r"^(:.*?!.*? TOPIC (.*?) :)((\+OK |mcps )?.*)$", string) - #match.group(0): message - #match.group(1): msg without payload - #match.group(2): channel - #match.group(3): topic - #match.group(4): +OK |mcps + if type(string) is bytes: + return string + + match = re.match(r"^((?:@[^ ]* )?:.*?!.*? TOPIC (.*?) :)((\+OK |mcps )?.*)$", string) + # match.group(0): message + # match.group(1): msg without payload + # match.group(2): channel + # match.group(3): topic + # match.group(4): "+OK "|"mcps " if not match: return string target = "%s/%s" % (server_name, match.group(2)) targetl = ("%s/%s" % (server_name, match.group(2))).lower() - buffer = weechat.info_get("irc_buffer", "%s,%s" % (server_name, - match.group(2))) + buffer = weechat.info_get("irc_buffer", "%s,%s" % ( + server_name, match.group(2))) if targetl not in fish_keys or not match.group(4): fish_announce_unencrypted(buffer, target) return string - if targetl not in fish_cyphers: - b = Blowfish(fish_keys[targetl]) - fish_cyphers[targetl] = b - else: - b = fish_cyphers[targetl] - clean = blowcrypt_unpack(match.group(3), b) + key = fish_keys[targetl] - fish_announce_encrypted(buffer, target) + try: + if targetl not in fish_cyphers: + b = Blowfish(key) + fish_cyphers[targetl] = b + else: + b = fish_cyphers[targetl] + + clean = blowcrypt_unpack(match.group(3), b, key) + + fish_announce_encrypted(buffer, target) - return "%s%s" % (match.group(1), fish_msg_w_marker(clean)) + return b"%s%s" % (match.group(1).encode(), fish_msg_w_marker(clean)) + except Exception as e: + fish_announce_unencrypted(buffer, target) + + raise e def fish_modifier_in_332_cb(data, modifier, server_name, string): global fish_keys, fish_cyphers - match = re.match(r"^(:.*? 332 .*? (.*?) :)((\+OK |mcps )?.*)$", string) + if type(string) is bytes: + return string + + match = re.match(r"^((?:@[^ ]* )?:.*? 332 .*? (.*?) :)((\+OK |mcps )?.*)$", string) if not match: return string target = "%s/%s" % (server_name, match.group(2)) targetl = ("%s/%s" % (server_name, match.group(2))).lower() - buffer = weechat.info_get("irc_buffer", "%s,%s" % (server_name, - match.group(2))) + buffer = weechat.info_get("irc_buffer", "%s,%s" % ( + server_name, match.group(2))) if targetl not in fish_keys or not match.group(4): fish_announce_unencrypted(buffer, target) return string - if targetl not in fish_cyphers: - b = Blowfish(fish_keys[targetl]) - fish_cyphers[targetl] = b - else: - b = fish_cyphers[targetl] + key = fish_keys[targetl] + + try: + if targetl not in fish_cyphers: + b = Blowfish(key) + fish_cyphers[targetl] = b + else: + b = fish_cyphers[targetl] - clean = blowcrypt_unpack(match.group(3), b) + clean = blowcrypt_unpack(match.group(3), b, key) - fish_announce_encrypted(buffer, target) + fish_announce_encrypted(buffer, target) + + return b"%s%s" % (match.group(1).encode(), fish_msg_w_marker(clean)) + except Exception as e: + fish_announce_unencrypted(buffer, target) - return "%s%s" % (match.group(1), fish_msg_w_marker(clean)) + raise e def fish_modifier_out_privmsg_cb(data, modifier, server_name, string): global fish_keys, fish_cyphers + if type(string) is bytes: + return string + match = re.match(r"^(PRIVMSG (.*?) :)(.*)$", string) if not match: return string target = "%s/%s" % (server_name, match.group(2)) targetl = ("%s/%s" % (server_name, match.group(2))).lower() - buffer = weechat.info_get("irc_buffer", "%s,%s" % (server_name, - match.group(2))) + buffer = weechat.info_get("irc_buffer", "%s,%s" % ( + server_name, match.group(2))) if targetl not in fish_keys: fish_announce_unencrypted(buffer, target) @@ -796,7 +844,7 @@ def fish_modifier_out_privmsg_cb(data, modifier, server_name, string): fish_cyphers[targetl] = b else: b = fish_cyphers[targetl] - cypher = blowcrypt_pack(match.group(3), b) + cypher = blowcrypt_pack(fish_msg_wo_marker(match.group(3)).encode(), b) fish_announce_encrypted(buffer, target) @@ -806,6 +854,9 @@ def fish_modifier_out_privmsg_cb(data, modifier, server_name, string): def fish_modifier_out_topic_cb(data, modifier, server_name, string): global fish_keys, fish_cyphers + if type(string) is bytes: + return string + match = re.match(r"^(TOPIC (.*?) :)(.*)$", string) if not match: return string @@ -814,8 +865,8 @@ def fish_modifier_out_topic_cb(data, modifier, server_name, string): target = "%s/%s" % (server_name, match.group(2)) targetl = ("%s/%s" % (server_name, match.group(2))).lower() - buffer = weechat.info_get("irc_buffer", "%s,%s" % (server_name, - match.group(2))) + buffer = weechat.info_get("irc_buffer", "%s,%s" % ( + server_name, match.group(2))) if targetl not in fish_keys: fish_announce_unencrypted(buffer, target) @@ -827,13 +878,25 @@ def fish_modifier_out_topic_cb(data, modifier, server_name, string): fish_cyphers[targetl] = b else: b = fish_cyphers[targetl] - cypher = blowcrypt_pack(match.group(3), b) + cypher = blowcrypt_pack(match.group(3).encode(), b) fish_announce_encrypted(buffer, target) return "%s%s" % (match.group(1), cypher) +def fish_modifier_input_text(data, modifier, server_name, string): + if weechat.string_is_command_char(string): + return string + buffer = weechat.current_buffer() + name = weechat.buffer_get_string(buffer, "name") + target = name.replace(".", "/") + targetl = target.lower() + if targetl not in fish_keys: + return string + return "%s" % (fish_msg_w_marker(string.encode()).decode()) + + def fish_unload_cb(): fish_config_write() @@ -845,16 +908,17 @@ def fish_unload_cb(): # def fish_cmd_blowkey(data, buffer, args): - global fish_keys, fish_cyphers, fish_DH1080ctx - global fish_config_option, fish_secure_cipher + global fish_keys, fish_cyphers, fish_DH1080ctx, fish_config_option + global fish_secure_cipher if args == "" or args == "list": fish_list_keys(buffer) return weechat.WEECHAT_RC_OK - elif args =="genkey": + elif args == "genkey": fish_secure_genkey(buffer) + return weechat.WEECHAT_RC_OK argv = args.split(" ") @@ -870,10 +934,13 @@ def fish_cmd_blowkey(data, buffer, args): server_name = weechat.buffer_get_string(buffer, "localvar_server") buffer_type = weechat.buffer_get_string(buffer, "localvar_type") - # if no target user has been specified grab the one from the buffer if it is private + # if no target user has been specified grab the one from the buffer if it + # is private if argv[0] == "exchange" and len(argv) == 1 and buffer_type == "private": target_user = weechat.buffer_get_string(buffer, "localvar_channel") - elif argv[0] == "set" and (buffer_type == "private" or buffer_type == "channel") and len(argv) == 2: + elif (argv[0] == "set" and + (buffer_type == "private" or buffer_type == "channel") and + len(argv) == 2): target_user = weechat.buffer_get_string(buffer, "localvar_channel") elif len(argv) < 2: return weechat.WEECHAT_RC_ERROR @@ -887,7 +954,7 @@ def fish_cmd_blowkey(data, buffer, args): if pos > 0: argv2eol = args[pos + 1:] else: - argv2eol = args[args.find(" ") +1:] + argv2eol = args[args.find(" ") + 1:] target = "%s/%s" % (server_name, target_user) targetl = ("%s/%s" % (server_name, target_user)).lower() @@ -895,7 +962,7 @@ def fish_cmd_blowkey(data, buffer, args): if argv[0] == "set": fish_keys[targetl] = argv2eol - if target in fish_cyphers: + if targetl in fish_cyphers: del fish_cyphers[targetl] weechat.prnt(buffer, "set key for %s to %s" % (target, argv2eol)) @@ -925,12 +992,11 @@ def fish_cmd_blowkey(data, buffer, args): weechat.prnt(buffer, "Initiating DH1080 Exchange with %s" % target) fish_DH1080ctx[targetl] = DH1080Ctx() msg = dh1080_pack(fish_DH1080ctx[targetl]) - weechat.command(buffer, "/mute -all notice -server %s %s %s" % (server_name, target_user, msg)) + weechat.command(buffer, "/mute -all notice -server %s %s %s" % ( + server_name, target_user, msg)) return weechat.WEECHAT_RC_OK - - return weechat.WEECHAT_RC_ERROR @@ -938,7 +1004,6 @@ def fish_cmd_blowkey(data, buffer, args): # HELPERS # - def fish_secure(): global fish_secure_key, fish_secure_cipher @@ -946,25 +1011,36 @@ def fish_secure(): # if blank, do nothing if fish_secure_key == "": - fish_success() return # if ${sec.data.fish}, check if sec.conf is decrypted # and decrypt elif fish_secure_key[:6] == "${sec.": - decrypted = weechat.string_eval_expression( - fish_secure_key, {}, {}, {} - ) + decrypted = weechat.string_eval_expression(fish_secure_key, {}, {}, {}) if decrypted: fish_secure_cipher = Blowfish(decrypted) fish_decrypt_keys() - fish_success() return else: global SCRIPT_NAME - fish_secure_error() + message = ("\n%s%sblowkey:%s unable to recover key from sec.conf\n" + "%s%sblowkey:%s fish.py %sNOT LOADED\n" + "%s%sblowkey:%s decrypt secured data first\n" + "%s%sblowkey:%s then reload fish.py\n\n") % ( + weechat.prefix("error"), + weechat.color("underline"), + weechat.color("reset"), weechat.prefix("error"), + weechat.color("underline"), + weechat.color("reset"), weechat.color("*red"), + weechat.prefix("error"), + weechat.color("underline"), + weechat.color("reset"), weechat.prefix("error"), + weechat.color("underline"), + weechat.color("reset")) + + weechat.prnt("", "%s" % message) weechat.command(weechat.current_buffer(), "/wait 1ms /python unload %s" % SCRIPT_NAME) return @@ -974,7 +1050,6 @@ def fish_secure(): if fish_secure_key != "": fish_secure_cipher = Blowfish(fish_secure_key) fish_decrypt_keys() - fish_success() return @@ -984,48 +1059,13 @@ def fish_decrypt_keys(): fish_keys_tmp = {} for target, key in fish_keys.iteritems(): - ### DECRYPT Targets/Keys ### fish_keys_tmp[blowcrypt_unpack( - target, - fish_secure_cipher)] = blowcrypt_unpack(key, - fish_secure_cipher) + target, fish_secure_cipher)] = blowcrypt_unpack( + key, fish_secure_cipher) fish_keys = fish_keys_tmp -def fish_success(): - weechat.prnt("", - "%s%sblowkey: succesfully loaded\n" % ( - weechat.prefix("join"), - weechat.color("_green")) - ) - - -def fish_secure_error(): - """print error message if secdata not decrypted""" - - message = ("\n%s%sblowkey:%s unable to recover key from sec.conf\n" - "%s%sblowkey:%s fish.py %sNOT LOADED\n" - "%s%sblowkey:%s decrypt secured data first\n" - "%s%sblowkey:%s then reload fish.py\n\n") % ( - weechat.prefix("error"), - weechat.color("underline"), - weechat.color("reset"), - weechat.prefix("error"), - weechat.color("underline"), - weechat.color("reset"), - weechat.color("*red"), - weechat.prefix("error"), - weechat.color("underline"), - weechat.color("reset"), - weechat.prefix("error"), - weechat.color("underline"), - weechat.color("reset") - ) - - weechat.prnt("", "%s" % message) - - def fish_secure_genkey(buffer): global fish_secure_cipher, fish_config_option @@ -1033,9 +1073,7 @@ def fish_secure_genkey(buffer): # test to see if sec.conf decrypted weechat.command(buffer, "/secure set fish test") - decrypted = weechat.string_eval_expression( - "${sec.data.fish}", {}, {}, {} - ) + decrypted = weechat.string_eval_expression("${sec.data.fish}", {}, {}, {}) if decrypted == "test": weechat.config_option_set(fish_config_option["key"], @@ -1048,7 +1086,7 @@ def fish_announce_encrypted(buffer, target): global fish_encryption_announced, fish_config_option if (not weechat.config_boolean(fish_config_option['announce']) or - fish_encryption_announced.get(target)): + fish_encryption_announced.get(target)): return (server, nick) = target.split("/") @@ -1094,55 +1132,65 @@ def fish_list_keys(buffer): global fish_keys weechat.prnt(buffer, "\tFiSH Keys: form target(server): key") - - if len(fish_keys) == 0: - weechat.prnt(buffer, "NO KEYS!\n") - return - for (target, key) in sorted(fish_keys.iteritems()): + for (target, key) in sorted(fish_keys.items()): (server, nick) = target.split("/") weechat.prnt(buffer, "\t%s(%s): %s" % (nick, server, key)) def fish_msg_w_marker(msg): + marker = weechat.config_string(fish_config_option["mark_encrypted"]).encode() + if weechat.config_string(fish_config_option["mark_position"]) == "end": + return b"%s%s" % (msg, marker) + elif weechat.config_string(fish_config_option["mark_position"]) == "begin": + return b"%s%s" % (marker, msg) + else: + return msg + + +def fish_msg_wo_marker(msg): marker = weechat.config_string(fish_config_option["mark_encrypted"]) if weechat.config_string(fish_config_option["mark_position"]) == "end": - return "%s%s" % (msg, marker) + return msg[0:-len(marker)] elif weechat.config_string(fish_config_option["mark_position"]) == "begin": - return "%s%s" % (marker, msg) + return msg[len(marker):] else: return msg + + # # MAIN # if (__name__ == "__main__" and import_ok and - weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, - SCRIPT_LICENSE, SCRIPT_DESC, "fish_unload_cb", "")): + weechat.register( + SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, + SCRIPT_DESC, "fish_unload_cb", "")): - weechat.hook_command("blowkey", "Manage FiSH keys", - "[list] | [genkey] |set [-server ] [] " + weechat.hook_command( + "blowkey", "Manage FiSH keys", + "[list] | set [-server ] [] " "| remove [-server ] " - "| exchange [-server ] []", - "Add, change or remove key for target or perform DH1080\n" - "keyexchange with .\n" + "| exchange [-server ] [] " + "| genkey", + "Add, change or remove key for target or perform DH1080 key" + "exchange with .\n" "Target can be a channel or a nick.\n" "\n" "Without arguments this command lists all keys.\n" "\n" "Examples:\n" - "Set the key for a channel: /blowkey set -server freenet #blowfish key\n" + "Set the key for a channel: /blowkey set -server freenet #blowfish" + " key\n" "Remove the key: /blowkey remove #blowfish\n" "Set the key for a query: /blowkey set nick secret+key\n" - "List all keys: /blowkey\n\n" - "\n** stores keys in plaintext by default **\n\n" + "List all keys: /blowkey\n" "DH1080: /blowkey exchange nick\n" "\nPlease read the source for a note about DH1080 key exchange\n", - "list" - "|| genkey" - "|| set %(irc_channel)|%(nicks)|-server %(irc_servers) %- " + "list || set %(irc_channel)|%(nicks)|-server %(irc_servers) %- " "|| remove %(irc_channel)|%(nicks)|-server %(irc_servers) %- " - "|| exchange %(nick)|-server %(irc_servers) %-", + "|| exchange %(nick)|-server %(irc_servers) %-" + "|| genkey", "fish_cmd_blowkey", "") fish_config_init() @@ -1153,6 +1201,13 @@ def fish_msg_w_marker(msg): weechat.hook_modifier("irc_in_privmsg", "fish_modifier_in_privmsg_cb", "") weechat.hook_modifier("irc_in_topic", "fish_modifier_in_topic_cb", "") weechat.hook_modifier("irc_in_332", "fish_modifier_in_332_cb", "") - weechat.hook_modifier("irc_out_privmsg", "fish_modifier_out_privmsg_cb", "") + weechat.hook_modifier( + "irc_out_privmsg", "fish_modifier_out_privmsg_cb", "") weechat.hook_modifier("irc_out_topic", "fish_modifier_out_topic_cb", "") + weechat.hook_modifier( + "input_text_for_buffer", "fish_modifier_input_text", "") weechat.hook_config("fish.secure.key", "fish_secure_key_cb", "") +elif (__name__ == "__main__" and len(sys.argv) == 3): + key = sys.argv[1] + msg = sys.argv[2] + print(blowcrypt_unpack(msg, Blowfish(key), key)) diff --git a/python/force_nick.py b/python/force_nick.py new file mode 100644 index 00000000..33af91ca --- /dev/null +++ b/python/force_nick.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2015-2018 by Simmo Saan +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# +# History: +# +# 2018-06-19, Simmo Saan +# version 0.5: ignore channel modes arguments +# 2015-08-07, Simmo Saan +# version 0.4: option for invite-only channels +# 2015-08-07, Simmo Saan +# version 0.3: options to control risky channel cycling +# 2015-07-03, Simmo Saan +# version 0.2: ability to rejoin passworded channels +# 2015-07-01, Simmo Saan +# version 0.1: initial script +# + +""" +Force nick change on channels which disallow it +""" + +from __future__ import print_function + +SCRIPT_NAME = "force_nick" +SCRIPT_AUTHOR = "Simmo Saan " +SCRIPT_VERSION = "0.5" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = "Force nick change on channels which disallow it" + +IMPORT_OK = True + +try: + import weechat +except ImportError: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") + IMPORT_OK = False + +SETTINGS = { + "cycle_detach": ( + "off", + "automatically cycle channels which are not open in WeeChat"), + "cycle_key": ( + "on", + "automatically cycle passworded channels (+k)"), + "cycle_invite": ( + "off", + "automatically cycle invite-only channels (+i)") +} + +import re + +servers = {} + +def parse_message(signal_data): + hashtable = weechat.info_get_hashtable("irc_message_parse", {"message": signal_data}) + + # parse arguments string into usable pieces + args = hashtable["arguments"].split(":", 1) + hashtable["args"] = args[0].split() + if len(args) > 1: + hashtable["text"] = args[1] + + return hashtable + +def channel_block(server, channel): + fail = None + config_cycle = lambda opt: weechat.config_string_to_boolean(weechat.config_get_plugin("cycle_%s" % opt)) + + channels = weechat.infolist_get("irc_channel", "", "%s,%s" % (server, channel)) + if weechat.infolist_next(channels): + modes = weechat.infolist_string(channels, "modes") + if " " in modes: + modes, modes_args = modes.split(" ", 1) + + if not config_cycle("key") and weechat.infolist_string(channels, "key") != "": + fail = "cycle_key" + elif not config_cycle("invite") and "i" in modes: + fail = "cycle_invite" + elif not config_cycle("detach"): + fail = "cycle_detach" + + weechat.infolist_free(channels) + + if fail: + weechat.prnt("", "%s: won't automatically cycle %s.%s: %s" % (SCRIPT_NAME, server, channel, fail)) + else: + servers[server]["channels"].append(channel) + buffer = weechat.buffer_search("irc", server) + weechat.command(buffer, "/part %s" % channel) + weechat.command(buffer, "/nick %s" % servers[server]["nick"]) + +def nick_out_cb(data, signal, signal_data): + server = signal.split(",")[0] + parsed = parse_message(signal_data) + nick = parsed["args"][0] + + if server not in servers: # initialize new nickchange + servers[server] = {} + servers[server]["channels"] = [] + + servers[server]["nick"] = nick + + return weechat.WEECHAT_RC_OK + +def nick_in_cb(data, signal, signal_data): + server = signal.split(",")[0] + parsed = parse_message(signal_data) + mynick = weechat.info_get("irc_nick", server) + + if parsed["nick"] == mynick: # nick change worked + channels = weechat.infolist_get("irc_channel", "", server) + keys = {} + while weechat.infolist_next(channels): + keys[weechat.infolist_string(channels, "name")] = weechat.infolist_string(channels, "key") + + buffer = weechat.buffer_search("irc", server) + for channel in servers[server]["channels"]: + weechat.command(buffer, "/join -noswitch %s %s" % (channel, keys.get(channel, ""))) + + weechat.infolist_free(channels) + + servers.pop(server) + + return weechat.WEECHAT_RC_OK + +def unreal_cb(data, signal, signal_data): + server = signal.split(",")[0] + parsed = parse_message(signal_data) + + match = re.match(r"Can not change nickname while on (#\w+) \(\+N\)", parsed["text"]) + if match: + channel = match.group(1) + channel_block(server, channel) + + return weechat.WEECHAT_RC_OK + +def freenode_cb(data, signal, signal_data): + server = signal.split(",")[0] + parsed = parse_message(signal_data) + + channel_block(server, parsed["args"][2]) + + return weechat.WEECHAT_RC_OK + +if __name__ == "__main__" and IMPORT_OK: + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + weechat.hook_signal("*,irc_out1_nick", "nick_out_cb", "") + weechat.hook_signal("*,irc_in_nick", "nick_in_cb", "") + weechat.hook_signal("*,irc_in_447", "unreal_cb", "") + weechat.hook_signal("*,irc_in_435", "freenode_cb", "") + + for option, value in SETTINGS.items(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value[0]) + + weechat.config_set_desc_plugin(option, "%s (default: \"%s\")" % (value[1], value[0])) diff --git a/python/fullwidth.py b/python/fullwidth.py index 8b4dee06..d5b1263b 100644 --- a/python/fullwidth.py +++ b/python/fullwidth.py @@ -27,7 +27,7 @@ SCRIPT_NAME = "fullwidth" SCRIPT_AUTHOR = "GermainZ " -SCRIPT_VERSION = "0.1" +SCRIPT_VERSION = "0.1.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = ("Convert text to its fullwidth equivalent and send it " "to buffer.") @@ -50,7 +50,9 @@ def cb_fullwidth_cmd(data, buf, args): args = args.decode("utf-8") for char in list(args): ord_char = ord(char) - if ord_char >= 32 and ord_char <= 126: + if ord_char == 32: + char = unichr(12288) + elif ord_char > 32 and ord_char <= 126: char = unichr(ord_char + 65248) chars.append(char) send(buf, ''.join(chars)) diff --git a/python/giphy.py b/python/giphy.py new file mode 100644 index 00000000..a25ec49a --- /dev/null +++ b/python/giphy.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# Insert a giphy URL based on a command and search +# Use giphys random, search and translate from weechat +# Usage: /giphy search Search Term +# Usage: /giphy msg message +# Usage: /giphy random Search Term +# Usage: /gipgy Search Term +# +# History: +# +# 2018-10-14, butlerx +# Version 1.0.2: clean up code +# 2017-04-19, butlerx +# Version 1.0.1: remove + from message +# 2017-04-18, butlerx +# Version 1.0.0: initial version +# + +from __future__ import absolute_import + +from requests import get + +from weechat import WEECHAT_RC_OK, command, hook_command, register + +SCRIPT_NAME = "giphy" +SCRIPT_AUTHOR = "butlerx " +SCRIPT_VERSION = "1.0.2" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = "Insert giphy gif" + + +def giphy(data, buf, args): + """ Parse args to decide what api to use """ + search_string = args.split() + arg = search_string.pop(0) + search_string = "+".join(search_string) + results = ( + api_request("search", {"limit": 1, "q": search_string}) + if arg == "search" + else api_request("translate", {"s": search_string}) + if arg == "msg" + else api_request("random", {"tag": search_string}) + if arg == "random" + else api_request("random", {"tag": "+".join([arg, search_string])}) + ) + command( + buf, "giphy {} -- {}".format(search_string.replace("+", " ").strip(), results) + ) + return WEECHAT_RC_OK + + +def api_request(method, params): + """Query giphy api for search""" + try: + params["api_key"] = "dc6zaTOxFJmzC" + response = get("http://api.giphy.com/v1/gifs/{}".format(method), params=params) + data = response.json()["data"] + data = data[0] if isinstance(data, list) else data + return ( + data["images"]["original"]["url"] + if "image_url" not in data + else data["image_url"] + ) + except TypeError: + return "No GIF good enough" + + +if __name__ == "__main__": + if register( + SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "" + ): + hook_command(SCRIPT_NAME, SCRIPT_DESC, "", "", "", SCRIPT_NAME, "") diff --git a/python/glitter.py b/python/glitter.py new file mode 100644 index 00000000..b2c84b6b --- /dev/null +++ b/python/glitter.py @@ -0,0 +1,25 @@ +import weechat, re + +SCRIPT_NAME = "glitter" +SCRIPT_AUTHOR = "jotham.read@gmail.com" +SCRIPT_VERSION = "0.1" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = "Replaces ***text*** you write with rainbow text" + +if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + weechat.hook_command_run("/input return", "command_run_input", "") + +glitter_pat = re.compile("\*\*\*([^\*]+)\*\*\*") +def glitter_it(match): + lut = ("13","4","8","9","11","12") # len=6 + text = match.group(1) + return "".join(["\03"+lut[i%6]+text[i] for i in range(len(text))]) + "\03" + +def command_run_input(data, buffer, command): + if command == "/input return": + input = weechat.buffer_get_string(buffer, 'input') + if input.startswith('/set '): # Skip modification of settings + return weechat.WEECHAT_RC_OK + input = glitter_pat.sub(glitter_it, input) + weechat.buffer_set(buffer, 'input', input) + return weechat.WEECHAT_RC_OK diff --git a/python/gnome_screensaver_away.py b/python/gnome_screensaver_away.py index 8117e3c1..1761fadb 100644 --- a/python/gnome_screensaver_away.py +++ b/python/gnome_screensaver_away.py @@ -25,6 +25,9 @@ # - Allow configuration of message and poll time # - Tolerate gnome-shell crashes # +# 2021-04-05: Sébastien Helleu +# 0.2.1 : - Add missing call to infolist_free +# # Contributions welcome at: # https://github.com/grdryn/weechat-gnome-screensaver-away @@ -39,7 +42,7 @@ SCRIPT_NAME = 'gnome-screensaver-away' SCRIPT_AUTHOR = 'Gerard Ryan ' -SCRIPT_VERSION = '0.2.0' +SCRIPT_VERSION = '0.2.1' SCRIPT_LICENSE = 'GPLv3+' SCRIPT_DESC = 'Set away status based on GNOME ScreenSaver status' @@ -55,7 +58,7 @@ def get_poll_interval_safely(): try: poll_interval = int(weechat.config_get_plugin('poll_interval')) except ValueError: - weechat.println('poll_interval is not an int, falling back to default') + weechat.prnt('', 'poll_interval is not an int, falling back to default') return poll_interval @@ -72,6 +75,8 @@ def check_away_status(): is_away = bool(weechat.infolist_integer(irc_servers, "is_away")) away = (is_away, is_away_by_me) + weechat.infolist_free(irc_servers) + return away def check_screensaver_status(data, remaining_calls): diff --git a/python/go.py b/python/go.py index 509cfb8b..c4f7f8ed 100644 --- a/python/go.py +++ b/python/go.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2009-2014 Sébastien Helleu +# Copyright (C) 2009-2023 Sébastien Helleu # Copyright (C) 2010 m4v # Copyright (C) 2011 stfn # @@ -21,8 +21,27 @@ # # History: # +# 2024-05-30, Sébastien Helleu : +# version 3.0.1: refresh buffer input at the end of search +# 2024-05-30, Sébastien Helleu : +# version 3.0: refresh immediately buffer input when /go command is executed +# (needed for WeeChat >= 4.3.0) +# 2023-06-21, Sébastien Helleu : +# version 2.9: add option "min_chars" +# 2023-01-08, Sébastien Helleu : +# version 2.8: send buffer pointer with signal "input_text_changed" +# 2021-05-25, Tomáš Janoušek : +# version 2.7: add new option to prefix short names with server names +# 2019-07-11, Simmo Saan +# version 2.6: fix detection of "/input search_text_here" +# 2017-04-01, Sébastien Helleu : +# version 2.5: add option "buffer_number" +# 2017-03-02, Sébastien Helleu : +# version 2.4: fix syntax and indentation error +# 2017-02-25, Simmo Saan +# version 2.3: fix fuzzy search breaking buffer number search display # 2016-01-28, ylambda -# version 2.2: add option fuzzy_search +# version 2.2: add option "fuzzy_search" # 2015-11-12, nils_2 # version 2.1: fix problem with buffer short_name "weechat", using option # "use_core_instead_weechat", see: @@ -38,7 +57,7 @@ # version 1.8: fix jump to non-active merged buffers (jump with buffer name # instead of number) # 2012-01-03 nils_2 -# version 1.7: add option use_core_instead_weechat +# version 1.7: add option "use_core_instead_weechat" # 2012-01-03, Sébastien Helleu : # version 1.6: make script compatible with Python 3.x # 2011-08-24, stfn : @@ -52,9 +71,9 @@ # version 1.2: use high priority for hooks to prevent conflict with other # plugins/scripts (WeeChat >= 0.3.4 only) # 2010-03-25, Elián Hanisch : -# version 1.1: use a space for match the end of a string +# version 1.1: use a space to match the end of a string # 2009-11-16, Sébastien Helleu : -# version 1.0: add new option for displaying short names +# version 1.0: add new option to display short names # 2009-06-15, Sébastien Helleu : # version 0.9: fix typo in /help go with command /key # 2009-05-16, Sébastien Helleu : @@ -86,7 +105,7 @@ SCRIPT_NAME = 'go' SCRIPT_AUTHOR = 'Sébastien Helleu ' -SCRIPT_VERSION = '2.2' +SCRIPT_VERSION = '3.0.1' SCRIPT_LICENSE = 'GPL3' SCRIPT_DESC = 'Quick jump to buffers' @@ -98,13 +117,19 @@ import weechat except ImportError: print('This script must be run under WeeChat.') - print('Get WeeChat now at: http://www.weechat.org/') + print('Get WeeChat now at: https://weechat.org/') IMPORT_OK = False import re # script options SETTINGS = { + 'auto_jump': ( + 'off', + 'automatically jump to buffer when it is uniquely selected'), + 'buffer_number': ( + 'on', + 'display buffer number'), 'color_number': ( 'yellow,magenta', 'color for buffer number (not selected)'), @@ -123,12 +148,21 @@ 'color_name_highlight_selected': ( 'red,brown', 'color for highlight in a selected buffer name'), + 'fuzzy_search': ( + 'off', + 'search buffer matches using approximation'), 'message': ( 'Go to: ', 'message to display before list of buffers'), + 'min_chars': ( + '0', + 'Minimum chars to search and display list of matching buffers'), 'short_name': ( 'off', 'display and search in short names instead of buffer name'), + 'short_name_server': ( + 'off', + 'prefix short names with server names for search and display'), 'sort': ( 'number,beginning', 'comma-separated list of keys to sort buffers ' @@ -141,12 +175,6 @@ 'use_core_instead_weechat': ( 'off', 'use name "core" instead of "weechat" for core buffer'), - 'auto_jump': ( - 'off', - 'automatically jump to buffer when it is uniquely selected'), - 'fuzzy_search': ( - 'off', - 'search buffer matches using approximation'), } # hooks management @@ -192,6 +220,7 @@ def go_unhook_all(): go_unhook_one('modifier') for hook in HOOK_COMMAND_RUN: go_unhook_one(hook) + weechat.bar_item_update('input_text') def go_hook_all(): @@ -211,6 +240,7 @@ def go_hook_all(): if 'modifier' not in hooks: hooks['modifier'] = weechat.hook_modifier( 'input_text_display_with_cursor', 'go_input_modifier', '') + weechat.bar_item_update('input_text') def go_start(buf): @@ -306,9 +336,14 @@ def go_matching_buffers(strinput): strinput = strinput.lower() infolist = weechat.infolist_get('buffer', '', '') while weechat.infolist_next(infolist): + pointer = weechat.infolist_pointer(infolist, 'pointer') short_name = weechat.infolist_string(infolist, 'short_name') + server = weechat.buffer_get_string(pointer, 'localvar_server') if go_option_enabled('short_name'): - name = weechat.infolist_string(infolist, 'short_name') + if go_option_enabled('short_name_server') and server: + name = server + '.' + short_name + else: + name = short_name else: name = weechat.infolist_string(infolist, 'name') if name == 'weechat' \ @@ -321,7 +356,6 @@ def go_matching_buffers(strinput): full_name = '%s.%s' % ( weechat.infolist_string(infolist, 'plugin_name'), weechat.infolist_string(infolist, 'name')) - pointer = weechat.infolist_pointer(infolist, 'pointer') matching = name.lower().find(strinput) >= 0 if not matching and strinput[-1] == ' ': matching = name.lower().endswith(strinput.strip()) @@ -390,6 +424,11 @@ def _sort_match_beginning(buf): def go_buffers_to_string(listbuf, pos, strinput): """Return string built with list of buffers found (matching user input).""" + try: + if len(strinput) < int(weechat.config_get_plugin('min_chars')): + return '' + except: + pass string = '' strinput = strinput.lower() for i in range(len(listbuf)): @@ -406,7 +445,8 @@ def go_buffers_to_string(listbuf, pos, strinput): weechat.color(weechat.config_get_plugin( 'color_name' + selected)), buffer_name[index2:]) - elif go_option_enabled("fuzzy_search"): + elif go_option_enabled("fuzzy_search") and \ + go_match_fuzzy(buffer_name.lower(), strinput): name = "" prev_index = -1 for char in strinput.lower(): @@ -429,10 +469,13 @@ def go_buffers_to_string(listbuf, pos, strinput): name += buffer_name[prev_index+1:] else: name = buffer_name - string += ' %s%s%s%s%s' % ( - weechat.color(weechat.config_get_plugin( - 'color_number' + selected)), - str(listbuf[i]['number']), + string += ' ' + if go_option_enabled('buffer_number'): + string += '%s%s' % ( + weechat.color(weechat.config_get_plugin( + 'color_number' + selected)), + str(listbuf[i]['number'])) + string += '%s%s%s' % ( weechat.color(weechat.config_get_plugin( 'color_name' + selected)), name, @@ -467,7 +510,7 @@ def go_input_modifier(data, modifier, modifier_data, string): def go_command_run_input(data, buf, command): """Function called when a command "/input xxx" is run.""" global buffers, buffers_pos - if command == '/input search_text' or command.find('/input jump') == 0: + if command.startswith('/input search_text') or command.startswith('/input jump'): # search text or jump to another buffer is forbidden now return weechat.WEECHAT_RC_OK_EAT elif command == '/input complete_next': @@ -476,7 +519,7 @@ def go_command_run_input(data, buf, command): if buffers_pos >= len(buffers): buffers_pos = 0 weechat.hook_signal_send('input_text_changed', - weechat.WEECHAT_HOOK_SIGNAL_STRING, '') + weechat.WEECHAT_HOOK_SIGNAL_POINTER, buf) return weechat.WEECHAT_RC_OK_EAT elif command == '/input complete_previous': # choose previous buffer in list @@ -484,7 +527,7 @@ def go_command_run_input(data, buf, command): if buffers_pos < 0: buffers_pos = len(buffers) - 1 weechat.hook_signal_send('input_text_changed', - weechat.WEECHAT_HOOK_SIGNAL_STRING, '') + weechat.WEECHAT_HOOK_SIGNAL_POINTER, buf) return weechat.WEECHAT_RC_OK_EAT elif command == '/input return': # switch to selected buffer (if any) diff --git a/python/greentext.py b/python/greentext.py new file mode 100644 index 00000000..8a1a6fcd --- /dev/null +++ b/python/greentext.py @@ -0,0 +1,78 @@ +# # Weechat-Greentext +# Version: v1 +# Min WeeChat +# version tested: 3.8 +# Author: AGVXOV +# Contact: agvxov@gmail.com +# Project Home: https://github.com/agvxov/weechat-greentext +# This script is Public Domain. +# +# Weechat script for applying imageboard formatting to messages. The following are supported: +# + greentext +# + purpletext +# + redtext +# +# Both inbound and outbound messages are colored. +# Since the coloring uses IRC color codes, +# outbound greentexting will be visible to both you and your friends. +# +import weechat +import re + +SCRIPT_NAME = "greentext" +SCRIPT_AUTHOR = "AGVXOV" +SCRIPT_VERSION = "1" +SCRIPT_LICENSE = "PD" +SCRIPT_DESC = "Colorize imageboard-style text formatting." + +greentext_re = re.compile("^\s*>.*$") +purpletext_re = re.compile("^\s*<.*$") +redtext_re = re.compile("^.*(==.*==).*$") + +COLOR_GREEN = chr(3) + str(3) +COLOR_PURPLE = chr(3) + str(6) +COLOR_RED = chr(3) + str(4) + chr(2) +COLOR_END = chr(3) + str(0) + +def hi_greentext(modifier, s): + if greentext_re.search(s): + if modifier == 'irc_out1_PRIVMSG': + s = COLOR_GREEN + s + else: + s = weechat.color("green") + s + return s + +def hi_purpletext(modifier, s): + if purpletext_re.search(s): + if modifier == 'irc_out1_PRIVMSG': + s = COLOR_PURPLE + s + else: + s = weechat.color("magenta") + s + return s + +def hi_redtext(modifier, s): + if redtext_re.search(s): + if modifier == 'irc_out1_PRIVMSG': + m = redtext_re.search(s) + s = s[:m.start(1)] + COLOR_RED + m.group(1) + COLOR_END + s[m.end(1):] + else: + s = weechat.color("red") + s + return s + +def hi(data, modifier, modifier_data, s): + msg = weechat.info_get_hashtable('irc_message_parse', {'message': s}) + r = msg["text"] + r = hi_greentext(modifier, r) + r = hi_purpletext(modifier, r) + r = hi_redtext(modifier, r) + r = s[:-len(msg["text"])] + r + return r + +def main(): + if not weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + return + weechat.hook_modifier('irc_in2_privmsg', 'hi', '') + weechat.hook_modifier('irc_out1_privmsg', 'hi', '') + +if __name__ == "__main__": + main() diff --git a/python/grep.py b/python/grep.py index 30bf2a6c..b6751225 100644 --- a/python/grep.py +++ b/python/grep.py @@ -53,6 +53,9 @@ # It can be used for force or disable background process, using '0' forces to always grep in # background, while using '' (empty string) will disable it. # +# * plugins.var.python.grep.timeout_secs: +# Timeout (in seconds) for background grepping. +# # * plugins.var.python.grep.default_tail_head: # Config option for define default number of lines returned when using --head or --tail options. # Can be overriden in the command with --number option. @@ -66,6 +69,34 @@ # # History: # +# 2022-11-11, anonymous2ch +# version 0.8.6: ignore utf-8 decoding errors +# +# 2021-05-02, Sébastien Helleu +# version 0.8.5: add compatibility with WeeChat >= 3.2 (XDG directories) +# +# 2020-10-11, Thom Wiggers +# version 0.8.4: Python3 compatibility fix +# +# 2020-05-06, Dominique Martinet and hexa- +# version 0.8.3: more python3 compatibility fixes... +# +# 2019-06-30, dabbill +# and Sébastien Helleu +# version 0.8.2: make script compatible with Python 3 +# +# 2018-04-10, Sébastien Helleu +# version 0.8.1: fix infolist_time for WeeChat >= 2.2 (WeeChat returns a long +# integer instead of a string) +# +# 2017-09-20, mickael9 +# version 0.8: +# * use weechat 1.5+ api for background processing (old method was unsafe and buggy) +# * add timeout_secs setting (was previously hardcoded to 5 mins) +# +# 2017-07-23, Sébastien Helleu +# version 0.7.8: fix modulo by zero when nick is empty string +# # 2016-06-23, mickael9 # version 0.7.7: fix get_home function # @@ -102,9 +133,9 @@ # * supress highlights when printing in grep buffer # # 2010-10-06 -# version 0.6.7: by xt +# version 0.6.7: by xt # * better temporary file: -# use tempfile.mkstemp. to create a temp file in log dir, +# use tempfile.mkstemp. to create a temp file in log dir, # makes it safer with regards to write permission and multi user # # 2010-04-08 @@ -195,7 +226,12 @@ ### from os import path -import sys, getopt, time, os, re, tempfile +import sys, getopt, time, os, re + +try: + import cPickle as pickle +except ImportError: + import pickle try: import weechat @@ -206,20 +242,21 @@ SCRIPT_NAME = "grep" SCRIPT_AUTHOR = "Elián Hanisch " -SCRIPT_VERSION = "0.7.7" +SCRIPT_VERSION = "0.8.6" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Search in buffers and logs" SCRIPT_COMMAND = "grep" ### Default Settings ### settings = { -'clear_buffer' : 'off', -'log_filter' : '', -'go_to_buffer' : 'on', -'max_lines' : '4000', -'show_summary' : 'on', -'size_limit' : '2048', -'default_tail_head' : '10', + 'clear_buffer' : 'off', + 'log_filter' : '', + 'go_to_buffer' : 'on', + 'max_lines' : '4000', + 'show_summary' : 'on', + 'size_limit' : '2048', + 'default_tail_head' : '10', + 'timeout_secs' : '300', } ### Class definitions ### @@ -238,14 +275,14 @@ def __setitem__(self, key, value): def get_matches_count(self): """Return the sum of total matches stored.""" if dict.__len__(self): - return sum(map(lambda L: L.matches_count, self.itervalues())) + return sum(map(lambda L: L.matches_count, self.values())) else: return 0 def __len__(self): """Return the sum of total lines stored.""" if dict.__len__(self): - return sum(map(len, self.itervalues())) + return sum(map(len, self.values())) else: return 0 @@ -253,7 +290,7 @@ def __str__(self): """Returns buffer count or buffer name if there's just one stored.""" n = len(self.keys()) if n == 1: - return self.keys()[0] + return list(self.keys())[0] elif n > 1: return '%s logs' %n else: @@ -261,18 +298,18 @@ def __str__(self): def items(self): """Returns a list of items sorted by line count.""" - items = dict.items(self) + items = list(dict.items(self)) items.sort(key=lambda i: len(i[1])) return items def items_count(self): """Returns a list of items sorted by match count.""" - items = dict.items(self) + items = list(dict.items(self)) items.sort(key=lambda i: i[1].matches_count) return items def strip_separator(self): - for L in self.itervalues(): + for L in self.values(): L.strip_separator() def get_last_lines(self, n): @@ -281,7 +318,7 @@ def get_last_lines(self, n): if n >= total_lines: # nothing to do return - for k, v in reversed(self.items()): + for k, v in reversed(list(self.items())): l = len(v) if n > 0: if l > n: @@ -378,13 +415,15 @@ def color_nick(nick): else: mode = mode_color = '' # nick color - nick_color = weechat.info_get('irc_nick_color', nick) - if not nick_color: - # probably we're in WeeChat 0.3.0 - #debug('no irc_nick_color') - color_nicks_number = config_int('weechat.look.color_nicks_number') - idx = (sum(map(ord, nick))%color_nicks_number) + 1 - nick_color = wcolor(config_string('weechat.color.chat_nick_color%02d' %idx)) + nick_color = '' + if nick: + nick_color = weechat.info_get('irc_nick_color', nick) + if not nick_color: + # probably we're in WeeChat 0.3.0 + #debug('no irc_nick_color') + color_nicks_number = config_int('weechat.look.color_nicks_number') + idx = (sum(map(ord, nick))%color_nicks_number) + 1 + nick_color = wcolor(config_string('weechat.color.chat_nick_color%02d' %idx)) return ''.join((prefix_c, prefix, mode_color, mode, nick_color, nick, suffix_c, suffix)) ### Config and value validation ### @@ -419,9 +458,13 @@ def get_config_log_filter(): return [] def get_home(): - home = weechat.config_string(weechat.config_get('logger.file.path')) - home = home.replace('%h', weechat.info_get('weechat_dir', '')) - home = path.abspath(path.expanduser(home)) + options = { + 'directory': 'data', + } + home = weechat.string_eval_path_home( + weechat.config_string(weechat.config_get('logger.file.path')), + {}, {}, options, + ) return home def strip_home(s, dir=''): @@ -464,7 +507,7 @@ def dir_list(dir, filter_list=(), filter_excludes=True, include_dir=False): return cache_dir[key] except KeyError: pass - + filter_list = filter_list or get_config_log_filter() dir_len = len(dir) if filter_list: @@ -569,7 +612,10 @@ def get_file_by_name(buffer_name): if '$server' in mask: mask = mask.replace('$server', server) # change the unreplaced vars by '*' - from string import letters + try: + from string import letters + except ImportError: + from string import ascii_letters as letters if '%' in mask: # vars for time formatting mask = mask.replace('%', '$') @@ -634,8 +680,8 @@ def make_regexp(pattern, matchcase=False): regexp = re.compile(pattern, re.IGNORECASE) else: regexp = re.compile(pattern) - except Exception, e: - raise Exception, 'Bad pattern, %s' %e + except Exception as e: + raise Exception('Bad pattern, %s' % e) return regexp def check_string(s, regexp, hilight='', exact=False): @@ -693,9 +739,9 @@ def check(s): return s else: check = lambda s: check_string(s, regexp, hilight, exact) - + try: - file_object = open(file, 'r') + file_object = open(file, 'r', errors='ignore') except IOError: # file doesn't exist return lines @@ -713,7 +759,7 @@ def check(s): before_context, after_context = after_context, before_context if before_context: - before_context_range = range(1, before_context + 1) + before_context_range = list(range(1, before_context + 1)) before_context_range.reverse() limit = tail or head @@ -777,7 +823,7 @@ def check(s): while id < after_context + offset: id += 1 try: - context_line = file_object.next() + context_line = next(file_object) _context_line = check(context_line) if _context_line: offset = id @@ -825,6 +871,10 @@ def function(infolist): prefix = string_remove_color(infolist_string(infolist, 'prefix'), '') message = string_remove_color(infolist_string(infolist, 'message'), '') date = infolist_time(infolist, 'date') + # since WeeChat 2.2, infolist_time returns a long integer + # instead of a string + if not isinstance(date, str): + date = time.strftime('%F %T', time.localtime(int(date))) return '%s\t%s\t%s' %(date, prefix, message) return function get_line = make_get_line_funcion() @@ -854,8 +904,7 @@ def check(s): check = lambda s: check_string(s, regexp, hilight, exact) if before_context: - before_context_range = range(1, before_context + 1) - before_context_range.reverse() + before_context_range = reversed(range(1, before_context + 1)) while infolist_next(infolist): line = get_line(infolist) @@ -938,104 +987,88 @@ def show_matching_lines(): elif size_limit == '': background = False + regexp = make_regexp(pattern, matchcase) + + global grep_options, log_pairs + grep_options = (head, tail, after_context, before_context, + count, regexp, hilight, exact, invert) + + log_pairs = [(strip_home(log), log) for log in search_in_files] + if not background: # run grep normally - regexp = make_regexp(pattern, matchcase) - for log in search_in_files: - log_name = strip_home(log) - matched_lines[log_name] = grep_file(log, head, tail, after_context, before_context, - count, regexp, hilight, exact, invert) + for log_name, log in log_pairs: + matched_lines[log_name] = grep_file(log, *grep_options) buffer_update() else: - # we hook a process so grepping runs in background. - #debug('on background') - global hook_file_grep, script_path, bytecode - timeout = 1000*60*5 # 5 min - - quotify = lambda s: '"%s"' %s - files_string = ', '.join(map(quotify, search_in_files)) - - global tmpFile - # we keep the file descriptor as a global var so it isn't deleted until next grep - tmpFile = tempfile.NamedTemporaryFile(prefix=SCRIPT_NAME, - dir=weechat.info_get('weechat_dir', '')) - cmd = grep_process_cmd %dict(logs=files_string, head=head, pattern=pattern, tail=tail, - hilight=hilight, after_context=after_context, before_context=before_context, - exact=exact, matchcase=matchcase, home_dir=home_dir, script_path=script_path, - count=count, invert=invert, bytecode=bytecode, filename=tmpFile.name, - python=weechat.info_get('python2_bin', '') or 'python') - - #debug(cmd) - hook_file_grep = weechat.hook_process(cmd, timeout, 'grep_file_callback', tmpFile.name) - global pattern_tmpl + global hook_file_grep, grep_stdout, grep_stderr, pattern_tmpl + grep_stdout = grep_stderr = b'' + hook_file_grep = weechat.hook_process( + 'func:grep_process', + get_config_int('timeout_secs') * 1000, + 'grep_process_cb', + '' + ) if hook_file_grep: - buffer_create("Searching for '%s' in %s worth of data..." %(pattern_tmpl, - human_readable_size(size))) + buffer_create("Searching for '%s' in %s worth of data..." % ( + pattern_tmpl, + human_readable_size(size) + )) else: buffer_update() -# defined here for commodity -grep_process_cmd = """%(python)s -%(bytecode)sc ' -import sys, cPickle, os -sys.path.append("%(script_path)s") # add WeeChat script dir so we can import grep -from grep import make_regexp, grep_file, strip_home -logs = (%(logs)s, ) -try: - regexp = make_regexp("%(pattern)s", %(matchcase)s) - d = {} - for log in logs: - log_name = strip_home(log, "%(home_dir)s") - lines = grep_file(log, %(head)s, %(tail)s, %(after_context)s, %(before_context)s, - %(count)s, regexp, "%(hilight)s", %(exact)s, %(invert)s) - d[log_name] = lines - fd = open("%(filename)s", "wb") - cPickle.dump(d, fd, -1) - fd.close() -except Exception, e: - print >> sys.stderr, e' -""" -grep_stdout = grep_stderr = '' -def grep_file_callback(filename, command, rc, stdout, stderr): - global hook_file_grep, grep_stderr, grep_stdout - global matched_lines - #debug("rc: %s\nstderr: %s\nstdout: %s" %(rc, repr(stderr), repr(stdout))) - if stdout: - grep_stdout += stdout - if stderr: - grep_stderr += stderr - if int(rc) >= 0: - - def set_buffer_error(): - grep_buffer = buffer_create() - title = weechat.buffer_get_string(grep_buffer, 'title') - title = title + ' %serror' %color_title - weechat.buffer_set(grep_buffer, 'title', title) +def grep_process(*args): + result = {} + try: + global grep_options, log_pairs + for log_name, log in log_pairs: + result[log_name] = grep_file(log, *grep_options) + except Exception as e: + result = e + + return pickle.dumps(result, 0) + +def grep_process_cb(data, command, return_code, out, err): + global grep_stdout, grep_stderr, matched_lines, hook_file_grep + + if isinstance(out, str): + out = out.encode() + grep_stdout += out + + if isinstance(err, str): + err = err.encode() + grep_stderr += err + + def set_buffer_error(message): + error(message) + grep_buffer = buffer_create() + title = weechat.buffer_get_string(grep_buffer, 'title') + title = title + ' %serror' % color_title + weechat.buffer_set(grep_buffer, 'title', title) + + if return_code == weechat.WEECHAT_HOOK_PROCESS_ERROR: + set_buffer_error("Background grep timed out") + hook_file_grep = None + return WEECHAT_RC_OK + + elif return_code >= 0: + hook_file_grep = None + if grep_stderr: + set_buffer_error(grep_stderr) + return WEECHAT_RC_OK try: - if grep_stderr: - error(grep_stderr) - set_buffer_error() - #elif grep_stdout: - #debug(grep_stdout) - elif path.exists(filename): - import cPickle - try: - #debug(file) - fd = open(filename, 'rb') - d = cPickle.load(fd) - matched_lines.update(d) - fd.close() - except Exception, e: - error(e) - set_buffer_error() - else: - buffer_update() - global tmpFile - tmpFile = None - finally: - grep_stdout = grep_stderr = '' - hook_file_grep = None + data = pickle.loads(grep_stdout) + if isinstance(data, Exception): + raise data + matched_lines.update(data) + except Exception as e: + set_buffer_error(repr(e)) + return WEECHAT_RC_OK + else: + buffer_update() + return WEECHAT_RC_OK def get_grep_file_status(): @@ -1172,7 +1205,7 @@ def format_line(s): # free matched_lines so it can be removed from memory del matched_lines - + def split_line(s): """Splits log's line 's' in 3 parts, date, nick and msg.""" global weechat_format @@ -1272,12 +1305,12 @@ def buffer_input(data, buffer, input_data): weechat.infolist_free(infolist) try: cmd_grep_parsing(input_data) - except Exception, e: - error('Argument error, %s' %e, buffer=buffer) + except Exception as e: + error('Argument error, %s' % e, buffer=buffer) return WEECHAT_RC_OK try: show_matching_lines() - except Exception, e: + except Exception as e: error(e) except NameError: error("There isn't any previous search to repeat.", buffer=buffer) @@ -1334,11 +1367,11 @@ def tmplReplacer(match): args = ' '.join(args) # join pattern for keep spaces if args: - pattern_tmpl = args + pattern_tmpl = args pattern = _tmplRe.sub(tmplReplacer, args) debug('Using regexp: %s', pattern) if not pattern: - raise Exception, 'No pattern for grep the logs.' + raise Exception('No pattern for grep the logs.') def positive_number(opt, val): try: @@ -1351,7 +1384,7 @@ def positive_number(opt, val): opt = '-' + opt else: opt = '--' + opt - raise Exception, "argument for %s must be a positive integer." %opt + raise Exception("argument for %s must be a positive integer." % opt) for opt, val in opts: opt = opt.strip('-') @@ -1410,18 +1443,18 @@ def positive_number(opt, val): tail = n def cmd_grep_stop(buffer, args): - global hook_file_grep, pattern, matched_lines, tmpFile + global hook_file_grep, pattern, matched_lines if hook_file_grep: if args == 'stop': weechat.unhook(hook_file_grep) hook_file_grep = None - s = 'Search for \'%s\' stopped.' %pattern + + s = 'Search for \'%s\' stopped.' % pattern say(s, buffer) grep_buffer = weechat.buffer_search('python', SCRIPT_NAME) if grep_buffer: weechat.buffer_set(grep_buffer, 'title', s) - del matched_lines - tmpFile = None + matched_lines = {} else: say(get_grep_file_status(), buffer) raise Exception @@ -1446,8 +1479,8 @@ def cmd_grep(data, buffer, args): # parse try: cmd_grep_parsing(args) - except Exception, e: - error('Argument error, %s' %e) + except Exception as e: + error('Argument error, %s' % e) return WEECHAT_RC_OK # find logs @@ -1493,7 +1526,7 @@ def cmd_grep(data, buffer, args): # grepping try: show_matching_lines() - except Exception, e: + except Exception as e: error(e) return WEECHAT_RC_OK @@ -1512,8 +1545,8 @@ def cmd_logs(data, buffer, args): opt = opt.strip('-') if opt in ('size', 's'): sort_by_size = True - except Exception, e: - error('Argument error, %s' %e) + except Exception as e: + error('Argument error, %s' % e) return WEECHAT_RC_OK # is there's a filter, filter_excludes should be False @@ -1536,7 +1569,7 @@ def cmd_logs(data, buffer, args): buffer = buffer_create() if get_config_boolean('clear_buffer'): weechat.buffer_clear(buffer) - file_list = zip(file_list, file_sizes) + file_list = list(zip(file_list, file_sizes)) msg = 'Found %s logs.' %len(file_list) print_line(msg, buffer, display=True) @@ -1695,7 +1728,7 @@ def delete_bytecode(): 'completion_grep_args', '') # settings - for opt, val in settings.iteritems(): + for opt, val in settings.items(): if not weechat.config_is_set_plugin(opt): weechat.config_set_plugin(opt, val) @@ -1708,7 +1741,7 @@ def delete_bytecode(): color_summary = weechat.color('lightcyan') color_delimiter = weechat.color('chat_delimiters') color_script_nick = weechat.color('chat_nick') - + # pretty [grep] script_nick = '%s[%s%s%s]%s' %(color_delimiter, color_script_nick, SCRIPT_NAME, color_delimiter, color_reset) @@ -1726,8 +1759,11 @@ def delete_bytecode(): debug = pybuffer.debugBuffer(globals(), '%s_debug' % SCRIPT_NAME) except: def debug(s, *args): - if not isinstance(s, basestring): - s = str(s) + try: + if not isinstance(s, basestring): + s = str(s) + except NameError: + pass if args: s = s %args prnt('', '%s\t%s' %(script_nick, s)) diff --git a/python/grep_filter.py b/python/grep_filter.py index 932c0473..71e707b3 100644 --- a/python/grep_filter.py +++ b/python/grep_filter.py @@ -19,6 +19,11 @@ # # History: # +# 2021-11-06, Sébastien Helleu +# version 0.11: make script compatible with WeeChat >= 3.4 +# (new parameters in function hdata_search) +# 2019-06-07, Trygve Aaberge +# version 0.10: remove newlines from command completion # 2015-10-04, Simmo Saan # version 0.9: fix text search imitation in filter # 2015-08-27, Simmo Saan @@ -47,7 +52,7 @@ SCRIPT_NAME = "grep_filter" SCRIPT_AUTHOR = "Simmo Saan " -SCRIPT_VERSION = "0.9" +SCRIPT_VERSION = "0.11" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Filter buffers automatically while searching them" @@ -60,220 +65,302 @@ IMPORT_OK = True try: - import weechat + import weechat except ImportError: - print("This script must be run under WeeChat.") - print("Get WeeChat now at: http://www.weechat.org/") - IMPORT_OK = False + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org/") + IMPORT_OK = False -import re # re.escape +import re # re.escape SETTINGS = { - "enable": ( - "off", - "enable automatically start filtering when searching"), - "bar_item": ( - "grep", - "text to show in bar item when filtering") + "enable": ("off", "enable automatically start filtering when searching"), + "bar_item": ("grep", "text to show in bar item when filtering"), } -KEYS = { - "ctrl-G": "/%s toggle" % SCRIPT_COMMAND -} - -def get_merged_buffers(ptr): - """ - Get a list of buffers which are merged with "ptr". - """ +KEYS = {"ctrl-G": "/%s toggle" % SCRIPT_COMMAND} - hdata = weechat.hdata_get("buffer") - buffers = weechat.hdata_get_list(hdata, "gui_buffers") - buffer = weechat.hdata_search(hdata, buffers, "${buffer.number} == %i" % weechat.hdata_integer(hdata, ptr, "number"), 1) - nbuffer = weechat.hdata_move(hdata, buffer, 1) - ret = [] - while buffer: - ret.append(weechat.hdata_string(hdata, buffer, "full_name")) - - if (weechat.hdata_integer(hdata, buffer, "number") == weechat.hdata_integer(hdata, nbuffer, "number")): - buffer = nbuffer - nbuffer = weechat.hdata_move(hdata, nbuffer, 1) - else: - buffer = None +def get_merged_buffers(ptr): + """ + Get a list of buffers which are merged with "ptr". + """ + + weechat_version = int(weechat.info_get("version_number", "") or 0) + + hdata = weechat.hdata_get("buffer") + buffers = weechat.hdata_get_list(hdata, "gui_buffers") + if weechat_version >= 0x03040000: + buffer = weechat.hdata_search( + hdata, + buffers, + "${buffer.number} == ${value}", + {}, + {"value": str(weechat.hdata_integer(hdata, ptr, "number"))}, + {}, + 1, + ) + else: + buffer = weechat.hdata_search( + hdata, + buffers, + "${buffer.number} == %i" + % weechat.hdata_integer(hdata, ptr, "number"), + 1, + ) + nbuffer = weechat.hdata_move(hdata, buffer, 1) + + ret = [] + while buffer: + ret.append(weechat.hdata_string(hdata, buffer, "full_name")) + + if weechat.hdata_integer( + hdata, buffer, "number" + ) == weechat.hdata_integer(hdata, nbuffer, "number"): + buffer = nbuffer + nbuffer = weechat.hdata_move(hdata, nbuffer, 1) + else: + buffer = None + + return ret - return ret def filter_exists(name): - """ - Check whether a filter named "name" exists. - """ + """ + Check whether a filter named "name" exists. + """ + + weechat_version = int(weechat.info_get("version_number", "") or 0) + + hdata = weechat.hdata_get("filter") + filters = weechat.hdata_get_list(hdata, "gui_filters") + if weechat_version >= 0x03040000: + filter = weechat.hdata_search( + hdata, + filters, + "${filter.name} == ${name}", + {}, + {"name": name}, + {}, + 1, + ) + else: + filter = weechat.hdata_search( + hdata, + filters, + "${filter.name} == %s" % name, + 1, + ) + + return bool(filter) - hdata = weechat.hdata_get("filter") - filters = weechat.hdata_get_list(hdata, "gui_filters") - filter = weechat.hdata_search(hdata, filters, "${filter.name} == %s" % name, 1) - - return bool(filter) def filter_del(name): - """ - Delete a filter named "name". - """ + """ + Delete a filter named "name". + """ + + weechat.command( + weechat.buffer_search_main(), "/mute filter del %s" % name + ) - weechat.command(weechat.buffer_search_main(), "/mute filter del %s" % name) def filter_addreplace(name, buffers, tags, regex): - """ - Add (or replace if already exists) a filter named "name" with specified argumets. - """ + """ + Add (or replace if already exists) a filter named "name" with specified argumets. + """ - if filter_exists(name): - filter_del(name) + if filter_exists(name): + filter_del(name) + + weechat.command( + weechat.buffer_search_main(), + "/mute filter add %s %s %s %s" % (name, buffers, tags, regex), + ) - weechat.command(weechat.buffer_search_main(), "/mute filter add %s %s %s %s" % (name, buffers, tags, regex)) def buffer_searching(buffer): - """ - Check whether "buffer" is in search mode. - """ + """ + Check whether "buffer" is in search mode. + """ + + hdata = weechat.hdata_get("buffer") - hdata = weechat.hdata_get("buffer") + return bool(weechat.hdata_integer(hdata, buffer, "text_search")) - return bool(weechat.hdata_integer(hdata, buffer, "text_search")) def buffer_filtering(buffer): - """ - Check whether "buffer" should be filtered. - """ + """ + Check whether "buffer" should be filtered. + """ + + local = weechat.buffer_get_string(buffer, "localvar_%s" % SCRIPT_LOCALVAR) + return {"": None, "0": False, "1": True}[local] - local = weechat.buffer_get_string(buffer, "localvar_%s" % SCRIPT_LOCALVAR) - return {"": None, "0": False, "1": True}[local] def buffer_build_regex(buffer): - """ - Build a regex according to "buffer"'s search settings. - """ + """ + Build a regex according to "buffer"'s search settings. + """ - hdata = weechat.hdata_get("buffer") - input = weechat.hdata_string(hdata, buffer, "input_buffer") - exact = weechat.hdata_integer(hdata, buffer, "text_search_exact") - where = weechat.hdata_integer(hdata, buffer, "text_search_where") - regex = weechat.hdata_integer(hdata, buffer, "text_search_regex") + hdata = weechat.hdata_get("buffer") + input = weechat.hdata_string(hdata, buffer, "input_buffer") + exact = weechat.hdata_integer(hdata, buffer, "text_search_exact") + where = weechat.hdata_integer(hdata, buffer, "text_search_where") + regex = weechat.hdata_integer(hdata, buffer, "text_search_regex") - if not regex: - input = re.escape(input) + if not regex: + input = re.escape(input) - if exact: - input = "(?-i)%s" % input + if exact: + input = "(?-i)%s" % input - filter_regex = None - if where == 1: # message - filter_regex = input - elif where == 2: # prefix - filter_regex = "%s\\t" % input - else: # prefix | message - filter_regex = input # TODO: impossible with current filter regex + filter_regex = None + if where == 1: # message + filter_regex = input + elif where == 2: # prefix + filter_regex = "%s\\t" % input + else: # prefix | message + filter_regex = input # TODO: impossible with current filter regex - return "!%s" % filter_regex + return "!%s" % filter_regex -def buffer_update(buffer): - """ - Refresh filtering in "buffer" by updating (or removing) the filter and update the bar item. - """ - hdata = weechat.hdata_get("buffer") +def buffer_update(buffer): + """ + Refresh filtering in "buffer" by updating (or removing) the filter and update the bar item. + """ - buffers = ",".join(get_merged_buffers(buffer)) - name = "%s_%s" % (SCRIPT_NAME, buffers) + hdata = weechat.hdata_get("buffer") - if buffer_searching(buffer): - if buffer_filtering(buffer): - filter_addreplace(name, buffers, "*", buffer_build_regex(buffer)) - elif not buffer_filtering(buffer) and filter_exists(name): - filter_del(name) - elif filter_exists(name): - filter_del(name) + buffers = ",".join(get_merged_buffers(buffer)) + name = "%s_%s" % (SCRIPT_NAME, buffers) - where = weechat.hdata_integer(hdata, buffer, "text_search_where") - weechat.buffer_set(buffer, "localvar_set_%s_warn" % SCRIPT_LOCALVAR, "1" if where == 3 else "0") # warn about incorrect filter + if buffer_searching(buffer): + if buffer_filtering(buffer): + filter_addreplace(name, buffers, "*", buffer_build_regex(buffer)) + elif not buffer_filtering(buffer) and filter_exists(name): + filter_del(name) + elif filter_exists(name): + filter_del(name) - weechat.bar_item_update(SCRIPT_BAR_ITEM) + where = weechat.hdata_integer(hdata, buffer, "text_search_where") + weechat.buffer_set( + buffer, + "localvar_set_%s_warn" % SCRIPT_LOCALVAR, + "1" if where == 3 else "0", + ) # warn about incorrect filter -def input_search_cb(data, signal, buffer): - """ - Handle "input_search" signal. - """ + weechat.bar_item_update(SCRIPT_BAR_ITEM) - if buffer_searching(buffer) and buffer_filtering(buffer) is None: - enable = weechat.config_string_to_boolean(weechat.config_get_plugin("enable")) - weechat.buffer_set(buffer, "localvar_set_%s" % SCRIPT_LOCALVAR, "1" if enable else "0") - weechat.buffer_set(buffer, "localvar_set_%s_warn" % SCRIPT_LOCALVAR, "0") - elif not buffer_searching(buffer): - weechat.buffer_set(buffer, "localvar_del_%s" % SCRIPT_LOCALVAR, "") - weechat.buffer_set(buffer, "localvar_del_%s_warn" % SCRIPT_LOCALVAR, "") - buffer_update(buffer) +def input_search_cb(data, signal, buffer): + """ + Handle "input_search" signal. + """ + + if buffer_searching(buffer) and buffer_filtering(buffer) is None: + enable = weechat.config_string_to_boolean( + weechat.config_get_plugin("enable") + ) + weechat.buffer_set( + buffer, + "localvar_set_%s" % SCRIPT_LOCALVAR, + "1" if enable else "0", + ) + weechat.buffer_set( + buffer, "localvar_set_%s_warn" % SCRIPT_LOCALVAR, "0" + ) + elif not buffer_searching(buffer): + weechat.buffer_set(buffer, "localvar_del_%s" % SCRIPT_LOCALVAR, "") + weechat.buffer_set( + buffer, "localvar_del_%s_warn" % SCRIPT_LOCALVAR, "" + ) + + buffer_update(buffer) + + return weechat.WEECHAT_RC_OK - return weechat.WEECHAT_RC_OK def input_text_changed_cb(data, signal, buffer): - """ - Handle "input_text_changed" signal. - """ + """ + Handle "input_text_changed" signal. + """ + + if buffer_searching(buffer) and buffer_filtering(buffer): + buffers = ",".join(get_merged_buffers(buffer)) + name = "%s_%s" % (SCRIPT_NAME, buffers) - if buffer_searching(buffer) and buffer_filtering(buffer): - buffers = ",".join(get_merged_buffers(buffer)) - name = "%s_%s" % (SCRIPT_NAME, buffers) + filter_addreplace(name, buffers, "*", buffer_build_regex(buffer)) - filter_addreplace(name, buffers, "*", buffer_build_regex(buffer)) + return weechat.WEECHAT_RC_OK - return weechat.WEECHAT_RC_OK def command_cb(data, buffer, args): - """ - Handle command. - """ + """ + Handle command. + """ - if args == "enable": - weechat.buffer_set(buffer, "localvar_set_%s" % SCRIPT_LOCALVAR, "1") - elif args == "disable": - weechat.buffer_set(buffer, "localvar_set_%s" % SCRIPT_LOCALVAR, "0") - elif args == "toggle": - weechat.buffer_set(buffer, "localvar_set_%s" % SCRIPT_LOCALVAR, "0" if buffer_filtering(buffer) else "1") - else: - pass + if args == "enable": + weechat.buffer_set(buffer, "localvar_set_%s" % SCRIPT_LOCALVAR, "1") + elif args == "disable": + weechat.buffer_set(buffer, "localvar_set_%s" % SCRIPT_LOCALVAR, "0") + elif args == "toggle": + weechat.buffer_set( + buffer, + "localvar_set_%s" % SCRIPT_LOCALVAR, + "0" if buffer_filtering(buffer) else "1", + ) + else: + pass - buffer_update(buffer) + buffer_update(buffer) + + return weechat.WEECHAT_RC_OK - return weechat.WEECHAT_RC_OK def bar_item_cb(data, item, window, buffer, extra_info): - """ - Build the bar item's content for "buffer". - """ + """ + Build the bar item's content for "buffer". + """ + + buffers = ",".join(get_merged_buffers(buffer)) + name = "%s_%s" % (SCRIPT_NAME, buffers) - buffers = ",".join(get_merged_buffers(buffer)) - name = "%s_%s" % (SCRIPT_NAME, buffers) + if filter_exists(name): + warn = int( + weechat.buffer_get_string( + buffer, "localvar_%s_warn" % SCRIPT_LOCALVAR + ) + ) - if filter_exists(name): - warn = int(weechat.buffer_get_string(buffer, "localvar_%s_warn" % SCRIPT_LOCALVAR)) + return "%s%s%s" % ( + weechat.color("input_text_not_found" if warn else "bar_fg"), + weechat.config_get_plugin("bar_item"), + weechat.color("reset"), + ) + else: + return "" - return "%s%s%s" % ( - weechat.color("input_text_not_found" if warn else "bar_fg"), - weechat.config_get_plugin("bar_item"), - weechat.color("reset")) - else: - return "" if __name__ == "__main__" and IMPORT_OK: - if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - weechat.hook_signal("input_search", "input_search_cb", "") - weechat.hook_signal("input_text_changed", "input_text_changed_cb", "") - - weechat.hook_command(SCRIPT_COMMAND, SCRIPT_DESC, -"""enable - || disable - || toggle""", -""" enable: enable {0} in current buffer + if weechat.register( + SCRIPT_NAME, + SCRIPT_AUTHOR, + SCRIPT_VERSION, + SCRIPT_LICENSE, + SCRIPT_DESC, + "", + "", + ): + weechat.hook_signal("input_search", "input_search_cb", "") + weechat.hook_signal("input_text_changed", "input_text_changed_cb", "") + + weechat.hook_command( + SCRIPT_COMMAND, + SCRIPT_DESC, + """enable || disable || toggle""", + """ enable: enable {0} in current buffer disable: disable {0} in current buffer toggle: toggle {0} in current buffer @@ -281,18 +368,22 @@ def bar_item_cb(data, item, window, buffer, extra_info): To see {0} status during search, add "{1}" item to some bar. On default configuration you can do it with: /set weechat.bar.input.items "[input_prompt]+(away),[{1}],[input_search],[input_paste],input_text" -Due to technical reasons with /filter it is not possible to exactly {0} in "pre|msg" search mode, thus the bar item is shown in warning color.""".format(SCRIPT_NAME, SCRIPT_BAR_ITEM), -"""enable - || disable - || toggle""", - "command_cb", "") +Due to technical reasons with /filter it is not possible to exactly {0} in "pre|msg" search mode, thus the bar item is shown in warning color.""".format( + SCRIPT_NAME, SCRIPT_BAR_ITEM + ), + """enable || disable || toggle""", + "command_cb", + "", + ) - weechat.bar_item_new("(extra)%s" % SCRIPT_BAR_ITEM, "bar_item_cb", "") + weechat.bar_item_new("(extra)%s" % SCRIPT_BAR_ITEM, "bar_item_cb", "") - for option, value in SETTINGS.items(): - if not weechat.config_is_set_plugin(option): - weechat.config_set_plugin(option, value[0]) + for option, value in SETTINGS.items(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value[0]) - weechat.config_set_desc_plugin(option, "%s (default: \"%s\")" % (value[1], value[0])) + weechat.config_set_desc_plugin( + option, '%s (default: "%s")' % (value[1], value[0]) + ) - weechat.key_bind("search", KEYS) + weechat.key_bind("search", KEYS) diff --git a/python/gribble.py b/python/gribble.py new file mode 100644 index 00000000..35e50163 --- /dev/null +++ b/python/gribble.py @@ -0,0 +1,115 @@ +# gribble - automatically authenticate to gribble for bitcoin otc exchange +# by Alex Fluter +# irc nick @fluter +# +# before load this script, set your gpg passphrase by +# /secure set gpg_passphrase xxxxxx + +import re +import subprocess +import urllib2 +import weechat + +NAME = "gribble" +AUTHOR = "fluter " +VERSION = "0.1" +LICENSE = "Apache" +DESCRIPTION = "Script to talk to gribble" + +gribble_channel = "#bitcoin-fr" +gribble_nick = "gribble" +ident_nick = "fluter" +options = { + # the channel name to watch to trigger this script + "channel": "#bitcoin-otc", + # the key of the secure data storing gpg passphrase + "pass_key": "gpg_passphrase" +} +gribble_buffer = None + + +hook_msg = None + +def init(): + global gribble_buffer + gribble_buffer = weechat.buffer_new(NAME, "", "", "", "") + weechat.prnt(gribble_buffer, "Options:") + for opt, val in options.iteritems(): + if not weechat.config_is_set_plugin(opt): + weechat.config_set_plugin(opt, val) + else: + options[opt] = weechat.config_get_plugin(opt) + weechat.prnt(gribble_buffer, " %s: %s" % (opt, options[opt])) + +def privmsg(server, to, msg): + buffer = weechat.info_get("irc_buffer", server) + weechat.command(buffer, "/msg %s %s" % (to, msg)) + +def join_cb(data, signal, signal_data): + dict_in = {"message": signal_data} + dict_out = weechat.info_get_hashtable("irc_message_parse", dict_in) + channel = dict_out["channel"] + if channel != options["channel"]: + return weechat.WEECHAT_RC_OK + + server = signal.split(",")[0] + nick = dict_out["nick"] + me = weechat.info_get("irc_nick", server) + if nick != me: + return weechat.WEECHAT_RC_OK + + weechat.prnt(gribble_buffer, "Channel %s joined" % channel) + hook_msg = weechat.hook_signal("*,irc_in2_PRIVMSG", "privmsg_cb", "") + weechat.prnt(gribble_buffer, "Sent eauth to %s" % gribble_nick) + privmsg(server, gribble_nick, "eauth %s" % ident_nick) + + return weechat.WEECHAT_RC_OK + +def privmsg_cb(data, signal, signal_data): + dict_in = {"message": signal_data} + dict_out = weechat.info_get_hashtable("irc_message_parse", dict_in) + if not weechat.info_get("irc_is_nick", dict_out["channel"]): + return weechat.WEECHAT_RC_OK + + server = signal.split(",")[0] + nick = dict_out["channel"] + me = weechat.info_get("irc_nick", server) + if nick != me: + return weechat.WEECHAT_RC_OK + + if dict_out["nick"] != gribble_nick: + return weechat.WEECHAT_RC_OK + + msg = dict_out["text"] + m = re.match("^.*Get your encrypted OTP from (.*)$", msg) + if m is None: + return weechat.WEECHAT_RC_OK + weechat.prnt(gribble_buffer, "Got OTP") + otp_url = m.group(1) + otp = urllib2.urlopen(otp_url).read() + # the passphrase is stored encrypted in secure data + expr = "${sec.data.%s}" % options["pass_key"] + gpg_pass = weechat.string_eval_expression(expr, "", "", "") + if gpg_pass == "": + weechat.prnt(gribble_buffer, "no gpg pass found in secure data") + return weechat.WEECHAT_RC_OK + + p = subprocess.Popen(["gpg", "--batch", "--decrypt", "--passphrase", gpg_pass], + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE) + out, err = p.communicate(otp) + if err != "": + weechat.prnt(gribble_buffer, "gpg output: " + err) + if out != "": + privmsg(server, gribble_nick, "everify %s" % out) + + return weechat.WEECHAT_RC_OK + +def main(): + init() + hook_join = weechat.hook_signal("*,irc_in2_JOIN", "join_cb", "foo") + +weechat.register(NAME, AUTHOR, VERSION, LICENSE, DESCRIPTION, "", "") +weechat.prnt("", "%s %s loaded" % (NAME, VERSION)) +main() diff --git a/python/growl.py b/python/growl.py index 396ed035..47939279 100644 --- a/python/growl.py +++ b/python/growl.py @@ -24,7 +24,7 @@ SCRIPT_NAME = 'growl' SCRIPT_AUTHOR = 'Sorin Ionescu ' -SCRIPT_VERSION = '1.0.6' +SCRIPT_VERSION = '1.0.7' SCRIPT_LICENSE = 'MIT' SCRIPT_DESC = 'Sends Growl notifications upon events.' @@ -447,10 +447,10 @@ def main(): name = "WeeChat" hostname = weechat.config_get_plugin('hostname') password = weechat.config_get_plugin('password') + base_dir = weechat.info_get("weechat_data_dir", "") \ + or weechat.info_get("weechat_dir", "") icon = 'file://{0}'.format( - os.path.join( - weechat.info_get("weechat_dir", ""), - weechat.config_get_plugin('icon'))) + os.path.join(base_dir, weechat.config_get_plugin('icon'))) notifications = [ 'Public', 'Private', diff --git a/python/gweather.py b/python/gweather.py deleted file mode 100644 index b147f675..00000000 --- a/python/gweather.py +++ /dev/null @@ -1,280 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2010 by Jani Kesänen -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -# -# Google Weather to bar item -# (This script requires WeeChat 0.3.0.) -# -# Usage: Add "gweather" to weechat.bar.status.items or other bar you like. -# Specify city: "/set plugins.var.python.gweather.city Tokyo". -# -# Formatting: "/set plugins.var.python.gweather.format %C: %D°%U, %O". -# Where: %C - city -# %D - temperature degrees -# %U - temperature unit -# %O - current condition -# -# History: -# 2011-03-11, Sebastien Helleu : -# version 0.4: get python 2.x binary for hook_process (fix problem when -# python 3.x is default python version) -# 2010-04-15, jkesanen -# version 0.3: - added output formatting -# - removed output and city color related options -# 2010-04-09, jkesanen -# version 0.2.1: - added support for different languages -# 2010-04-07, jkesanen -# version 0.2: - fetch weather using non-blocking hook_process interface -# 2010-04-06, jkesanen -# version 0.1: - initial release. -# - -import weechat - -from urllib import quote -from xml.dom import minidom -from time import time -from sys import version_info - -SCRIPT_NAME = "gweather" -SCRIPT_AUTHOR = "Jani Kesänen " -SCRIPT_VERSION = "0.4" -SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Bar item with current Google weather" - -# Script options -settings = { - # City to monitor (ex. "Tokyo", "Austin, Texas", ...) - 'city' : '', - # Language of the conditions (ex. en, ja, fi, fr, ...) - 'language' : 'en', - # Temperature units (C or F) - 'unit' : 'C', - # Update interval in minutes - 'interval' : '10', - # Timeout in seconds for fetching weather data - 'timeout' : '10', - # The color of the output - 'output_color' : 'white', - # Formatting (%C = city, %D = degrees, %U = unit, %O = condition) - 'format' : '%C: %D%U, %O', -} - -# Timestamp for the last update -last_run = 0 - -# The last city, language and format for the need of refresh -last_city = '' -last_lang = '' -last_format = '' - -# Cached copy of the last successful output -gweather_output = 'WAIT' - -gweather_hook_process = '' -gweather_stdout = '' - -# The url to Google's "unofficial" weather API -GOOGLE_WEATHER_URL = 'http://www.google.com/ig/api?weather=%s&hl=%s' - -def parse_google_weather(xml_response): - ''' - Parses weather report from Google - - This uses code from python-weather-api 0.2.2 by Eugene Kaznacheev . - - Returns: - weather_data: a dictionary of weather data that exists in XML feed. - ''' - try: - dom = minidom.parseString(xml_response) - weather_data = {} - weather_dom = dom.getElementsByTagName('weather')[0] - except: - return - - data_structure = { - 'forecast_information': ('city', 'postal_code', 'latitude_e6', 'longitude_e6', 'forecast_date', 'current_date_time', 'unit_system'), - 'current_conditions': ('condition','temp_f', 'temp_c', 'humidity', 'wind_condition', 'icon') - } - - for (tag, list_of_tags2) in data_structure.iteritems(): - tmp_conditions = {} - for tag2 in list_of_tags2: - try: - tmp_conditions[tag2] = weather_dom.getElementsByTagName(tag)[0].getElementsByTagName(tag2)[0].getAttribute('data').strip() - except IndexError: - pass - weather_data[tag] = tmp_conditions - - dom.unlink() - - return weather_data - - -def format_weather(weather_data): - ''' - Formats the weather data dictionary received from Google - - Returns: - output: a string of formatted weather data. - ''' - output = weechat.color(weechat.config_get_plugin('output_color')) + weechat.config_get_plugin('format') - output = output.replace('%C', weechat.config_get_plugin('city')) - - temp = 'N/A' - condition = 'N/A' - - if weather_data: - if len(weather_data['current_conditions']): - if weechat.config_get_plugin('unit') == 'F': - temp = weather_data['current_conditions']['temp_f'].encode('utf-8') - else: - temp = weather_data['current_conditions']['temp_c'].encode('utf-8') - - if weather_data['current_conditions'].has_key('condition'): - condition = weather_data['current_conditions']['condition'].encode('utf-8') - - output = output.replace('%D', temp) - output = output.replace('%O', condition) - output = output.replace('%U', weechat.config_get_plugin('unit')) - - output += weechat.color('reset') - - return output - - -def gweather_data_cb(data, command, rc, stdout, stderr): - ''' - Callback for the data fetching process. - ''' - global last_city, last_lang, last_run, last_format - global gweather_hook_process, gweather_stdout, gweather_output - - if rc == weechat.WEECHAT_HOOK_PROCESS_ERROR or stderr != '': - weechat.prnt('', '%sgweather: Weather information fetching failed: %s' % (\ - weechat.prefix("error"), stderr)) - return weechat.WEECHAT_RC_ERROR - - if stdout: - gweather_stdout += stdout - - if int(rc) < 0: - # Process not ready - return weechat.WEECHAT_RC_OK - - # Update status variables for succesful run - last_run = time() - last_city = weechat.config_get_plugin('city') - last_lang = weechat.config_get_plugin('language') - last_format = weechat.config_get_plugin('format') - gweather_hook_process = '' - - if not gweather_stdout: - return weechat.WEECHAT_RC_OK - - try: - # The first row should contain "content-type" from HTTP header - content_type, xml_response = gweather_stdout.split('\n', 1) - except: - # Failed to split received data in two at carridge return - weechat.prnt('', '%sgweather: Invalid data received' % (weechat.prefix("error"))) - gweather_stdout = '' - return weechat.WEECHAT_RC_ERROR - - gweather_stdout = '' - - # Determine the used character set in the response - try: - charset = content_type.split('charset=')[1] - except: - charset = 'utf-8' - - if charset.lower() != 'utf-8': - xml_response = xml_response.decode(charset).encode('utf-8') - - # Feed the respose to parser and parsed data to formatting - weather_data = parse_google_weather(xml_response) - gweather_output = format_weather(weather_data) - - # Request bar item to update to the latest "gweather_output" - weechat.bar_item_update('gweather') - - return weechat.WEECHAT_RC_OK - - -def gweather_cb(*kwargs): - ''' Callback for the Google weather bar item. ''' - global last_run, last_city, last_lang, last_format - global gweather_output, gweather_hook_process - - # Nag if user has not specified the city - if not weechat.config_get_plugin('city'): - return 'SET CITY' - - # Nag if user has not specified the language - if not weechat.config_get_plugin('language'): - return 'SET LANGUAGE' - - # Use cached copy if it is updated recently enough - if weechat.config_get_plugin('city') == last_city and \ - weechat.config_get_plugin('language') == last_lang and \ - weechat.config_get_plugin('format') == last_format and \ - (time() - last_run) < (int(weechat.config_get_plugin('interval')) * 60): - return gweather_output - - location_id, hl = map(quote, (weechat.config_get_plugin('city'), \ - weechat.config_get_plugin('language'))) - url = GOOGLE_WEATHER_URL % (location_id, hl) - - command = 'urllib2.urlopen(\'%s\')' % (url) - - if gweather_hook_process != "": - weechat.unhook(gweather_hook_process) - gweather_hook_process = '' - - # Fire up the weather informationg fetching - python2_bin = weechat.info_get("python2_bin", "") or "python" - gweather_hook_process = weechat.hook_process(\ - python2_bin + " -c \"import urllib2;\ - handler = " + command + ";\ - print handler.info().dict['content-type'];\ - print handler.read();\ - handler.close();\"", - int(weechat.config_get_plugin('timeout')) * 1000, "gweather_data_cb", "") - - # The old cached string is returned here. gweather_data_cb() will - # request a new update after the data is fetched and parsed. - return gweather_output - - -def gweather_update(*kwargs): - weechat.bar_item_update('gweather') - - return weechat.WEECHAT_RC_OK - - -if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, '', ''): - for option, default_value in settings.iteritems(): - if not weechat.config_is_set_plugin(option): - weechat.config_set_plugin(option, default_value) - - weechat.bar_item_new('gweather', 'gweather_cb', '') - weechat.bar_item_update('gweather') - weechat.hook_timer(int(weechat.config_get_plugin('interval')) * 1000 * 60, - 0, 0, 'gweather_update', '') diff --git a/python/himan.py b/python/himan.py new file mode 100644 index 00000000..dcc3753d --- /dev/null +++ b/python/himan.py @@ -0,0 +1,159 @@ +# Hi, man! and its versions are (c) 2019 by pX <@havok.org> (in #pX on EFNet) +# Hi, man! was written under GPL3 license. http://gnu.org/licenses +# The gist of the licnse: use and/or modify at your own risk! +# +# Hi, man! What have they been saying about you? +# Log highlights to a private buffer while /away (or not!) +# Hi, man! relies on words in weechat.look.highlight +# https://github.com/px-havok/weechat-himan +# +# I wrote this because my good pal narcolept wanted a highmon.pl +# that looked good with fUrlbuf (https://github.com/px-havok/weechat-furlbuf) +# +# Hi narcolept! +# +# History: +# 04.09.2019: +# v0.1: Initial release, py3-ok +# 04.11.2019: +# : Added 'notify' as an option +# : Added hook_config so don't have to reload script for changes. + +try: + import weechat as w +except Exception: + print('WeeChat (https://weechat.org/) required.') + quit() + + +SCRIPT_NAME = 'himan' +SCRIPT_AUTHOR = 'pX @ havok' +SCRIPT_VERSION = '0.1' +SCRIPT_LICENSE = 'GPL3' +SCRIPT_DESC = "What have they been saying about you?" + +OPTIONS = {'buffer_color' : ("gray", 'color of buffer name'), + 'nick_color' : ("gray", 'color of mentioners nick'), + 'notify' : ("off", 'highlight (notify) buffer if written to'), + 'only_away' : ("on", 'only log highlights while /away'), + 'outp_left' : ("<", 'character(s) left of nick'), + 'outp_left_color' : ("gray", 'color of character(s) left of nick'), + 'outp_right' : (">", 'character(s) right of buffer name'), + 'outp_right_color' : ("gray", 'color of character(s) right of nick'), + 'outp_sep' : (" / ", 'nick/buffer separator(s)'), + 'outp_sep_color' : ("gray", 'color of / buffer separator(s)'), + } + +global rst +rst = w.color('reset') + + +# ================================[ item ]=============================== +def c(color): + return w.color(color) + + +def cg(option): + return w.config_get_plugin(option) + + +def himan_buffer_create(): + + global himan_buffer + himan_buffer = w.buffer_new('himan', 'himan_input_cb', '', '', '') + w.buffer_set(himan_buffer, 'title', '-[Hi, man! v' + SCRIPT_VERSION + ']- ' + SCRIPT_DESC) + w.buffer_set(himan_buffer, 'nicklist', '0') + + # configurable option to set buffer notify on or off + w.buffer_set(himan_buffer, 'notify', '0') + if cg('notify') == 'on': + w.buffer_set(himan_buffer, 'notify', '1') + + +def checker(data, buffer, date, tags, displayed, highlight, prefix, message): + + # Do nothing if no highlight words set + if w.config_get('weechat.look.highlight') == '': + return w.WEECHAT_RC_OK + + # if away logging is on but you're not away, do nothing + if cg('only_away') == 'on' and not w.buffer_get_string(buffer, 'localvar_away'): + return w.WEECHAT_RC_OK + + if int(highlight): + + tags = tags.split(',') + nick = '' + for idx in range(len(tags)): + if 'nick_' in tags[idx]: + nick = c(cg('nick_color')) + tags[idx][5:] + rst + + outp_left = c(cg('outp_left_color')) + cg('outp_left') + rst + outp_right = c(cg('outp_right_color')) + cg('outp_right') + rst + outp_sep = c(cg('outp_sep_color')) + cg('outp_sep') + rst + buffername = c(cg('buffer_color')) + w.buffer_get_string(buffer, 'short_name') + rst + sp = ' ' + # account for ACTION (/me) + if '*' in prefix: + sp = ' * ' + + if not w.buffer_search('python', 'himan'): + himan_buffer_create() + + w.prnt(himan_buffer, outp_left + nick + outp_sep + buffername + outp_right + sp + message) + + return w.WEECHAT_RC_OK + + +# ===================[ weechat options & description ]=================== +def init_options(): + for option,value in list(OPTIONS.items()): + if not w.config_is_set_plugin(option): + w.config_set_plugin(option, value[0]) + OPTIONS[option] = value[0] + else: + OPTIONS[option] = w.config_get_plugin(option) + w.config_set_desc_plugin(option,'%s (default: "%s")' % (value[1], value[0])) + + +# dummy input bar, does nothing. +def himan_input_cb(data, buffer, input_data): + return w.WEECHAT_RC_OK + + +def timer_cb(data, remaining_calls): + w.prnt(w.current_buffer(), '%s' % data) + return w.WEECHAT_RC_OK + + +# if notify option changes, update without reloading +def notify_cb(data, option, value): + option = cg('notify') + if option == 'on': + w.buffer_set(himan_buffer, 'notify', '1') + elif option == 'off': + w.buffer_set(himan_buffer, 'notify', '0') + return w.WEECHAT_RC_OK + + +def shutdown_cb(): + global himan_buffer + himan_buffer = None + return w.WEECHAT_RC_OK + + +# ================================[ main ]=============================== +if __name__ == '__main__': + if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, 'shutdown_cb', ''): + + init_options() + + himan_buffer_create() + + w.hook_timer(2000, 0, 1, 'timer_cb', '[himan]\tHi, man! What are they saying about you?\n' + '[himan]\tHighlights will be logged to "himan" buffer\n' + '[himan]\tOptions: /fset himan') + + w.hook_config("plugins.var.python." + SCRIPT_NAME + ".notify", "notify_cb", "") + + w.hook_print('', 'notify_message', '', 0, 'checker', '') diff --git a/python/histman.py b/python/histman.py index 68486c92..a7c050fc 100644 --- a/python/histman.py +++ b/python/histman.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2012-2015 by nils_2 +# Copyright (c) 2012-2018 by nils_2 # # save and restore global and/or buffer command history # @@ -17,6 +17,20 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +# 2021-05-02, Sébastien Helleu +# 0.8.2 : add compatibility with WeeChat >= 3.2 (XDG directories) +# +# 2018-08-04: nils_2 (freenode.#weechat) +# 0.8 : add option 'save_buffer' +# : thanks catbeard for revise the help text +# +# 2018-08-01: nils_2 (freenode.#weechat) +# 0.7 : rmodifier routine removed +# : bug fixes +# +# 2017-12-14: Sébastien Helleu +# 0.6 : rename command "/autosetbuffer" by "/buffer_autoset" in example +# # 2015-04-05: nils_2 (freenode.#weechat) # 0.5 : change priority of hook_signal('buffer_opened') to 100 # @@ -49,18 +63,18 @@ SCRIPT_NAME = 'histman' SCRIPT_AUTHOR = 'nils_2 ' -SCRIPT_VERSION = '0.5' +SCRIPT_VERSION = '0.8.2' SCRIPT_LICENSE = 'GPL' SCRIPT_DESC = 'save and restore global and/or buffer command history' OPTIONS = { 'number' : ('0','number of history commands/text to save. A positive number will save from oldest to latest, a negative number will save from latest to oldest. 0 = save whole history (e.g. -10 will save the last 10 history entries'), 'pattern' : ('(.*password|.*nickserv|/quit)','a simple regex to ignore commands/text. Empty value disable pattern matching'), 'skip_double' : ('on','skip lines that already exists (case sensitive)'), - 'save' : ('all','define what should be save from history. Possible values are \"command\", \"text\", \"all\". This is a fallback option (see /help ' + SCRIPT_NAME +')'), - 'history_dir' : ('%h/history','locale cache directory for history files (\"%h\" will be replaced by WeeChat home, \"~/.weechat\" by default)'), + 'save' : ('all','define what should be saved from history. Possible values are \"command\", \"text\", \"all\". This is a fallback option (see /help ' + SCRIPT_NAME +')'), + 'history_dir' : ('%h/history','locale cache directory for history files (\"%h\" will be replaced by WeeChat data directory)'), 'save_global' : ('off','save global history, possible values are \"command\", \"text\", \"all\" or \"off\"(default: off)'), + 'save_buffer' : ('off','save buffer history from all buffers, possible values are \"on\", \"off\". Using this option, localvar from buffer will be ignored (default: off)'), 'min_length' : ('2','minimum length of command/text (default: 2)'), - 'rmodifier' : ('off','use rmodifier options to ignore commands/text (default:off)'), 'buffer_close' : ('off','save command history, when buffer will be closed (default: off)'), } @@ -72,7 +86,6 @@ # =================================[ save/restore buffer history ]================================ def save_history(): global history_list - # get buffers ptr_infolist_buffer = weechat.infolist_get('buffer','','') @@ -80,7 +93,7 @@ def save_history(): ptr_buffer = weechat.infolist_pointer(ptr_infolist_buffer,'pointer') # check for localvar_save_history - if not weechat.buffer_get_string(ptr_buffer, 'localvar_save_history'): + if not weechat.buffer_get_string(ptr_buffer, 'localvar_save_history') and OPTIONS['save_buffer'].lower() == 'off': continue plugin = weechat.buffer_get_string(ptr_buffer, 'localvar_plugin') @@ -93,9 +106,8 @@ def save_history(): weechat.infolist_free(ptr_infolist_buffer) - # global history if OPTIONS['save_global'].lower() != 'off': - get_buffer_history('') + get_buffer_history('') # buffer pointer (if not set, return global history) if len(history_list): write_history(filename_global_history) @@ -127,27 +139,29 @@ def add_buffer_line(line, ptr_buffer): add_line = 0 + # ptr_buffer empty = global history if ptr_buffer: # buffer history - save_history = weechat.buffer_get_string(ptr_buffer, 'localvar_save_history') + save_history = weechat.buffer_get_string(ptr_buffer, 'localvar_save_history').lower() if not save_history.lower() in possible_save_options: - save_history = OPTIONS['save'] + save_history = OPTIONS['save'].lower() else: # global history + save_history = OPTIONS['save_global'].lower() if not OPTIONS['save_global'].lower() in possible_save_options: - save_history = OPTIONS['save'] + save_history = OPTIONS['save'].lower() # no save option given? save nothing if save_history == '': return 0 - if save_history.lower() == 'command': - command_chars = weechat.config_string(weechat.config_get('weechat.look.command_chars')) + '/' + if save_history == 'command': + command_chars = ("/%s" % weechat.config_string(weechat.config_get('weechat.look.command_chars'))) # a valid command must have at least two chars and first and second char are not equal! if len(line) > 1 and line[0] in command_chars and line[0] != line[1]: add_line = 1 else: return 0 - elif save_history.lower() == 'text': - command_chars = weechat.config_string(weechat.config_get('weechat.look.command_chars')) + '/' + elif save_history == 'text': + command_chars = ("/%s" % weechat.config_string(weechat.config_get('weechat.look.command_chars'))) # test for "//" = text if line[0] == line[1] and line[0] in command_chars: add_line = 1 @@ -156,7 +170,7 @@ def add_buffer_line(line, ptr_buffer): return 0 else: add_line = 1 - elif save_history.lower() == 'all': + elif save_history == 'all': add_line = 1 else: # not one of given values. save nothing! return 0 @@ -176,27 +190,13 @@ def add_buffer_line(line, ptr_buffer): return 0 pattern_matching = 0 - # pattern matching for user option and rmodifier options + # pattern matching for user option if OPTIONS['pattern'] != '': filter_re=re.compile(OPTIONS['pattern'], re.I) # pattern matched if filter_re.match(line): pattern_matching = 1 - if OPTIONS['rmodifier'].lower() == 'on' and pattern_matching == 0: - ptr_infolist_options = weechat.infolist_get('option','','rmodifier.modifier.*') - if ptr_infolist_options: - while weechat.infolist_next(ptr_infolist_options): - value = weechat.infolist_string(ptr_infolist_options,'value') - pattern = re.findall(r";(.*);", value) - - filter_re=re.compile(pattern[0], re.I) - # pattern matched - if filter_re.match(line): - pattern_matching = 1 - break - weechat.infolist_free(ptr_infolist_options) - if add_line == 1 and pattern_matching == 0: return 1 return 0 @@ -205,6 +205,12 @@ def add_buffer_line(line, ptr_buffer): def read_history(filename,ptr_buffer): global_history = 0 + # get buffer_autoset_option as a fallback. could happen that the localvar is not already set to buffer + plugin = weechat.buffer_get_string(ptr_buffer, 'localvar_plugin') + name = weechat.buffer_get_string(ptr_buffer, 'localvar_name') + buffer_autoset_option = ('buffer_autoset.buffer.%s.%s.localvar_set_save_history' % (plugin,name)) + buffer_autoset_option = weechat.config_get(buffer_autoset_option) + # global history does not use buffer pointers! if filename == filename_global_history: global_history = 1 @@ -215,12 +221,11 @@ def read_history(filename,ptr_buffer): if not os.path.isfile(filename): return - # check for global history - if global_history == 0: + # check for buffer history (0, global = 1) + if global_history == 0 and OPTIONS['save_buffer'].lower() == 'off': # localvar_save_history exists for buffer? - if not ptr_buffer or not weechat.buffer_get_string(ptr_buffer, 'localvar_save_history'): + if not ptr_buffer and not buffer_autoset_option and not weechat.buffer_get_string(ptr_buffer, 'localvar_save_history'): return - hdata = weechat.hdata_get('history') if not hdata: return @@ -283,11 +288,17 @@ def write_history(filename): raise def get_filename_with_path(filename): - path = OPTIONS['history_dir'].replace("%h",weechat.info_get("weechat_dir", "")) - return os.path.join(path,filename) + options = { + 'directory': 'data', + } + path = weechat.string_eval_path_home(OPTIONS['history_dir'], {}, {}, options) + return os.path.join(path, filename) def config_create_dir(): - dir = OPTIONS['history_dir'].replace("%h",weechat.info_get("weechat_dir", "")) + options = { + 'directory': 'data', + } + dir = weechat.string_eval_path_home(OPTIONS['history_dir'], {}, {}, options) if not os.path.isdir(dir): os.makedirs(dir, mode=0o700) @@ -297,7 +308,7 @@ def create_hooks(): weechat.hook_signal('quit', 'quit_signal_cb', '') weechat.hook_signal('upgrade_ended', 'upgrade_ended_cb', '') # low priority for hook_signal('buffer_opened') to ensure that buffer_autoset hook_signal() runs first - weechat.hook_signal('100|buffer_opened', 'buffer_opened_cb', '') + weechat.hook_signal('1000|buffer_opened', 'buffer_opened_cb', '') weechat.hook_config('plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '' ) weechat.hook_signal('buffer_closing', 'buffer_closing_cb', '') @@ -308,17 +319,17 @@ def quit_signal_cb(data, signal, signal_data): return weechat.WEECHAT_RC_OK def buffer_opened_cb(data, signal, signal_data): - plugin = weechat.buffer_get_string(signal_data, 'localvar_plugin') - name = weechat.buffer_get_string(signal_data, 'localvar_name') + ptr_buffer = signal_data + plugin = weechat.buffer_get_string(ptr_buffer, 'localvar_plugin') + name = weechat.buffer_get_string(ptr_buffer, 'localvar_name') filename = get_filename_with_path('%s.%s' % (plugin,name)) - - read_history(filename,signal_data) + read_history(filename,ptr_buffer) return weechat.WEECHAT_RC_OK def buffer_closing_cb(data, signal, signal_data): if OPTIONS['buffer_close'].lower() == 'on' and signal_data: # check for localvar_save_history - if not weechat.buffer_get_string(signal_data, 'localvar_save_history'): + if not weechat.buffer_get_string(signal_data, 'localvar_save_history') and OPTIONS['save_buffer'].lower() == 'off': return weechat.WEECHAT_RC_OK plugin = weechat.buffer_get_string(signal_data, 'localvar_plugin') @@ -344,7 +355,8 @@ def histman_cmd_cb(data, buffer, args): return weechat.WEECHAT_RC_OK if argv[0].lower() == 'save': - quit_signal_cb('', '', '') + config_create_dir() + save_history() elif argv[0].lower() == 'list': weechat.command('','/set *.localvar_set_save_history') else: @@ -375,29 +387,31 @@ def toggle_refresh(pointer, name, value): version = weechat.info_get('version_number', '') or 0 weechat.hook_command(SCRIPT_NAME, SCRIPT_DESC, '[save] || [list]', - ' save: force to save command history:\n' + ' save: force to save command history\n' ' list: list local buffer variable(s)\n' '\n' - 'If you \"/quit\" WeeChat, the script will automatically save the command history to file.\n' - 'You can also force the script to save command history, when a buffer will be closed.\n' - 'If you restart WeeChat again the command history will be restored, when buffer opens again.\n' + 'If you \"/quit\" WeeChat, the script will automatically save the command history to a file.\n' + 'You can also force the script to save command history, when you close a buffer with /buffer close.\n' + 'If you restart WeeChat, the command history will be restored, when buffer opens again.\n' 'To save and restore \"global\" command history, use option \"save_global\".\n' '\n' 'The command history of a buffer will be saved \"only\", if the the local variable \"save_history\" is set.\n' - 'You will need script \"buffer_autoset.py\" to make local variabe persistent (see examples, below)!!\n' + 'You will need script \"buffer_autoset.py\" to make a local variabe persistent (see examples, below)!!\n' '\n' + 'You can use option \"save_buffer\" to save command history for *all* buffers, localvar will be ignored using\n' + 'this option.\n' 'You can use following values for local variable:\n' ' command: save commands only\n' ' text: save text only (text sent to a channel buffer)\n' ' all: save commands and text\n' '\n' 'Examples:\n' - ' save the command history manually (for example with /cron script):\n' + ' save the command history manually (for example with /cron script or with an /trigger):\n' ' /' + SCRIPT_NAME + ' save\n' ' save and restore command history for buffer #weechat on freenode (text only):\n' - ' /autosetbuffer add irc.freenode.#weechat localvar_set_save_history text\n' + ' /buffer_autoset add irc.freenode.#weechat localvar_set_save_history text\n' ' save and restore command history for weechat core buffer (commands only):\n' - ' /autosetbuffer add core.weechat localvar_set_save_history command\n', + ' /buffer_autoset add core.weechat localvar_set_save_history command\n', 'save %-' '|| list %-', 'histman_cmd_cb', '') diff --git a/python/histsearch.py b/python/histsearch.py index db935032..c3a53d44 100644 --- a/python/histsearch.py +++ b/python/histsearch.py @@ -1,4 +1,3 @@ -''' History searcher ''' # -*- coding: utf-8 -*- # # Copyright (c) 2009 by xt @@ -24,6 +23,15 @@ # (this script requires WeeChat 0.3.0 or newer) # # History: +# +# 2023-01-08, Sébastien Helleu +# version 0.7: send buffer pointer with signal "input_text_changed" +# 2019-10-31, Simmo Saan +# version 0.6: fix hook_command_run hooks not being unhooked +# 2019-07-11, Sébastien Helleu +# version 0.5: make script compatible with Python 3 +# 2019-07-11, Simmo Saan +# version 0.4: fix detection of "/input search_text_here" # 2016-02-20, Simmo Saan # version 0.3: add option to only display selected command # 2010-01-19, xt @@ -37,7 +45,7 @@ SCRIPT_NAME = "histsearch" SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "0.3" +SCRIPT_VERSION = "0.7" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Quick search in command history (think ctrl-r in bash)" SCRIPT_COMMAND = 'histsearch' @@ -80,7 +88,7 @@ "You can use completion key (commonly Tab and shift-Tab) to select " + "next/previous command in list.", "", "histsearch_cmd", "") - for option, default_value in settings.iteritems(): + for option, default_value in settings.items(): if w.config_get_plugin(option) == "": w.config_set_plugin(option, default_value) @@ -96,13 +104,14 @@ def unhook_all(): """ Unhook all """ global hook_command_run unhook_one("modifier") - map(unhook_one, hook_command_run.keys()) + for hook in hook_command_run.keys(): + unhook_one(hook) return w.WEECHAT_RC_OK def hook_all(): """ Hook command_run and modifier """ global hook_command_run, hooks - for hook, value in hook_command_run.iteritems(): + for hook, value in hook_command_run.items(): if hook not in hooks: hooks[hook] = w.hook_command_run(value[0], value[1], "") if "modifier" not in hooks: @@ -204,7 +213,7 @@ def input_modifier(data, modifier, modifier_data, string): def command_run_input(data, buffer, command): """ Function called when a command "/input xxxx" is run """ global commands, commands_pos - if command == "/input search_text" or command.find("/input jump") == 0: + if command.startswith('/input search_text') or command.startswith('/input jump'): # search text or jump to another buffer is forbidden now return w.WEECHAT_RC_OK_EAT elif command == "/input complete_next": @@ -213,7 +222,7 @@ def command_run_input(data, buffer, command): if commands_pos >= len(commands): commands_pos = 0 w.hook_signal_send("input_text_changed", - w.WEECHAT_HOOK_SIGNAL_STRING, "") + w.WEECHAT_HOOK_SIGNAL_POINTER, buffer) return w.WEECHAT_RC_OK_EAT elif command == "/input complete_previous": # choose previous buffer in list @@ -221,7 +230,7 @@ def command_run_input(data, buffer, command): if commands_pos < 0: commands_pos = len(commands) - 1 w.hook_signal_send("input_text_changed", - w.WEECHAT_HOOK_SIGNAL_STRING, "") + w.WEECHAT_HOOK_SIGNAL_POINTER, buffer) return w.WEECHAT_RC_OK_EAT elif command == "/input return": # As in enter was pressed. diff --git a/python/hl2file.py b/python/hl2file.py index c906464a..630eb328 100644 --- a/python/hl2file.py +++ b/python/hl2file.py @@ -18,6 +18,8 @@ # This script generates a file containing a formatted list of highlights to be # used by an external program like conky. # +# 2021-05-04, Sébastien Helleu +# 0.3: add compatibility with WeeChat >= 3.2 (XDG directories) # 2014-05-10, Sébastien Helleu # 0.2: change hook_print callback argument type of displayed/highlight # (WeeChat >= 1.0) @@ -36,7 +38,7 @@ name = "hl2file" author = "nesthib " -version = "0.2" +version = "0.3" license = "GPL" description = "Generates a file with highlights for external programs like conky" shutdown_function = "shutdown" @@ -70,7 +72,10 @@ def bufname(buffer): def write(my_file, content): if my_file == "": return - my_file = os.path.expanduser(my_file.replace("%h", w.info_get("weechat_dir", ""))) + options = { + 'directory': 'data', + } + my_file = w.string_eval_path_home(my_file, {}, {}, options) try: f = open(my_file, 'w') except IOError: @@ -134,8 +139,11 @@ def buffer_switch_cb(data, signal, signal_data): return w.WEECHAT_RC_OK def shutdown(): + options = { + 'directory': 'data', + } for my_file in ['output_file', 'output_active_buffer']: - filename = w.config_get_plugin(my_file).replace("%h", w.info_get("weechat_dir", "")) + filename = w.string_eval_path_home(my_file, {}, {}, options) if os.path.exists(filename): os.remove(filename) return w.WEECHAT_RC_OK diff --git a/python/i3lock_away.py b/python/i3lock_away.py new file mode 100644 index 00000000..808a4331 --- /dev/null +++ b/python/i3lock_away.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Bertrand Ciroux +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Set away status if i3lock is running +# This script a copy the slock_away.py script, by Peter A. Shevtsov. +# The only change is the detection of the i3lock process instead of the slock +# one. +# +# History: +# 2019-12-23, Christian Trenkwalder : +# version 0.2: updated to work with python3 +# +# 2017-06-07, Bertrand Ciroux : +# version 0.1: initial release +# + +from __future__ import print_function + +SCRIPT_NAME = "i3lock_away" +SCRIPT_AUTHOR = "Bertrand Ciroux " +SCRIPT_VERSION = "0.2" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = "Set away status if i3lock is running" + +SCRIPT_COMMAND = "i3lock_away" + +import_ok = True + +try: + import weechat +except ImportError: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") + import_ok = False + +try: + import subprocess +except ImportError as message: + print("Missing package(s) for %s: %s" % (SCRIPT_NAME, message)) + import_ok = False + +TIMER = None + +settings = { + 'away_message': 'Away', + 'interval': '20', # How often to check for inactivity (in seconds) + 'away': '0' +} + + +def set_back(overridable_messages): + """Removes away status for servers + where one of the overridable_messages is set""" + if (weechat.config_get_plugin('away') == '0'): + return # No need to come back again + serverlist = weechat.infolist_get('irc_server', '', '') + if serverlist: + buffers = [] + while weechat.infolist_next(serverlist): + if (weechat.infolist_string(serverlist, 'away_message') + in overridable_messages): + ptr = weechat.infolist_pointer(serverlist, 'buffer') + if ptr: + buffers.append(ptr) + weechat.infolist_free(serverlist) + for buffer in buffers: + weechat.command(buffer, "/away") + weechat.config_set_plugin('away', '0') + + +def set_away(message, overridable_messages=[]): + """Sets away status, but respectfully + (so it doesn't change already set statuses""" + if (weechat.config_get_plugin('away') == '1'): + # No need to go away again + # (this prevents some repeated messages) + return + serverlist = weechat.infolist_get('irc_server', '', '') + if serverlist: + buffers = [] + while weechat.infolist_next(serverlist): + if weechat.infolist_integer(serverlist, 'is_away') == 0: + ptr = weechat.infolist_pointer(serverlist, 'buffer') + if ptr: + buffers.append(ptr) + elif (weechat.infolist_string(serverlist, 'away_message') + in overridable_messages): + buffers.append(weechat.infolist_pointer(serverlist, 'buffer')) + weechat.infolist_free(serverlist) + for buffer in buffers: + weechat.command(buffer, "/away %s" % message) + weechat.config_set_plugin('away', '1') + + +def i3lock_away_cb(data, buffer, args): + """Callback for /i3lock_away command""" + response = { + 'msg': lambda status: weechat.config_set_plugin( + 'away_message', status) + } + if args: + words = args.strip().partition(' ') + if words[0] in response: + response[words[0]](words[2]) + else: + weechat.prnt('', "i3lock_away error: %s not a recognized command. " + "Try /help i3lock_away" % words[0]) + weechat.prnt('', "i3lock_away: away message: \"%s\"" % + weechat.config_get_plugin('away_message')) + return weechat.WEECHAT_RC_OK + + +def auto_check(data, remaining_calls): + """Callback from timer""" + check() + return weechat.WEECHAT_RC_OK + + +def check(): + """Check for existance of process and set away if it isn't there""" + pidof = subprocess.Popen("pidof i3lock", + shell=True, stdout=subprocess.PIPE) + pidof.wait() + if pidof.returncode == 0: + set_away(weechat.config_get_plugin('away_message'), []) + else: + set_back([weechat.config_get_plugin('away_message')]) + + +def check_timer(): + """Sets or unsets the timer + based on whether or not the plugin is enabled""" + global TIMER + if TIMER: + weechat.unhook(TIMER) + TIMER = weechat.hook_timer( + int(weechat.config_get_plugin('interval')) * 1000, + 0, 0, "auto_check", "") + + +if __name__ == "__main__" and import_ok: + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, + SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + for option, default_value in settings.items(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, default_value) + + weechat.hook_command(SCRIPT_COMMAND, + SCRIPT_DESC, + "msg ", + "msg: set the away message\n", + "", "i3lock_away_cb", "") + check_timer() diff --git a/python/ichatts.py b/python/ichatts.py deleted file mode 100644 index c93aad3b..00000000 --- a/python/ichatts.py +++ /dev/null @@ -1,94 +0,0 @@ -# -# Copyright (c) 2010, Chris Branch -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# History: -# 2010-07-26 -# revision 1.0 -# - -import weechat -import time - -SCRIPT_NAME = "ichatts" -SCRIPT_AUTHOR = "Chris Branch " -SCRIPT_VERSION = "1.0" -SCRIPT_LICENSE = "BSD" -SCRIPT_DESC = "iChat-style timestamps" - -settings = { - "minutes_until_timestamp" : '5', # print when no message occurs for this long - "remind_every" : '15', # print a new timestamp every X minutes if there is activity -} - -buffer_dates = {} - -def prnt_timestamp(buffer, timestamp): - weechat.prnt(buffer, '%s[%s%s%s:%s%s%s]' % - (weechat.color("chat_delimiters"), - weechat.color("chat_time"), - time.strftime('%H', time.localtime(timestamp)), - weechat.color("chat_time_delimiters"), - weechat.color("chat_time"), - time.strftime('%M', time.localtime(timestamp)), - weechat.color("chat_delimiters"))) - -def timer_cb(data, remaining_calls): - global buffer_dates - current_time = int(time.time()) - timestamp_secs = int(weechat.config_get_plugin('minutes_until_timestamp')) * 60 - # Which buffers need a timestamp printing? - for (buffer, (last_message, last_printed)) in buffer_dates.items(): - # If X minutes have elapsed since the last message, and we haven't printed anything since then. - if last_printed < last_message and current_time - last_message >= timestamp_secs: - buffer_dates[buffer] = (last_message, current_time) - prnt_timestamp(buffer, last_message) - return weechat.WEECHAT_RC_OK - -def print_cb(data, buffer, date, tags, displayed, highlight, prefix, message): - # Update buffer with date of last message - global buffer_dates - current_time = int(date) - last_printed = buffer_dates.get(buffer, (0, 0))[1] - remind_secs = int(weechat.config_get_plugin('remind_every')) * 60 - # Has it been X minutes since we last printed a timestamp? - if current_time - last_printed >= remind_secs: - last_printed = current_time - prnt_timestamp(buffer, current_time) - - buffer_dates[buffer] = (current_time, last_printed) - return weechat.WEECHAT_RC_OK - -if __name__ == "__main__": - if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, "", ""): - # Set default settings - for option, default_value in settings.iteritems(): - if not weechat.config_is_set_plugin(option): - weechat.config_set_plugin(option, default_value) - - weechat.hook_timer(60000, 60, 0, 'timer_cb', '') - weechat.hook_print('', '', '', 0, 'print_cb', '') - diff --git a/python/identica.py b/python/identica.py index 5ac25f75..090e6385 100644 --- a/python/identica.py +++ b/python/identica.py @@ -74,6 +74,10 @@ # 2011-01-18, fauno: # - Fixed error on load when no username nor password were given # +# 2020-05-09, FlashCode: +# - Add compatibility with new weechat_print modifier data +# (WeeChat >= 2.9) +# # TODO - cache json requests import weechat @@ -88,7 +92,7 @@ SCRIPT_NAME = 'identica' SCRIPT_AUTHOR = 'fauno ' -SCRIPT_VERSION = '0.4.2' +SCRIPT_VERSION = '0.4.3' SCRIPT_LICENSE = 'GPL3' SCRIPT_DESC = 'Formats identi.ca\'s bot messages' @@ -469,7 +473,16 @@ def clean (message): def parse_in (server, modifier, data, the_string): '''Parses incoming messages''' - plugin, channel, flags = data.split(';') + if data.startswith('0x'): + # WeeChat >= 2.9 + buffer, flags = data.split(';', 1) + else: + # WeeChat <= 2.8 + plugin, buffer_name, flags = data.split(';', 2) + buffer = weechat.buffer_search(plugin, buffer_name) + + channel = weechat.buffer_get_string(buffer, 'localvar_channel') + flag = flags.split(',') if channel == weechat.config_get_plugin('channel') and 'irc_privmsg' in flag: diff --git a/python/imap_status.py b/python/imap_status.py index 9e9d3c6a..f5144939 100644 --- a/python/imap_status.py +++ b/python/imap_status.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- # Copyright (c) 2009-2015 by xt -# (this script requires WeeChat 0.3.0 or newer) +# (this script requires WeeChat 0.4.2 or newer) # # History: +# 2019-01-26, nils_2@freenode +# version 0.9: make script python3 compatible +# : remove option "message_color" and "separator_color" # 2016-05-07, Sebastien Helleu : # version 0.8: add options "mailbox_color", "separator", "separator_color", # remove extra colon in bar item content, use hook_process @@ -47,7 +50,7 @@ SCRIPT_NAME = "imap_status" SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "0.8" +SCRIPT_VERSION = "0.9" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Bar item with unread imap messages count" @@ -62,26 +65,21 @@ 'hostname': '', # gmail uses imap.gmail.com 'port': '993', 'mailboxes': 'INBOX', # comma separated list of mailboxes (gmail: "Inbox") - 'message': 'Mail: ', - 'message_color': 'default', + 'message': '${color:default}Mail: ', 'mailbox_color': 'default', - 'separator': ', ', - 'separator_color': 'default', + 'separator': '${color:default}, ', 'count_color': 'default', 'interval': '5', } def string_eval_expression(text): - if WEECHAT_VERSION >= 0x00040200: - return w.string_eval_expression(text, {}, {}, {}) - return text - + return w.string_eval_expression(text, {}, {}, {}) class Imap(object): """Simple helper class for interfacing with IMAP server.""" - iRe = re.compile(r"UNSEEN (\d+)") + iRe = re.compile(br"UNSEEN (\d+)") conn = False def __init__(self): @@ -117,15 +115,12 @@ def logout(self): def imap_get_unread(data): """Return the unread count.""" - imap = Imap() - if not w.config_get_plugin('message'): output = "" else: - output = '%s%s' % ( - w.color(w.config_get_plugin('message_color')), - w.config_get_plugin('message')) + output = '%s' % ( + string_eval_expression(w.config_get_plugin('message'))) any_with_unread = False mailboxes = w.config_get_plugin('mailboxes').split(',') count = [] @@ -140,9 +135,8 @@ def imap_get_unread(data): w.color(w.config_get_plugin('count_color')), unreadCount)) imap.logout() - sep = '%s%s' % ( - w.color(w.config_get_plugin('separator_color')), - w.config_get_plugin('separator')) + sep = '%s' % ( + string_eval_expression(w.config_get_plugin('separator'))) output = output + sep.join(count) + w.color('reset') return output if any_with_unread else '' @@ -177,7 +171,7 @@ def imap_timer_cb(data, remaining_calls): if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): - for option, default_value in settings.iteritems(): + for option, default_value in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_value) diff --git a/python/infolist.py b/python/infolist.py index 471f63c7..7d88a14f 100644 --- a/python/infolist.py +++ b/python/infolist.py @@ -1,5 +1,6 @@ +# -*- coding: utf-8 -*- # -# Copyright (C) 2008-2012 Sebastien Helleu +# Copyright (C) 2008-2018 Sébastien Helleu # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,56 +19,72 @@ # Display infolist in a buffer. # # History: +# +# 2018-04-10, Sébastien Helleu : +# version 0.7: fix infolist_time for WeeChat >= 2.2 (WeeChat returns a long +# integer instead of a string), fix PEP8 errors +# 2017-10-22, nils_2 : +# version 0.6: add string_eval_expression() # 2012-10-02, nils_2 : # version 0.5: switch to infolist buffer (if exists) when command /infolist # is called with arguments, add some examples to help page -# 2012-01-03, Sebastien Helleu : +# 2012-01-03, Sébastien Helleu : # version 0.4: make script compatible with Python 3.x # 2010-01-23, m4v : # version 0.3: user can give a pointer as argument -# 2010-01-18, Sebastien Helleu : +# 2010-01-18, Sébastien Helleu : # version 0.2: use tag "no_filter" for lines displayed, fix display bug # when infolist is empty -# 2009-11-30, Sebastien Helleu : +# 2009-11-30, Sébastien Helleu : # version 0.1: first version -# 2008-12-12, Sebastien Helleu : +# 2008-12-12, Sébastien Helleu : # script creation -SCRIPT_NAME = "infolist" -SCRIPT_AUTHOR = "Sebastien Helleu " -SCRIPT_VERSION = "0.5" +SCRIPT_NAME = "infolist" +SCRIPT_AUTHOR = "Sébastien Helleu " +SCRIPT_VERSION = "0.7" SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Display infolist in a buffer" +SCRIPT_DESC = "Display infolist in a buffer" import_ok = True - try: import weechat -except: +except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") import_ok = False +try: + import time +except ImportError as message: + print('Missing package(s) for %s: %s' % (SCRIPT_NAME, message)) + import_ok = False + + infolist_buffer = "" -infolist_var_type = { "i": "int", - "s": "str", - "p": "ptr", - "t": "tim", - "b": "buf", - } +infolist_var_type = { + "i": "int", + "s": "str", + "p": "ptr", + "t": "tim", + "b": "buf", +} def infolist_buffer_set_title(buffer): # get list of infolists available - list = "" + list_infolists = "" infolist = weechat.infolist_get("hook", "", "infolist") while weechat.infolist_next(infolist): - list += " %s" % weechat.infolist_string(infolist, "infolist_name") + list_infolists += " %s" % weechat.infolist_string(infolist, + "infolist_name") weechat.infolist_free(infolist) # set buffer title weechat.buffer_set(buffer, "title", - "%s %s | Infolists:%s" % (SCRIPT_NAME, SCRIPT_VERSION, list)) + "%s %s | Infolists:%s" % ( + SCRIPT_NAME, SCRIPT_VERSION, list_infolists)) + def infolist_display(buffer, args): global infolist_var_type @@ -91,9 +108,10 @@ def infolist_display(buffer, args): item_count = 0 weechat.buffer_clear(buffer) - weechat.prnt_date_tags(buffer, 0, "no_filter", - "Infolist '%s', with pointer '%s' and arguments '%s':" % (items[0], - infolist_pointer, infolist_args)) + weechat.prnt_date_tags( + buffer, 0, "no_filter", + "Infolist '%s', with pointer '%s' and arguments '%s':" % ( + items[0], infolist_pointer, infolist_args)) weechat.prnt(buffer, "") count = 0 while weechat.infolist_next(infolist): @@ -119,15 +137,22 @@ def infolist_display(buffer, args): value = weechat.infolist_pointer(infolist, name) elif type == "t": value = weechat.infolist_time(infolist, name) + # since WeeChat 2.2, infolist_time returns a long integer + # instead of a string + if not isinstance(value, str): + str_date = time.strftime('%F %T', + time.localtime(int(value))) + value = '%d (%s)' % (value, str_date) name_end = "." * (30 - len(name)) - weechat.prnt_date_tags(buffer, 0, "no_filter", - "%s%s%s: %s%s%s %s%s%s%s%s%s" % - (prefix, name, name_end, - weechat.color("brown"), infolist_var_type[type], - weechat.color("chat"), - weechat.color("chat"), quote, - weechat.color("cyan"), value, - weechat.color("chat"), quote)) + weechat.prnt_date_tags( + buffer, 0, "no_filter", + "%s%s%s: %s%s%s %s%s%s%s%s%s" % + (prefix, name, name_end, + weechat.color("brown"), infolist_var_type[type], + weechat.color("chat"), + weechat.color("chat"), quote, + weechat.color("cyan"), value, + weechat.color("chat"), quote)) prefix = "" count += 1 if count == 0: @@ -135,6 +160,7 @@ def infolist_display(buffer, args): weechat.infolist_free(infolist) return weechat.WEECHAT_RC_OK + def infolist_buffer_input_cb(data, buffer, input_data): if input_data == "q" or input_data == "Q": weechat.buffer_close(buffer) @@ -142,12 +168,14 @@ def infolist_buffer_input_cb(data, buffer, input_data): infolist_display(buffer, input_data) return weechat.WEECHAT_RC_OK + def infolist_buffer_close_cb(data, buffer): global infolist_buffer infolist_buffer = "" return weechat.WEECHAT_RC_OK + def infolist_buffer_new(): global infolist_buffer @@ -162,34 +190,46 @@ def infolist_buffer_new(): weechat.buffer_set(infolist_buffer, "time_for_each_line", "0") weechat.buffer_set(infolist_buffer, "display", "1") + def infolist_cmd(data, buffer, args): global infolist_buffer + args = string_eval_expression(args) + if infolist_buffer == "": infolist_buffer_new() if infolist_buffer != "" and args != "": infolist_display(infolist_buffer, args) - weechat.buffer_set(infolist_buffer, "display", "1"); + weechat.buffer_set(infolist_buffer, "display", "1") return weechat.WEECHAT_RC_OK + +def string_eval_expression(string): + return weechat.string_eval_expression(string, {}, {}, {}) + + if __name__ == "__main__" and import_ok: - if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, "", ""): - weechat.hook_command("infolist", "Display infolist in a buffer", - "[infolist [pointer] [arguments]]", - " infolist: name of infolist\n" - " pointer: optional pointer for infolist (\"\" for none)\n" - "arguments: optional arguments for infolist\n\n" - "Command without argument will open buffer used " - "to display infolists.\n\n" - "On infolist buffer, you can enter name of an " - "infolist, with optional arguments.\n" - "Enter 'q' to close infolist buffer.\n\n" - "Examples:\n" - " Show information about nick \"FlashCode\" in channel \"#weechat\" on server \"freenode\":\n" - " /infolist irc_nick freenode,#weechat,FlashCode\n" - " Show nicklist from a specific buffer:\n" - " /infolist nicklist " - "", - "%(infolists)", "infolist_cmd", "") + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, + SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + weechat.hook_command( + "infolist", "Display infolist in a buffer", + "[infolist [pointer] [arguments]]", + " infolist: name of infolist\n" + " pointer: optional pointer for infolist (\"\" for none)\n" + "arguments: optional arguments for infolist\n\n" + "Command without argument will open buffer used " + "to display infolists.\n\n" + "On infolist buffer, you can enter name of an " + "infolist, with optional arguments.\n" + "Enter 'q' to close infolist buffer.\n\n" + "Examples:\n" + " Show information about nick \"FlashCode\" in channel " + "\"#weechat\" on server \"freenode\":\n" + " /infolist irc_nick freenode,#weechat,FlashCode\n" + " Show nicklist from a specific buffer:\n" + " /infolist nicklist \n" + " Show current buffer:\n" + " /infolist buffer ${buffer}" + "", + "%(infolists)", "infolist_cmd", "") diff --git a/python/inotify.py b/python/inotify.py deleted file mode 100644 index 3422e34f..00000000 --- a/python/inotify.py +++ /dev/null @@ -1,616 +0,0 @@ -# -*- coding: utf-8 -*- -### -# Copyright (c) 2009-2010 by Elián Hanisch -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -### - -### -# Notifications for WeeChat -# -# Notification script that uses libnotify or dbus, supports WeeChat inside screen. -# Uses a xmlrpc daemon that must be running in the receiving machine (remotely or locally) -# -# The daemon can be setup in several ways, see inotify-daemon --help -# Download it from 'http://github.com/m4v/inotify-daemon/raw/stable/inotify-daemon' -# -# Commands: -# * /inotify -# See /help inotify -# -# Settings: -# * plugins.var.python.inotify.server_uri: -# inotify-daemon address and port to connect, must be the same address the daemon is using. -# By default it uses localhost and port 7766. -# -# Examples: -# http://www.your.home.com:7766 -# http://localhost:7766 -# -# * plugins.var.python.inotify.server_method: -# Notification method supported by the daemon to use. Defaults to 'libnotify'. -# See below for detailed help about them. -# -# * plugins.var.python.inotify.color_nick: -# Will use coloured nicks in notifications. -# -# * plugins.var.python.inotify.ignore_channel: -# Comma separated list of patterns for define ignores. Notifications from channels where its name -# matches any of these patterns will be ignored. -# Wildcards '*', '?' and char groups [..] can be used. -# An ignore exception can be added by prefixing '!' in the pattern. -# -# Example: -# *ubuntu*,!#ubuntu-offtopic -# any notifications from a 'ubuntu' channel will be ignored, except from #ubuntu-offtopic -# -# * plugins.var.python.inotify.ignore_nick: -# Same as ignore_channel, but for nicknames. -# -# Example: -# troll,b[0o]t -# will ignore notifications from troll, bot and b0t -# -# * plugins.var.python.inotify.ignore_text: -# Same as ignore_channel, but for the contents of the message. -# -# * plugins.var.python.inotify.passwd: -# In the case the daemon is using a password for verify that incoming notifications are trusted. -# If this password doesn't match with the password setup in inotify-daemon notification will not -# succeed. -# -# -# Notify methods: -# * libnotify: -# Use libnotify for notifications, needs python-notify installed in the machine running the -# daemon. This is the default method. -# -# * dbus: -# Uses dbus directly for notifications, this is KDE4 specific, might not work in other desktops. -# Needs python-dbus in the machine running the daemon. -# -# * any: -# Use daemon's configured method, this is usually libnotify. -# -# -# TODO -# add commands for configure ignores -# add more notifications methods (?) -# -# -# History: -# 2014-05-10, Sébastien Helleu -# version 0.1.4: change hook_print callback argument type of -# displayed/highlight (WeeChat >= 1.0) -# 2011-11-02, Sebastien Helleu : -# version 0.1.3: use local variable "channel" in buffer instead of reading "short_name", -# fix command for hook_process (remove line break before "-c") -# -# 2011-03-11, Sebastien Helleu : -# version 0.1.2: get python 2.x binary for hook_process (fix problem when -# python 3.x is default python version) -# 2010-03-10: -# version 0.1.1: fixes -# * improved shell escapes when using hook_process -# * fix ACTION messages -# -# 2010-02-24 -# version 0.1: release! -# -### - -SCRIPT_NAME = "inotify" -SCRIPT_AUTHOR = "Elián Hanisch " -SCRIPT_VERSION = "0.1.4" -SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Notifications for WeeChat." -SCRIPT_COMMAND = "inotify" - -DAEMON_URL = 'http://github.com/m4v/inotify-daemon/raw/stable/inotify-daemon' -DAEMON = 'inotify-daemon' -DAEMON_VERSION = '0.2' - -### Default Settings ### -settings = { -'server_uri' : 'http://localhost:7766', -'server_method' : 'any', -'color_nick' : 'on', -'ignore_channel' : '', -'ignore_nick' : '', -'ignore_text' : '', -'passwd' : '', -} - -max_error_count = 3 - -try: - import weechat - WEECHAT_RC_OK = weechat.WEECHAT_RC_OK - import_ok = True -except: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" - import_ok = False - -import xmlrpclib, socket -from fnmatch import fnmatch - -# remote daemon timeout -socket.setdefaulttimeout(4) - -### Messages ### -def debug(s, prefix=''): - """Debug msg""" - if not weechat.config_get_plugin('debug'): return - buffer_name = 'DEBUG_' + SCRIPT_NAME - buffer = weechat.buffer_search('python', buffer_name) - if not buffer: - buffer = weechat.buffer_new(buffer_name, '', '', '', '') - weechat.buffer_set(buffer, 'nicklist', '0') - weechat.buffer_set(buffer, 'time_for_each_line', '0') - weechat.buffer_set(buffer, 'localvar_set_no_log', '1') - weechat.prnt(buffer, '%s\t%s' %(prefix, s)) - -def error(s, prefix='', buffer='', trace=''): - """Error msg""" - if weechat.config_get_plugin('quiet'): return - prefix = prefix or script_nick - weechat.prnt(buffer, '%s%s %s' %(weechat.prefix('error'), prefix, s)) - if weechat.config_get_plugin('debug'): - if not trace: - import traceback - if traceback.sys.exc_type: - trace = traceback.format_exc() - not trace or weechat.prnt('', trace) - -def say(s, prefix='', buffer=''): - """normal msg""" - prefix = prefix or script_nick - weechat.prnt(buffer, '%s\t%s' %(prefix, s)) - -### Config and value validation ### -boolDict = {'on':True, 'off':False} -def get_config_boolean(config): - value = weechat.config_get_plugin(config) - try: - return boolDict[value] - except KeyError: - default = settings[config] - error("Error while fetching config '%s'. Using default value '%s'." %(config, default)) - error("'%s' is invalid, allowed: 'on', 'off'" %value) - return boolDict[default] - -def get_config_int(config, allow_empty_string=False): - value = weechat.config_get_plugin(config) - try: - return int(value) - except ValueError: - if value == '' and allow_empty_string: - return value - default = settings[config] - error("Error while fetching config '%s'. Using default value '%s'." %(config, default)) - error("'%s' is not a number." %value) - return int(default) - -valid_methods = set(('any', 'dbus', 'libnotify')) -def get_config_valid_string(config, valid_strings=valid_methods): - value = weechat.config_get_plugin(config) - if value not in valid_strings: - default = settings[config] - error("Error while fetching config '%s'. Using default value '%s'." %(config, default)) - error("'%s' is an invalid value, allowed: %s." %(value, ', '.join(valid_strings))) - return default - return value - -### Class definitions ### -class Ignores(object): - def __init__(self, ignore_type): - self.ignore_type = ignore_type - self.ignores = [] - self.exceptions = [] - self._get_ignores() - - def _get_ignores(self): - assert self.ignore_type is not None - ignores = weechat.config_get_plugin(self.ignore_type).split(',') - ignores = [ s.lower() for s in ignores if s ] - self.ignores = [ s for s in ignores if s[0] != '!' ] - self.exceptions = [ s[1:] for s in ignores if s[0] == '!' ] - - def __contains__(self, s): - s = s.lower() - for p in self.ignores: - if fnmatch(s, p): - for e in self.exceptions: - if fnmatch(s, e): - return False - return True - return False - - -class Server(object): - def catch_exceptions(f): - """ - This decorator is for catch exceptions in methods that communicate with the daemon.""" - def protected_method(self, *args): - try: - return f(self, *args) - except xmlrpclib.Fault, e: - self._error("A fault occurred." %e, trace="Code: %s\nString: %s" %(e.faultCode, e.faultString)) - except xmlrpclib.ProtocolError, e: - self._error("Protocol error: %s" %e, trace="Url: %s\nCode: %s\nHeaders: %s\nMessage: %s" %(e.url, e.errcode, - e.headers, e.errmsg)) - except socket.error, e: - self._error_connect() - except socket.timeout, e: - self._error('Timeout while sending to our daemon.') - if self.error_count < 3: # don't re-queue after 3 errors - return 'retry' - except: - # catch all exception - self._error("An error occurred, but I'm not sure what could it be...") - return protected_method - - def __init__(self): - self._reset() - self._create_server() - if not self.error_count: - self.send_rpc('Notification script loaded') - - def _reset(self): - self.msg = {} - self.timer = None - - def enqueue(self, msg, channel): - self._enqueue(msg, channel) - - def _enqueue(self, msg, channel='', timeout=3000): - if channel not in self.msg: - self.msg[channel] = msg - else: - s = self.msg[channel] - msg = '%s\n%s' %(s, msg) - self.msg[channel] = msg - if self.timer is None: - self.timer = weechat.hook_timer(timeout, 0, 1, 'msg_flush', '') - #debug('set timer: %s %s' %(self.timer, timeout)) - - def flush(self): - for channel, msg in self.msg.iteritems(): - if self.send_rpc(msg, channel) == 'retry': - # daemon is restarting, try again later - self._restart_timer() - return - if self.remote: - # we can't stop flushing if we're in remote mode, so save a copy as we might need - # to repeat the queue later - self.msg_bak = self.msg.copy() - self._reset() - - def _restart_timer(self): - if self.timer is not None: - #debug('reset and set timer') - weechat.unhook(self.timer) - self.timer = weechat.hook_timer(5000, 0, 1, 'msg_flush', '') - - @catch_exceptions - def _create_server(self): - self.error_count = 0 - self.method = get_config_valid_string('server_method') - self.address = weechat.config_get_plugin('server_uri') - # detect if we're going to connect to localhost. - if self.address[:17] in ('http://localhost:', 'http://127.0.0.1:'): - self.remote = False - else: - self.remote = True - self.msg_bak = {} - self.server = xmlrpclib.Server(self.address) - version = self.server.version() - if version != DAEMON_VERSION: - error('Incorrect daemon version, should be %s, but got %s' %(DAEMON_VERSION, - version)) - error('Download the latest %s from %s' %(DAEMON, DAEMON_URL)) - - def _error(self, s, **kwargs): - if self.error_count < max_error_count: # stop sending error msg after max reached - error(s, **kwargs) - elif self.error_count == max_error_count: - error('Suppressing future error messages...') - self.error_count += 1 - - def _error_connect(self): - self._error('Failed to connect to our notification daemon, check if the address' - ' \'%s\' is correct and if it\'s running.' %self.address) - - @catch_exceptions - def send_rpc(self, *args): - debug('sending rpc: %s' %' '.join(map(repr, args))) - passwd = weechat.config_get_plugin('passwd') - if self.remote: - return self._send_rpc_process(passwd, *args) - rt = getattr(self.server, self.method)(passwd, *args) - if rt == 'OK': - self.error_count = 0 - #debug('Success: %s' % rt) - elif rt.startswith('warning:'): - self._error(rt[8:]) - if self.error_count < 10: # don't re-queue after 10 errors - #debug('repeating queue') - # returning 'retry' will cause flush() to try to send msgs again later - return 'retry' - else: - error(rt) - - def _send_rpc_process(self, *args): - def quoted(s): - """ - Is important to escape quotes properly so hook_process doesn't break or sends stuff - outside single quotes.""" - if '\\' in s: - # escape any backslashes - s = s.replace('\\', '\\\\') - if '"' in s: - s = s.replace('"', '\\"') - if "'" in s: - # I must escape single quotes with \'\\\'\' because they will be within single - # quotes in the command string. Awesome. - s = s.replace("'", "'\\''") - return '"""%s"""' %s - - args = ', '.join(map(quoted, args)) - python2_bin = weechat.info_get('python2_bin', '') or 'python' - cmd = python2_bin + rpc_process_cmd %{'server_uri':self.address, 'method':self.method, 'args':args} - debug('\nRemote cmd:%s\n' %cmd) - weechat.hook_process(cmd, 30000, 'rpc_process_cb', '') - - @catch_exceptions - def quit(self): - passwd = weechat.config_get_plugin('passwd') - rt = self.server.quit(passwd) - debug(rt) - if rt != 'OK': - error(rt) - - @catch_exceptions - def restart(self): - passwd = weechat.config_get_plugin('passwd') - rt = self.server.restart(passwd) - debug(rt) - if rt != 'OK': - error(rt) - - -### Functions ### -def msg_flush(*args): - server.flush() - return WEECHAT_RC_OK - -# command MUST be within single quotes, otherwise the shell would try to expand stuff and it might -# be real nasty, somebody could run arbitrary code with a highlight. -rpc_process_cmd = """ -c ' -import xmlrpclib -try: - server = xmlrpclib.Server("%(server_uri)s") - print getattr(server, "%(method)s")(%(args)s) -except Exception, e: - print "error: %%s" %%e' -""" - -def rpc_process_cb(data, command, rc, stdout, stderr): - #debug("%s\nstderr: %s\nstdout: %s" %(rc, repr(stderr), repr(stdout))) - if stdout: - debug('Reply: %s' %stdout) - if stdout == 'OK\n': - server.error_count = 0 - elif stdout.startswith('warning:'): - server._error(stdout[8:]) - if server.error_count < 10: - server.msg = server.msg_bak - server._restart_timer() - else: - server._error(stdout) - if stderr: - error(stderr) - return WEECHAT_RC_OK - -color_table = ('teal', 'darkmagenta', 'darkgreen', 'brown', 'blue', 'darkblue', 'darkcyan', 'magenta', 'green', 'grey') - -def color_tag(nick): - n = len(color_table) - #generic_nick = nick.strip('_`').lower() - id = (sum(map(ord, nick))%n) - #debug('%s:%s' %(nick, id)) - return '<%s>' %(color_table[id], nick) - -def format(s, nick=''): - if '<' in s: - s = s.replace('<', '<') - if '>' in s: - s = s.replace('>', '>') - if '"' in s: - s = s.replace('"', '"') - if '\n' in s: - s = s.replace('\n', '
') - if nick: - if get_config_boolean('color_nick'): - nick = color_tag(nick) - else: - nick = '<%s>' %nick - s = '%s %s' %(nick, s) - return s - -def send_notify(s, channel='', nick=''): - #command = getattr(server, 'kde4') - s = format(s, nick) - server.enqueue(s, channel) - -def is_displayed(buffer): - """Returns True if buffer is in a window and the user is active. This is for not show - notifications of a visible buffer while the user is doing something and wouldn't need to be - notified.""" - window = weechat.buffer_get_integer(buffer, 'num_displayed') - if window != 0: - return not inactive() - return False - -def inactive(): - inactivity = int(weechat.info_get('inactivity', '')) - #debug('user inactivity: %s' %inactivity) - if inactivity > 20: - return True - else: - return False - -config_string = lambda s : weechat.config_string(weechat.config_get(s)) -def get_nick(s): - """Strip nickmodes and prefix, suffix.""" - if not s: return '' - # prefix and suffix - prefix = config_string('irc.look.nick_prefix') - suffix = config_string('irc.look.nick_suffix') - if s[0] == prefix: - s = s[1:] - if s[-1] == suffix: - s = s[:-1] - # nick mode - modes = '~+@!%' - s = s.lstrip(modes) - return s - -def notify_msg(workaround, buffer, time, tags, display, hilight, prefix, msg): - if workaround and 'notify_message' not in tags and 'notify_private' not in tags: - # weechat 0.3.0 bug - return WEECHAT_RC_OK - #debug(' '.join((buffer, time, tags, display, hilight, prefix, 'msg_len:%s' %len(msg))), - # prefix='MESSAGE') - private = 'notify_private' in tags - if (int(hilight) or private) and int(display): - if 'irc_action' in tags: - prefix, _, msg = msg.partition(' ') - msg = '%s %s' %(config_string('weechat.look.prefix_action'), msg) - prefix = get_nick(prefix) - if prefix not in ignore_nick \ - and msg not in ignore_text \ - and not is_displayed(buffer): - #debug('%sSending notification: %s' %(weechat.color('lightgreen'), channel), prefix='NOTIFY') - if not private: - channel = weechat.buffer_get_string(buffer, 'localvar_channel') - if channel not in ignore_channel: - send_notify(msg, channel=channel, nick=prefix) - else: - send_notify(msg, channel=prefix) - return WEECHAT_RC_OK - -def cmd_notify(data, buffer, args): - if args: - args = args.split() - cmd = args[0] - if cmd in ('test', 'quit', 'restart', 'notify'): - if cmd == 'test': - server.send_rpc(' '.join(args[1:]) or 'This is a test.', '#test') - elif cmd == 'notify': - send_notify(' '.join(args[1:]) or 'This is a test.', '#test') - elif cmd == 'quit': - say('Shutting down notification daemon...') - server.quit() - elif cmd == 'restart': - say('Restarting notification daemon...') - server.restart() - return WEECHAT_RC_OK - - weechat.command('', '/help %s' %SCRIPT_COMMAND) - return WEECHAT_RC_OK - -def ignore_update(*args): - ignore_channel._get_ignores() - ignore_nick._get_ignores() - ignore_text._get_ignores() - return WEECHAT_RC_OK - -def server_update(*args): - server._create_server() - return WEECHAT_RC_OK - - -if __name__ == '__main__' and import_ok and \ - weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, - '', ''): - - # pretty nick - color_delimiter = weechat.color('chat_delimiters') - color_nick = weechat.color('chat_nick') - color_reset = weechat.color('reset') - script_nick = '%s[%s%s%s]%s' %(color_delimiter, color_nick, SCRIPT_NAME, color_delimiter, color_reset) - - # check if we need to workaround a bug in 0.3.0 - workaround = '' - version = weechat.info_get('version', '') - if version == '0.3.0': - workaround = '1' - #debug('workaround enabled') - - for opt, val in settings.iteritems(): - if not weechat.config_is_set_plugin(opt): - weechat.config_set_plugin(opt, val) - - ignore_channel = Ignores('ignore_channel') - ignore_nick = Ignores('ignore_nick') - ignore_text = Ignores('ignore_text') - - server = Server() - - weechat.hook_command(SCRIPT_COMMAND, SCRIPT_DESC, '[test [text] | notify [text] | restart | quit ]', -"""\ - test: sends a test notification, with 'text' if provided ('text' - is sent raw). - notify: same as test, but the notification is sent through the - notification queue and after formatting. -restart: forces remote daemon to restart. - quit: forces remote daemon to shutdown, after this notifications - won't be available and the daemon should be started again - manually. - -Setting notification ignores: - It's possible to filter notification by channel, by nick or by - message content, with the config options ignore_channel, - ignore_nick and ignore_text in plugins.var.python.%(script)s - Each config option accepts a comma separated list of patterns. - Wildcards '*', '?' and char groups [..] can be used. - An ignore exception can be added by prefixing '!' in the pattern. - -Examples: - Setting 'ignore_nick' to 'troll,b[0o]t': - will ignore notifications from troll, bot and b0t. - Setting 'ignore_channel' to '*ubuntu*,!#ubuntu-offtopic': - will ignore notifications from any channel with the word 'ubuntu' - except from #ubuntu-offtopic. - -Daemon: - %(script)s script needs to connect to an external daemon for send - notifications, which can be used in localhost or remotely. - Download the daemon from: - %(daemon_url)s - and check its help with ./%(daemon)s --help. - See also help in script file. -""" %dict(script=SCRIPT_NAME, daemon_url=DAEMON_URL, daemon=DAEMON) - ,'test|notify|restart|quit', 'cmd_notify', '') - - weechat.hook_config('plugins.var.python.%s.ignore_*' %SCRIPT_NAME, 'ignore_update', '') - weechat.hook_config('plugins.var.python.%s.server_*' %SCRIPT_NAME, 'server_update', '') - - weechat.hook_print('', 'notify_message', '', 1, 'notify_msg', workaround) - weechat.hook_print('', 'notify_private', '', 1, 'notify_msg', workaround) - - -# vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100: diff --git a/python/irccloud_avatar_link.py b/python/irccloud_avatar_link.py new file mode 100644 index 00000000..86542c24 --- /dev/null +++ b/python/irccloud_avatar_link.py @@ -0,0 +1,46 @@ +# +# Copyright (9) 2024 Jesse McDowell +# +# Add IRCCloud avatar image link to WHOIS output +# +# This program is free software: you can redistribute it and/or modify it under the terms of the +# GNU General Public License as published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. If not, +# see . +# +# 2024-08-25: Jesse McDowell +# version 1.0: Initial release + + +try: + import weechat + from weechat import WEECHAT_RC_OK + import_ok = True +except ImportError: + print('This script must be run under WeeChat.') + import_ok = False + +import re + +def whois311_cb(data, signal, signal_data): + buffer = weechat.info_get("irc_buffer", signal.split(",")[0]) + + userid_text = signal_data.split(" ")[5 if signal_data[0] == "@" else 4] + + userid_match = userid_expression.match(userid_text) + if userid_match is not None: + weechat.prnt(buffer, "Avatar image: https://static.irccloud-cdn.com/avatar-redirect/%s" % userid_match.groups()[0]) + + return WEECHAT_RC_OK + +if __name__ == '__main__' and import_ok: + weechat.register("irccloud_avatar_link", "Jesse McDowell", "1.0", "GPL3", "Add IRCCloud avatar image link to WHOIS details", "", "") + userid_expression = re.compile("^[us]id([0-9]+)$") + + weechat.hook_signal("*,irc_in2_311", "whois311_cb", "") diff --git a/python/ircrypt.py b/python/ircrypt.py index 590e573e..c2d9341c 100644 --- a/python/ircrypt.py +++ b/python/ircrypt.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- -# # IRCrypt: Secure Encryption Layer Atop IRC # ========================================= # -# Copyright (C) 2013-2014 -# Lars Kiesow -# Sven Haardiek +# SPDX-FileCopyrightText: 2013-2014 Lars Kiesow +# SPDX-FileCopyrightText: 2013-2014 Sven Haardiek +# +# SPDX-License-Identifier: GPL-3.0-or-later # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -42,13 +41,12 @@ # https://github.com/IRCrypt/ircrypt-weechat # - import weechat, string, os, subprocess, base64, time # Constants used in this script SCRIPT_NAME = 'ircrypt' SCRIPT_AUTHOR = 'Sven Haardiek , Lars Kiesow ' -SCRIPT_VERSION = '1.0' +SCRIPT_VERSION = '1.0.1' SCRIPT_LICENSE = 'GPL3' SCRIPT_DESC = 'IRCrypt: Encryption layer for IRC' SCRIPT_HELP_TEXT = ''' @@ -109,569 +107,569 @@ class MessageParts: - '''Class used for storing parts of messages which were split after - encryption due to their length.''' - - modified = 0 - last_id = None - message = '' - - def update(self, id, msg): - '''This method updates an already existing message part by adding a new - part to the old ones and updating the identifier of the latest received - message part. - ''' - # Check if id is correct. If not, throw away old parts: - if self.last_id and self.last_id != id+1: - self.message = '' - # Check if the are old message parts which belong due to their old age - # probably not to this message: - if time.time() - self.modified > MSG_PART_TIMEOUT: - self.message = '' - self.last_id = id - self.message = msg + self.message - self.modified = time.time() + '''Class used for storing parts of messages which were split after + encryption due to their length.''' + + modified = 0 + last_id = None + message = '' + + def update(self, id, msg): + '''This method updates an already existing message part by adding a new + part to the old ones and updating the identifier of the latest received + message part. + ''' + # Check if id is correct. If not, throw away old parts: + if self.last_id and self.last_id != id+1: + self.message = '' + # Check if the are old message parts which belong due to their old age + # probably not to this message: + if time.time() - self.modified > MSG_PART_TIMEOUT: + self.message = '' + self.last_id = id + self.message = msg + self.message + self.modified = time.time() def ircrypt_gnupg(stdin, *args): - '''Try to execute gpg with given input and options. - - :param stdin: Input for GnuPG - :param args: Additional command line options for GnuPG - :returns: Tuple containing returncode, stdout and stderr - ''' - gnupg = weechat.config_string(weechat.config_get('ircrypt.general.binary')) - if not gnupg: - return (99, '', 'GnuPG could not be found') - p = subprocess.Popen( - [gnupg, '--batch', '--no-tty'] + list(args), - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate(stdin) - return (p.returncode, out, err) + '''Try to execute gpg with given input and options. + + :param stdin: Input for GnuPG + :param args: Additional command line options for GnuPG + :returns: Tuple containing returncode, stdout and stderr + ''' + gnupg = weechat.config_string(weechat.config_get('ircrypt.general.binary')) + if not gnupg: + return (99, '', 'GnuPG could not be found') + p = subprocess.Popen( + [gnupg, '--batch', '--no-tty'] + list(args), + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate(stdin) + return (p.returncode, out, err) def ircrypt_split_msg(cmd, pre, msg): - '''Convert encrypted message in MAX_PART_LEN sized blocks - ''' - msg = msg.rstrip() - return '\n'.join(['%s:>%s-%i %s' % - (cmd, pre, i // MAX_PART_LEN, msg[i:i+MAX_PART_LEN]) - for i in range(0, len(msg), MAX_PART_LEN)][::-1]) + '''Convert encrypted message in MAX_PART_LEN sized blocks + ''' + msg = msg.rstrip() + return '\n'.join(['%s:>%s-%i %s' % + (cmd, pre, i // MAX_PART_LEN, msg[i:i+MAX_PART_LEN]) + for i in range(0, len(msg), MAX_PART_LEN)][::-1]) def ircrypt_error(msg, buf): - '''Print errors to a given buffer. Errors are printed in red and have the - weechat error prefix. - ''' - weechat.prnt(buf, weechat.prefix('error') + weechat.color('red') + - ('\n' + weechat.color('red')).join(msg.split('\n'))) + '''Print errors to a given buffer. Errors are printed in red and have the + weechat error prefix. + ''' + weechat.prnt(buf, weechat.prefix('error') + weechat.color('red') + + ('\n' + weechat.color('red')).join(msg.split('\n'))) def ircrypt_warn(msg, buf=''): - '''Print warnings. If no buffer is set, the default weechat buffer is used. - Warnin are printed in gray without marker. - ''' - weechat.prnt(buf, weechat.color('gray') + - ('\n' + weechat.color('gray')).join(msg.split('\n'))) + '''Print warnings. If no buffer is set, the default weechat buffer is used. + Warnin are printed in gray without marker. + ''' + weechat.prnt(buf, weechat.color('gray') + + ('\n' + weechat.color('gray')).join(msg.split('\n'))) def ircrypt_info(msg, buf=None): - '''Print ifo message to specified buffer. If no buffer is set, the current - foreground buffer is used to print the message. - ''' - if buf is None: - buf = weechat.current_buffer() - weechat.prnt(buf, msg) + '''Print ifo message to specified buffer. If no buffer is set, the current + foreground buffer is used to print the message. + ''' + if buf is None: + buf = weechat.current_buffer() + weechat.prnt(buf, msg) def ircrypt_decrypt_hook(data, msgtype, server, args): - '''Hook for incomming PRVMSG commands. - This method will parse the input, check if it is an encrypted message and - call the appropriate decryption methods if necessary. - - :param data: - :param msgtype: - :param server: IRC server the message comes from. - :param args: IRC command line- - ''' - info = weechat.info_get_hashtable('irc_message_parse', { 'message': args }) - - # Check if channel is own nick and if change channel to nick of sender - if info['channel'][0] not in '#&': - info['channel'] = info['nick'] - - # Get key - key = ircrypt_keys.get(('%s/%s' % (server, info['channel'])).lower()) - - # Return everything as it is if we have no key - if not key: - return args - - if not '>CRY-' in args: - # if key exisits and no >CRY not part of message flag message as unencrypted - pre, message = args.split(' :', 1) - marker = weechat.config_string(ircrypt_config_option['unencrypted']) - return '%s :%s %s' % (pre, marker, message) - - # if key exists and >CRY part of message start symmetric encryption - pre, message = args.split('>CRY-', 1) - number, message = message.split(' ', 1 ) - - # Get key for the message memory - catchword = '%s.%s.%s' % (server, info['channel'], info['nick']) - - # Decrypt only if we got last part of the message - # otherwise put the message into a global memory and quit - if int(number) != 0: - if not catchword in ircrypt_msg_memory: - ircrypt_msg_memory[catchword] = MessageParts() - ircrypt_msg_memory[catchword].update(int(number), message) - return '' - - # Get whole message - try: - message = message + ircrypt_msg_memory[catchword].message - del ircrypt_msg_memory[catchword] - except KeyError: - pass - - # Get message buffer in case we need to print an error - buf = weechat.buffer_search('irc', '%s.%s' % (server,info['channel'])) - - # Decode base64 encoded message - try: - message = base64.b64decode(message) - except: - ircrypt_error('Could not Base64 decode message.', buf) - return args - - # Decrypt - try: - message = (key).encode('utf-8') + b'\n' + message - except: - # For Python 2.x - message = key + b'\n' + message - (ret, out, err) = ircrypt_gnupg(message, - '--passphrase-fd', '-', '-q', '-d') - - # Get and print GPG errors/warnings - if ret: - ircrypt_error(err.decode('utf-8'), buf) - return args - if err: - ircrypt_warn(err.decode('utf-8')) - - return pre + out.decode('utf-8') + '''Hook for incomming PRVMSG commands. + This method will parse the input, check if it is an encrypted message and + call the appropriate decryption methods if necessary. + + :param data: + :param msgtype: + :param server: IRC server the message comes from. + :param args: IRC command line- + ''' + info = weechat.info_get_hashtable('irc_message_parse', { 'message': args }) + + # Check if channel is own nick and if change channel to nick of sender + if info['channel'][0] not in '#&': + info['channel'] = info['nick'] + + # Get key + key = ircrypt_keys.get(('%s/%s' % (server, info['channel'])).lower()) + + # Return everything as it is if we have no key + if not key: + return args + + if not '>CRY-' in args: + # if key exisits and no >CRY not part of message flag message as unencrypted + pre, message = args.split(' :', 1) + marker = weechat.config_string(ircrypt_config_option['unencrypted']) + return '%s :%s %s' % (pre, marker, message) + + # if key exists and >CRY part of message start symmetric encryption + pre, message = args.split('>CRY-', 1) + number, message = message.split(' ', 1 ) + + # Get key for the message memory + catchword = '%s.%s.%s' % (server, info['channel'], info['nick']) + + # Decrypt only if we got last part of the message + # otherwise put the message into a global memory and quit + if int(number) != 0: + if not catchword in ircrypt_msg_memory: + ircrypt_msg_memory[catchword] = MessageParts() + ircrypt_msg_memory[catchword].update(int(number), message) + return '' + + # Get whole message + try: + message = message + ircrypt_msg_memory[catchword].message + del ircrypt_msg_memory[catchword] + except KeyError: + pass + + # Get message buffer in case we need to print an error + buf = weechat.buffer_search('irc', '%s.%s' % (server,info['channel'])) + + # Decode base64 encoded message + try: + message = base64.b64decode(message) + except: + ircrypt_error('Could not Base64 decode message.', buf) + return args + + # Decrypt + try: + message = (key).encode('utf-8') + b'\n' + message + except: + # For Python 2.x + message = key + b'\n' + message + (ret, out, err) = ircrypt_gnupg(message, + '--passphrase-fd', '-', '-q', '-d') + + # Get and print GPG errors/warnings + if ret: + ircrypt_error(err.decode('utf-8'), buf) + return args + if err: + ircrypt_warn(err.decode('utf-8')) + + return pre + out.decode('utf-8') def ircrypt_encrypt_hook(data, msgtype, server, args): - '''Hook for outgoing PRVMSG commands. - This method will call the appropriate methods for encrypting the outgoing - messages either symmetric or asymmetric - - :param data: - :param msgtype: - :param server: IRC server the message comes from. - :param args: IRC command line- - ''' - info = weechat.info_get_hashtable("irc_message_parse", { "message": args }) - - # check if this message is to be send as plain text - plain = ircrypt_message_plain.get('%s/%s' % (server, info['channel'])) - if plain: - del ircrypt_message_plain['%s/%s' % (server, info['channel'])] - if (plain[0] - time.time()) < 5 \ - and args == 'PRIVMSG %s :%s' % (info['channel'], plain[1]): - args = args.replace('PRIVMSG %s :%s ' % ( - info['channel'], - weechat.config_string(ircrypt_config_option['unencrypted'])), - 'PRIVMSG %s :' % info['channel']) - return args - - # check symmetric key - key = ircrypt_keys.get(('%s/%s' % (server, info['channel'])).lower()) - if not key: - # No key -> don't encrypt - return args - - # Get cipher - cipher = ircrypt_cipher.get(('%s/%s' % (server, info['channel'])).lower(), - weechat.config_string(ircrypt_config_option['sym_cipher'])) - # Get prefix and message - pre, message = args.split(':', 1) - - # encrypt message - try: - inp = key.encode('utf-8') + b'\n' + message.encode('utf-8') - except: - inp = key + b'\n' + message - (ret, out, err) = ircrypt_gnupg(inp, - '--symmetric', '--cipher-algo', cipher, '--passphrase-fd', '-') - - # Get and print GPG errors/warnings - if ret: - buf = weechat.buffer_search('irc', '%s.%s' % (server, info['channel'])) - ircrypt_error(err.decode('utf-8'), buf) - return args - if err: - ircrypt_warn(err.decode('utf-8')) - - # Ensure the generated messages are not too long and send them - return ircrypt_split_msg(pre, 'CRY', base64.b64encode(out).decode('utf-8')) + '''Hook for outgoing PRVMSG commands. + This method will call the appropriate methods for encrypting the outgoing + messages either symmetric or asymmetric + + :param data: + :param msgtype: + :param server: IRC server the message comes from. + :param args: IRC command line- + ''' + info = weechat.info_get_hashtable("irc_message_parse", { "message": args }) + + # check if this message is to be send as plain text + plain = ircrypt_message_plain.get('%s/%s' % (server, info['channel'])) + if plain: + del ircrypt_message_plain['%s/%s' % (server, info['channel'])] + if (plain[0] - time.time()) < 5 \ + and args == 'PRIVMSG %s :%s' % (info['channel'], plain[1]): + args = args.replace('PRIVMSG %s :%s ' % ( + info['channel'], + weechat.config_string(ircrypt_config_option['unencrypted'])), + 'PRIVMSG %s :' % info['channel']) + return args + + # check symmetric key + key = ircrypt_keys.get(('%s/%s' % (server, info['channel'])).lower()) + if not key: + # No key -> don't encrypt + return args + + # Get cipher + cipher = ircrypt_cipher.get(('%s/%s' % (server, info['channel'])).lower(), + weechat.config_string(ircrypt_config_option['sym_cipher'])) + # Get prefix and message + pre, message = args.split(':', 1) + + # encrypt message + try: + inp = key.encode('utf-8') + b'\n' + message.encode('utf-8') + except: + inp = key + b'\n' + message + (ret, out, err) = ircrypt_gnupg(inp, + '--symmetric', '--cipher-algo', cipher, '--passphrase-fd', '-') + + # Get and print GPG errors/warnings + if ret: + buf = weechat.buffer_search('irc', '%s.%s' % (server, info['channel'])) + ircrypt_error(err.decode('utf-8'), buf) + return args + if err: + ircrypt_warn(err.decode('utf-8')) + + # Ensure the generated messages are not too long and send them + return ircrypt_split_msg(pre, 'CRY', base64.b64encode(out).decode('utf-8')) def ircrypt_config_init(): - ''' This method initializes the configuration file. It creates sections and - options in memory and prepares the handling of key sections. - ''' - global ircrypt_config_file - ircrypt_config_file = weechat.config_new('ircrypt', 'ircrypt_config_reload_cb', '') - if not ircrypt_config_file: - return - - # marker - ircrypt_config_section['marker'] = weechat.config_new_section( - ircrypt_config_file, 'marker', 0, 0, '', '', '', '', '', '', '', '', - '', '') - if not ircrypt_config_section['marker']: - weechat.config_free(ircrypt_config_file) - return - ircrypt_config_option['encrypted'] = weechat.config_new_option( - ircrypt_config_file, ircrypt_config_section['marker'], - 'encrypted', 'string', 'Marker for encrypted messages', '', 0, 0, - 'encrypted', 'encrypted', 0, '', '', '', '', '', '') - ircrypt_config_option['unencrypted'] = weechat.config_new_option( - ircrypt_config_file, ircrypt_config_section['marker'], 'unencrypted', - 'string', 'Marker for unencrypted messages received in an encrypted channel', - '', 0, 0, '', 'u', 0, '', '', '', '', '', '') - - # cipher options - ircrypt_config_section['cipher'] = weechat.config_new_section( - ircrypt_config_file, 'cipher', 0, 0, '', '', '', '', '', '', '', '', - '', '') - if not ircrypt_config_section['cipher']: - weechat.config_free(ircrypt_config_file) - return - ircrypt_config_option['sym_cipher'] = weechat.config_new_option( - ircrypt_config_file, ircrypt_config_section['cipher'], - 'sym_cipher', 'string', 'symmetric cipher used by default', '', 0, 0, - 'TWOFISH', 'TWOFISH', 0, '', '', '', '', '', '') - - # general options - ircrypt_config_section['general'] = weechat.config_new_section( - ircrypt_config_file, 'general', 0, 0, '', '', '', '', '', '', '', '', - '', '') - if not ircrypt_config_section['general']: - weechat.config_free(ircrypt_config_file) - return - ircrypt_config_option['binary'] = weechat.config_new_option( - ircrypt_config_file, ircrypt_config_section['general'], - 'binary', 'string', 'GnuPG binary to use', '', 0, 0, - '', '', 0, '', '', '', '', '', '') - - # keys - ircrypt_config_section['keys'] = weechat.config_new_section( - ircrypt_config_file, 'keys', 0, 0, 'ircrypt_config_keys_read_cb', '', - 'ircrypt_config_keys_write_cb', '', '', '', '', '', '', '') - if not ircrypt_config_section['keys']: - weechat.config_free(ircrypt_config_file) - - # Special Ciphers - ircrypt_config_section['special_cipher'] = weechat.config_new_section( - ircrypt_config_file, 'special_cipher', 0, 0, - 'ircrypt_config_special_cipher_read_cb', '', - 'ircrypt_config_special_cipher_write_cb', '', '', '', '', '', '', '') - if not ircrypt_config_section['special_cipher']: - weechat.config_free(ircrypt_config_file) + ''' This method initializes the configuration file. It creates sections and + options in memory and prepares the handling of key sections. + ''' + global ircrypt_config_file + ircrypt_config_file = weechat.config_new('ircrypt', 'ircrypt_config_reload_cb', '') + if not ircrypt_config_file: + return + + # marker + ircrypt_config_section['marker'] = weechat.config_new_section( + ircrypt_config_file, 'marker', 0, 0, '', '', '', '', '', '', '', '', + '', '') + if not ircrypt_config_section['marker']: + weechat.config_free(ircrypt_config_file) + return + ircrypt_config_option['encrypted'] = weechat.config_new_option( + ircrypt_config_file, ircrypt_config_section['marker'], + 'encrypted', 'string', 'Marker for encrypted messages', '', 0, 0, + 'encrypted', 'encrypted', 0, '', '', '', '', '', '') + ircrypt_config_option['unencrypted'] = weechat.config_new_option( + ircrypt_config_file, ircrypt_config_section['marker'], 'unencrypted', + 'string', 'Marker for unencrypted messages received in an encrypted channel', + '', 0, 0, '', 'u', 0, '', '', '', '', '', '') + + # cipher options + ircrypt_config_section['cipher'] = weechat.config_new_section( + ircrypt_config_file, 'cipher', 0, 0, '', '', '', '', '', '', '', '', + '', '') + if not ircrypt_config_section['cipher']: + weechat.config_free(ircrypt_config_file) + return + ircrypt_config_option['sym_cipher'] = weechat.config_new_option( + ircrypt_config_file, ircrypt_config_section['cipher'], + 'sym_cipher', 'string', 'symmetric cipher used by default', '', 0, 0, + 'TWOFISH', 'TWOFISH', 0, '', '', '', '', '', '') + + # general options + ircrypt_config_section['general'] = weechat.config_new_section( + ircrypt_config_file, 'general', 0, 0, '', '', '', '', '', '', '', '', + '', '') + if not ircrypt_config_section['general']: + weechat.config_free(ircrypt_config_file) + return + ircrypt_config_option['binary'] = weechat.config_new_option( + ircrypt_config_file, ircrypt_config_section['general'], + 'binary', 'string', 'GnuPG binary to use', '', 0, 0, + '', '', 0, '', '', '', '', '', '') + + # keys + ircrypt_config_section['keys'] = weechat.config_new_section( + ircrypt_config_file, 'keys', 0, 0, 'ircrypt_config_keys_read_cb', '', + 'ircrypt_config_keys_write_cb', '', '', '', '', '', '', '') + if not ircrypt_config_section['keys']: + weechat.config_free(ircrypt_config_file) + + # Special Ciphers + ircrypt_config_section['special_cipher'] = weechat.config_new_section( + ircrypt_config_file, 'special_cipher', 0, 0, + 'ircrypt_config_special_cipher_read_cb', '', + 'ircrypt_config_special_cipher_write_cb', '', '', '', '', '', '', '') + if not ircrypt_config_section['special_cipher']: + weechat.config_free(ircrypt_config_file) def ircrypt_config_reload_cb(data, config_file): - '''Handle a reload of the configuration file. - ''' - global ircrypt_keys, ircrypt_cipher - # Forget Keys and ciphers to make sure they are properly reloaded and no old - # ones are left - ircrypt_keys = {} - ircrypt_cipher = {} - return weechat.config_reload(config_file) + '''Handle a reload of the configuration file. + ''' + global ircrypt_keys, ircrypt_cipher + # Forget Keys and ciphers to make sure they are properly reloaded and no old + # ones are left + ircrypt_keys = {} + ircrypt_cipher = {} + return weechat.config_reload(config_file) def ircrypt_config_read(): - ''' Read IRCrypt configuration file (ircrypt.conf). - ''' - return weechat.config_read(ircrypt_config_file) + ''' Read IRCrypt configuration file (ircrypt.conf). + ''' + return weechat.config_read(ircrypt_config_file) def ircrypt_config_write(): - ''' Write IRCrypt configuration file (ircrypt.conf) to disk. - ''' - return weechat.config_write(ircrypt_config_file) + ''' Write IRCrypt configuration file (ircrypt.conf) to disk. + ''' + return weechat.config_write(ircrypt_config_file) def ircrypt_config_keys_read_cb(data, config_file, section_name, option_name, - value): - '''Read elements of the key section from the configuration file. - ''' - ircrypt_keys[option_name.lower()] = value - return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED + value): + '''Read elements of the key section from the configuration file. + ''' + ircrypt_keys[option_name.lower()] = value + return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED def ircrypt_config_keys_write_cb(data, config_file, section_name): - '''Write passphrases to the key section of the configuration file. - ''' - weechat.config_write_line(config_file, section_name, '') - for target, key in sorted(list(ircrypt_keys.items())): - weechat.config_write_line(config_file, target.lower(), key) + '''Write passphrases to the key section of the configuration file. + ''' + weechat.config_write_line(config_file, section_name, '') + for target, key in sorted(list(ircrypt_keys.items())): + weechat.config_write_line(config_file, target.lower(), key) - return weechat.WEECHAT_RC_OK + return weechat.WEECHAT_RC_OK def ircrypt_config_special_cipher_read_cb(data, config_file, section_name, - option_name, value): - '''Read elements of the key section from the configuration file. - ''' - ircrypt_cipher[option_name.lower()] = value - return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED + option_name, value): + '''Read elements of the key section from the configuration file. + ''' + ircrypt_cipher[option_name.lower()] = value + return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED def ircrypt_config_special_cipher_write_cb(data, config_file, section_name): - '''Write passphrases to the key section of the configuration file. - ''' - weechat.config_write_line(config_file, section_name, '') - for target, cipher in sorted(list(ircrypt_cipher.items())): - weechat.config_write_line(config_file, target.lower(), cipher) - return weechat.WEECHAT_RC_OK + '''Write passphrases to the key section of the configuration file. + ''' + weechat.config_write_line(config_file, section_name, '') + for target, cipher in sorted(list(ircrypt_cipher.items())): + weechat.config_write_line(config_file, target.lower(), cipher) + return weechat.WEECHAT_RC_OK def ircrypt_command_list(): - '''List set keys and channel specific ciphers. - ''' - # List keys - keys = '\n'.join([' %s : %s' % x for x in ircrypt_keys.items()]) - ircrypt_info('Symmetric Keys:\n' + keys if keys else 'No symmetric keys set') + '''List set keys and channel specific ciphers. + ''' + # List keys + keys = '\n'.join([' %s : %s' % x for x in ircrypt_keys.items()]) + ircrypt_info('Symmetric Keys:\n' + keys if keys else 'No symmetric keys set') - # List channel specific ciphers - ciphers = '\n'.join([' %s : %s' % x for x in ircrypt_cipher.items()]) - ircrypt_info('Special ciphers:\n' + ciphers if ciphers - else 'No special ciphers set') - return weechat.WEECHAT_RC_OK + # List channel specific ciphers + ciphers = '\n'.join([' %s : %s' % x for x in ircrypt_cipher.items()]) + ircrypt_info('Special ciphers:\n' + ciphers if ciphers + else 'No special ciphers set') + return weechat.WEECHAT_RC_OK def ircrypt_command_set_keys(target, key): - '''Set key for target. + '''Set key for target. - :param target: server/channel combination - :param key: Key to use for target - ''' - ircrypt_keys[target.lower()] = key - ircrypt_info('Set key for %s' % target) - return weechat.WEECHAT_RC_OK + :param target: server/channel combination + :param key: Key to use for target + ''' + ircrypt_keys[target.lower()] = key + ircrypt_info('Set key for %s' % target) + return weechat.WEECHAT_RC_OK def ircrypt_command_remove_keys(target): - '''Remove key for target. + '''Remove key for target. - :param target: server/channel combination - ''' - try: - del ircrypt_keys[target.lower()] - ircrypt_info('Removed key for %s' % target) - except KeyError: - ircrypt_info('No existing key for %s.' % target) - return weechat.WEECHAT_RC_OK + :param target: server/channel combination + ''' + try: + del ircrypt_keys[target.lower()] + ircrypt_info('Removed key for %s' % target) + except KeyError: + ircrypt_info('No existing key for %s.' % target) + return weechat.WEECHAT_RC_OK def ircrypt_command_set_cip(target, cipher): - '''Set cipher for target. + '''Set cipher for target. - :param target: server/channel combination - :param cipher: Cipher to use for target - ''' - ircrypt_cipher[target.lower()] = cipher - ircrypt_info('Set cipher %s for %s' % (cipher, target)) - return weechat.WEECHAT_RC_OK + :param target: server/channel combination + :param cipher: Cipher to use for target + ''' + ircrypt_cipher[target.lower()] = cipher + ircrypt_info('Set cipher %s for %s' % (cipher, target)) + return weechat.WEECHAT_RC_OK def ircrypt_command_remove_cip(target): - '''Remove cipher for target. + '''Remove cipher for target. - :param target: server/channel combination - ''' - try: - del ircrypt_cipher[target.lower()] - ircrypt_info('Removed special cipher. Using default cipher for %s instead.' % target) - except KeyError: - ircrypt_info('No special cipher set for %s.' % target) - return weechat.WEECHAT_RC_OK + :param target: server/channel combination + ''' + try: + del ircrypt_cipher[target.lower()] + ircrypt_info('Removed special cipher. Using default cipher for %s instead.' % target) + except KeyError: + ircrypt_info('No special cipher set for %s.' % target) + return weechat.WEECHAT_RC_OK def ircrypt_command_plain(buffer, server, args, argv): - '''Send unencrypted message - ''' - channel = '' - if (len(argv) > 2 and argv[1] == '-channel'): - channel = argv[2] - args = (args.split(' ', 2)+[''])[2] - else: - # Try to determine the server automatically - channel = weechat.buffer_get_string(buffer, 'localvar_channel') - # If there is no text, just ignore the command - if not args: - return weechat.WEECHAT_RC_OK - marker = weechat.config_string(ircrypt_config_option['unencrypted']) - msg = marker + ' ' + args.split(' ', 1)[-1] - ircrypt_message_plain['%s/%s' % (server, channel)] = (time.time(), msg) - weechat.command('','/msg -server %s %s %s' % \ - (server, channel, msg)) - return weechat.WEECHAT_RC_OK + '''Send unencrypted message + ''' + channel = '' + if (len(argv) > 2 and argv[1] == '-channel'): + channel = argv[2] + args = (args.split(' ', 2)+[''])[2] + else: + # Try to determine the server automatically + channel = weechat.buffer_get_string(buffer, 'localvar_channel') + # If there is no text, just ignore the command + if not args: + return weechat.WEECHAT_RC_OK + marker = weechat.config_string(ircrypt_config_option['unencrypted']) + msg = marker + ' ' + args.split(' ', 1)[-1] + ircrypt_message_plain['%s/%s' % (server, channel)] = (time.time(), msg) + weechat.command('','/msg -server %s %s %s' % \ + (server, channel, msg)) + return weechat.WEECHAT_RC_OK def ircrypt_command(data, buffer, args): - '''Hook to handle the /ircrypt weechat command. - ''' - argv = args.split() - - # list - if not argv or argv == ['list']: - return ircrypt_command_list() - - # Check if a server was set - if (len(argv) > 2 and argv[1] == '-server'): - server = argv[2] - del argv[1:3] - args = (args.split(' ', 2)+[''])[2] - else: - # Try to determine the server automatically - server = weechat.buffer_get_string(buffer, 'localvar_server') - - # All remaining commands need a server name - if not server: - ircrypt_error('Unknown Server. Please use -server to specify server', buffer) - return weechat.WEECHAT_RC_ERROR - - if argv[:1] == ['plain']: - return ircrypt_command_plain(buffer, server, args, argv) - - try: - target = '%s/%s' % (server, argv[1]) - except: - ircrypt_error('Unknown command. Try /help ircrypt', buffer) - return weechat.WEECHAT_RC_OK - - # Set keys - if argv[:1] == ['set-key']: - if len(argv) < 3: - return weechat.WEECHAT_RC_ERROR - return ircrypt_command_set_keys(target, ' '.join(argv[2:])) - - # Remove keys - if argv[:1] == ['remove-key']: - if len(argv) != 2: - return weechat.WEECHAT_RC_ERROR - return ircrypt_command_remove_keys(target) - - # Set special cipher for channel - if argv[:1] == ['set-cipher']: - if len(argv) < 3: - return weechat.WEECHAT_RC_ERROR - return ircrypt_command_set_cip(target, ' '.join(argv[2:])) - - # Remove secial cipher for channel - if argv[:1] == ['remove-cipher']: - if len(argv) != 2: - return weechat.WEECHAT_RC_ERROR - return ircrypt_command_remove_cip(target) - - ircrypt_error('Unknown command. Try /help ircrypt', buffer) - return weechat.WEECHAT_RC_OK + '''Hook to handle the /ircrypt weechat command. + ''' + argv = args.split() + + # list + if not argv or argv == ['list']: + return ircrypt_command_list() + + # Check if a server was set + if (len(argv) > 2 and argv[1] == '-server'): + server = argv[2] + del argv[1:3] + args = (args.split(' ', 2)+[''])[2] + else: + # Try to determine the server automatically + server = weechat.buffer_get_string(buffer, 'localvar_server') + + # All remaining commands need a server name + if not server: + ircrypt_error('Unknown Server. Please use -server to specify server', buffer) + return weechat.WEECHAT_RC_ERROR + + if argv[:1] == ['plain']: + return ircrypt_command_plain(buffer, server, args, argv) + + try: + target = '%s/%s' % (server, argv[1]) + except: + ircrypt_error('Unknown command. Try /help ircrypt', buffer) + return weechat.WEECHAT_RC_OK + + # Set keys + if argv[:1] == ['set-key']: + if len(argv) < 3: + return weechat.WEECHAT_RC_ERROR + return ircrypt_command_set_keys(target, ' '.join(argv[2:])) + + # Remove keys + if argv[:1] == ['remove-key']: + if len(argv) != 2: + return weechat.WEECHAT_RC_ERROR + return ircrypt_command_remove_keys(target) + + # Set special cipher for channel + if argv[:1] == ['set-cipher']: + if len(argv) < 3: + return weechat.WEECHAT_RC_ERROR + return ircrypt_command_set_cip(target, ' '.join(argv[2:])) + + # Remove secial cipher for channel + if argv[:1] == ['remove-cipher']: + if len(argv) != 2: + return weechat.WEECHAT_RC_ERROR + return ircrypt_command_remove_cip(target) + + ircrypt_error('Unknown command. Try /help ircrypt', buffer) + return weechat.WEECHAT_RC_OK def ircrypt_encryption_statusbar(*args): - '''This method will set the “ircrypt” element of the status bar if - encryption is enabled for the current channel. The placeholder {{cipher}} - can be used, which will be replaced with the cipher used for the current - channel. - ''' - channel = weechat.buffer_get_string(weechat.current_buffer(), 'localvar_channel') - server = weechat.buffer_get_string(weechat.current_buffer(), 'localvar_server') - key = ircrypt_keys.get(('%s/%s' % (server, channel)).lower()) + '''This method will set the “ircrypt” element of the status bar if + encryption is enabled for the current channel. The placeholder {{cipher}} + can be used, which will be replaced with the cipher used for the current + channel. + ''' + channel = weechat.buffer_get_string(weechat.current_buffer(), 'localvar_channel') + server = weechat.buffer_get_string(weechat.current_buffer(), 'localvar_server') + key = ircrypt_keys.get(('%s/%s' % (server, channel)).lower()) - # Return nothing if no key is set for current channel - if not key: - return '' + # Return nothing if no key is set for current channel + if not key: + return '' - # Get cipher used for current channel - cipher = weechat.config_string(ircrypt_config_option['sym_cipher']) - cipher = ircrypt_cipher.get(('%s/%s' % (server, channel)).lower(), cipher) + # Get cipher used for current channel + cipher = weechat.config_string(ircrypt_config_option['sym_cipher']) + cipher = ircrypt_cipher.get(('%s/%s' % (server, channel)).lower(), cipher) - # Return marker, but replace {{cipher}} - marker = weechat.config_string(ircrypt_config_option['encrypted']) - return marker.replace('{{cipher}}', cipher) + # Return marker, but replace {{cipher}} + marker = weechat.config_string(ircrypt_config_option['encrypted']) + return marker.replace('{{cipher}}', cipher) def ircrypt_find_gpg_binary(names=('gpg2','gpg')): - '''Check for GnuPG binary to use - :returns: Tuple with binary name and version. - ''' - for binary in names: - p = subprocess.Popen([binary, '--version'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - version = p.communicate()[0].decode('utf-8').split('\n',1)[0] - if not p.returncode: - return binary, version - return None, None + '''Check for GnuPG binary to use + :returns: Tuple with binary name and version. + ''' + for binary in names: + p = subprocess.Popen([binary, '--version'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + version = p.communicate()[0].decode('utf-8').split('\n',1)[0] + if not p.returncode: + return binary, version + return None, None def ircrypt_check_binary(): - '''If binary is not set, try to determine it automatically - ''' - cfg_option = weechat.config_get('ircrypt.general.binary') - gnupg = weechat.config_string(cfg_option) - if not gnupg: - (gnupg, version) = ircrypt_find_gpg_binary(('gpg','gpg2')) - if not gnupg: - ircrypt_error('Automatic detection of the GnuPG binary failed and ' - 'nothing is set manually. You wont be able to use IRCrypt like ' - 'this. Please install GnuPG or set the path to the binary to ' - 'use.', '') - else: - ircrypt_info('Found %s' % version, '') - weechat.config_option_set(cfg_option, gnupg, 1) + '''If binary is not set, try to determine it automatically + ''' + cfg_option = weechat.config_get('ircrypt.general.binary') + gnupg = weechat.config_string(cfg_option) + if not gnupg: + (gnupg, version) = ircrypt_find_gpg_binary(('gpg','gpg2')) + if not gnupg: + ircrypt_error('Automatic detection of the GnuPG binary failed and ' + 'nothing is set manually. You wont be able to use IRCrypt like ' + 'this. Please install GnuPG or set the path to the binary to ' + 'use.', '') + else: + ircrypt_info('Found %s' % version, '') + weechat.config_option_set(cfg_option, gnupg, 1) # register plugin if __name__ == '__main__' and weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, - SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, 'ircrypt_unload_script', - 'UTF-8'): - # register the modifiers - ircrypt_config_init() - ircrypt_config_read() - ircrypt_check_binary() - weechat.hook_modifier('irc_in_privmsg', 'ircrypt_decrypt_hook', '') - weechat.hook_modifier('irc_out_privmsg', 'ircrypt_encrypt_hook', '') - - weechat.hook_command('ircrypt', 'Commands to manage IRCrypt options and execute IRCrypt commands', - '[list]' - '| set-key [-server ] ' - '| remove-key [-server ] ' - '| set-cipher [-server ] ' - '| remove-cipher [-server ] ' - '| plain [-server ] [-channel ] ', - SCRIPT_HELP_TEXT, - 'list || set-key %(irc_channel)|%(nicks)|-server %(irc_servers) %- ' - '|| remove-key %(irc_channel)|%(nicks)|-server %(irc_servers) %- ' - '|| set-cipher %(irc_channel)|-server %(irc_servers) %- ' - '|| remove-cipher |%(irc_channel)|-server %(irc_servers) %- ' - '|| plain |-channel %(irc_channel)|-server %(irc_servers) %-', - 'ircrypt_command', '') - weechat.bar_item_new('ircrypt', 'ircrypt_encryption_statusbar', '') - weechat.hook_signal('ircrypt_buffer_opened', 'update_encryption_status', '') + SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, 'ircrypt_unload_script', + 'UTF-8'): + # register the modifiers + ircrypt_config_init() + ircrypt_config_read() + ircrypt_check_binary() + weechat.hook_modifier('irc_in_privmsg', 'ircrypt_decrypt_hook', '') + weechat.hook_modifier('irc_out_privmsg', 'ircrypt_encrypt_hook', '') + + weechat.hook_command('ircrypt', 'Commands to manage IRCrypt options and execute IRCrypt commands', + '[list]' + '| set-key [-server ] ' + '| remove-key [-server ] ' + '| set-cipher [-server ] ' + '| remove-cipher [-server ] ' + '| plain [-server ] [-channel ] ', + SCRIPT_HELP_TEXT, + 'list || set-key %(irc_channel)|%(nicks)|-server %(irc_servers) %- ' + '|| remove-key %(irc_channel)|%(nicks)|-server %(irc_servers) %- ' + '|| set-cipher %(irc_channel)|-server %(irc_servers) %- ' + '|| remove-cipher |%(irc_channel)|-server %(irc_servers) %- ' + '|| plain |-channel %(irc_channel)|-server %(irc_servers) %-', + 'ircrypt_command', '') + weechat.bar_item_new('ircrypt', 'ircrypt_encryption_statusbar', '') + weechat.hook_signal('ircrypt_buffer_opened', 'update_encryption_status', '') def ircrypt_unload_script(): - '''Hook to ensure the configuration is properly written to disk when the - script is unloaded. - ''' - ircrypt_config_write() - return weechat.WEECHAT_RC_OK + '''Hook to ensure the configuration is properly written to disk when the + script is unloaded. + ''' + ircrypt_config_write() + return weechat.WEECHAT_RC_OK diff --git a/python/irssi_awaylog.py b/python/irssi_awaylog.py index 07439636..b239dcd6 100644 --- a/python/irssi_awaylog.py +++ b/python/irssi_awaylog.py @@ -24,15 +24,15 @@ try: import weechat as wc except Exception: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org/") import_ok = False import time SCRIPT_NAME = "irssi_awaylog" SCRIPT_AUTHOR = "henrik" -SCRIPT_VERSION = "0.3" +SCRIPT_VERSION = "0.4" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Emulates irssis awaylog behaviour" diff --git a/python/irssinotifier.py b/python/irssinotifier.py index bc9d705e..7a1cd3e4 100644 --- a/python/irssinotifier.py +++ b/python/irssinotifier.py @@ -18,6 +18,14 @@ # Requires Weechat >= 0.3.7, openssl # Released under GNU GPL v3 # +# 2018-10-02, Pol Van Aubel +# version 0.9: - Make python3-compatible. +# 2017-05-17, das_aug +# version 0.8.1 - change openssl commandline to how the android app uses it now +# (add "-md md5") +# 2017-05-11, paalka +# version 0.8: - add the ability to store the API token and +# encryption key as secured data. # 2016-01-11, dbendit # version 0.7: - ignore_nicks option # 2014-05-10, Sébastien Helleu @@ -49,12 +57,20 @@ # 2012-10-26, ccm : # version 0.1: - initial release - working proof of concept -import weechat, string, os, urllib, urllib2, shlex +import weechat, os, shlex from subprocess import Popen, PIPE +import sys # Only required for python version check. +PY3 = not sys.version_info < (3,) + +if PY3: + from urllib.parse import urlencode +else: + from urllib import urlencode + weechat.register("irssinotifier", "Caspar Clemens Mierau ", - "0.7", + "0.9", "GPL3", "irssinotifier: Send push notifications to Android's IrssiNotifier about your private message and highligts.", "", @@ -71,7 +87,7 @@ required_settings = ["api_token", "encryption_password"] -for option, help_text in settings.items(): +for option, help_text in list(settings.items()): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, "") @@ -124,21 +140,47 @@ def notify_show(data, bufferp, uber_empty, tagsn, isdisplayed, def encrypt(text): encryption_password = weechat.config_get_plugin("encryption_password") - command="openssl enc -aes-128-cbc -salt -base64 -A -pass env:OpenSSLEncPW" + + # decrypt the password if it is stored as secured data + if encryption_password.startswith("${sec."): + encryption_password = weechat.string_eval_expression(encryption_password, {}, {}, {}) + + if PY3: + text = text.encode("UTF-8") + + command="openssl enc -aes-128-cbc -salt -base64 -md md5 -A -pass env:OpenSSLEncPW" opensslenv = os.environ.copy(); + # Unknown whether the encryption password should or should not be + # (UTF8-)encoded before being passed to the environment in python 3. opensslenv['OpenSSLEncPW'] = encryption_password - output,errors = Popen(shlex.split(command),stdin=PIPE,stdout=PIPE,stderr=PIPE,env=opensslenv).communicate(text+" ") - output = string.replace(output,"/","_") - output = string.replace(output,"+","-") - output = string.replace(output,"=","") + output, errors = Popen(shlex.split(command), stdin=PIPE, stdout=PIPE, + stderr=PIPE,env=opensslenv).communicate(text + b" ") + output = output.replace(b"/", b"_") + output = output.replace(b"+", b"-") + output = output.replace(b"=", b"") + + if PY3: + output = output.decode("UTF-8") + return output def show_notification(chan, nick, message): API_TOKEN = weechat.config_get_plugin("api_token") + + # decrypt the API token if it is stored as secured data + if API_TOKEN.startswith("${sec."): + API_TOKEN = weechat.string_eval_expression(API_TOKEN, {}, {}, {}) + if API_TOKEN != "": url = "https://irssinotifier.appspot.com/API/Message" - postdata = urllib.urlencode({'apiToken':API_TOKEN,'nick':encrypt(nick),'channel':encrypt(chan),'message':encrypt(message),'version':13}) + postdata = urlencode({'apiToken' : API_TOKEN, + 'nick' : encrypt(nick), + 'channel' : encrypt(chan), + 'message' : encrypt(message), + 'version' : 13}) version = weechat.info_get("version_number", "") or 0 - hook1 = weechat.hook_process_hashtable("url:"+url, { "postfields": postdata}, 2000, "", "") + hook1 = weechat.hook_process_hashtable("url:" + url, + {"postfields": postdata}, + 2000, "", "") # vim: autoindent expandtab smarttab shiftwidth=4 diff --git a/python/kbtimeout.py b/python/kbtimeout.py index 9744c0d7..0271fc00 100644 --- a/python/kbtimeout.py +++ b/python/kbtimeout.py @@ -1,9 +1,9 @@ # -# Copyright (c) 2009 by kinabalu (andrew AT mysticcoders DOT com) +# Copyright (c) 2009 - 2018 by kinabalu (https://mysticcoders.com) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, @@ -12,8 +12,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# along with this program. If not, see . # # @@ -21,7 +20,11 @@ # # History: # -# 2009-05-03, kinabalu +# 2022-01-25, Sébastien Helleu +# version 0.3, fix mixed spaces and tabs for indentation +# 2018-07-22, kinabalu (https://mysticcoders.com) +# version 0.2, script cleanup and ensure py3 compatibility +# 2009-05-03, kinabalu # version 0.1, initial version # # /kbtimeout and /kbt can be used in combination with these actions @@ -39,7 +42,7 @@ # For up-to-date information about this script, and new # version downloads, please go to: # -# http://www.mysticcoders.com/apps/kickban-timeout/ +# https://www.mysticcoders.com/ # # If you have any questions, please contact me on-line at: # @@ -48,31 +51,55 @@ # - kinabalu # +import os +import string -import os -import string -import weechat +try: + import weechat +except ImportError: + print('This script has to run under WeeChat (https://weechat.org/).') + sys.exit(1) + +SCRIPT_NAME = 'kbtimeout' +SCRIPT_AUTHOR = 'kinabalu (https://mysticcoders.com)' +SCRIPT_VERSION = '0.3' +SCRIPT_LICENSE = 'GPL3' def handler(data, buffer, argList): - if len(argList.split(" ")) < 2: - weechat.prnt("", "Wrong number of parameters for kbtimeout") - return weechat.WEECHAT_RC_ERROR; - - nick = argList.split(" ")[0] - timeout = argList.split(" ")[1] - + split_args = argList.split(" ") + if len(split_args) < 2: + weechat.prnt("", "Wrong number of parameters for kbtimeout") + return weechat.WEECHAT_RC_ERROR; + + nick = split_args[0] + timeout = split_args[1] + message = "" - if len(argList.split(" ")) > 2: - message = argList.split(" ", 2)[2] + if len(split_args) > 2: + message = argList.split(" ", 2)[2] nick_ptr = weechat.nicklist_search_nick(buffer, "", nick) - + infolist = weechat.infolist_get("irc_nick", "", "{},{}".format("freenode", "##kbtimeout")) + buffer_name = weechat.buffer_get_string(buffer, "name"); - if nick_ptr: - weechat.command(buffer, "/kickban " + nick + " " + message) - weechat.hook_timer(int(timeout) * 1000, 0, 1, "kickban_callback", buffer_name + ":" + nick) - + + found_ban_host = None + while weechat.infolist_next(infolist): + ban_nick = weechat.infolist_string(infolist, "name") + ban_host = weechat.infolist_string(infolist, "host") + ban_account = weechat.infolist_string(infolist, "account") + + if ban_nick == nick: + found_ban_host = ban_host + weechat.infolist_free(infolist) + + found_ban_host = found_ban_host[1:] + + if found_ban_host: + weechat.command(buffer, "/kickban " + found_ban_host + " " + message) + weechat.hook_timer(int(timeout) * 1000, 0, 1, "kickban_callback", buffer_name + ":" + found_ban_host) + return weechat.WEECHAT_RC_OK # END handler @@ -83,10 +110,10 @@ def kickban_callback(data, times_left): if buffer_ptr: weechat.command(buffer_ptr, "/unban " + details[1]) return weechat.WEECHAT_RC_OK -# END kickban_callback - -# *** Script starts here *** +# END kickban_callback -weechat.register("kbtimeout", "kinabalu ", "0.1", "GPL2", "kickban with timeout", "", "") +if __name__ == '__main__': + weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, "kickban with timeout", "", "") -weechat.hook_command("kbtimeout", "Kickban with ban timeout", "[nick] [timeout] [comment]", "kickban nick with comment", "%(irc_channel_nicks_hosts) %-", "handler", "") + weechat.hook_command(SCRIPT_NAME, "Kickban with ban timeout", "[nick] [timeout] [comment]", "kickban nick with comment", "%(irc_channel_nicks_hosts) %-", "handler", "") + weechat.hook_command("kbt", "Kickban with ban timeout", "[nick] [timeout] [comment]", "kickban nick with comment", "%(irc_channel_nicks_hosts) %-", "handler", "") diff --git a/python/keepnick.py b/python/keepnick.py index 2ed858d8..c005f56a 100644 --- a/python/keepnick.py +++ b/python/keepnick.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2012-2014 by nils_2 +# Copyright (c) 2012-2017 by nils_2 # Copyright (c) 2006 by EgS # # script to keep your nick and recover it in case it's occupied @@ -18,6 +18,28 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +# 2023-06-14: Sébastien Helleu +# 1.7 : remove use of infolist variable "ssl_connected" +# +# 2017-10-19: nils_2 (freenode.#weechat) +# 1.6 : fix parsing error, now using weechat.info_get_hashtable() (reported by Mikaela) +# +# 2017-10-14: Kaijo & nils_2 (freenode.#weechat) +# 1.5 : fix empty string breaks output +# : add evaluation for option "text" and use variable "$server" instead of "%s" +# +# 2017-09-06: nils_2 (freenode.#weechat) +# 1.4.2: fix missing weechat.config_string() +# +# 2017-08-17: nils_2 (freenode.#weechat) +# 1.4.1: fix eval_expression for nicks +# : add evaluation for options +# +# 2017-08-17: nils_2 (freenode.#weechat) +# 1.4 : eval_expression for nicks +# : use irc.server..password +# : add short /help +# # 2016-05-12: picasso (freenode.#weechat) # 1.3 : monitor quits and nick changes # @@ -46,7 +68,7 @@ # 2012-02-08: nils_2, (freenode.#weechat) # 0.5 : sync with 0.3.x API (requested by CAHbI4) # -# requires: WeeChat version 0.3.4 +# requires: WeeChat version 1.3 # # Development is currently hosted at # https://github.com/weechatter/weechat-scripts @@ -82,7 +104,7 @@ # -------------------------------[ Constants ]------------------------------------- SCRIPT_NAME = "keepnick" SCRIPT_AUTHOR = "nils_2 " -SCRIPT_VERSION = "1.3" +SCRIPT_VERSION = "1.7" SCRIPT_LICENCE = "GPL3" SCRIPT_DESC = "keep your nick and recover it in case it's occupied" @@ -90,10 +112,10 @@ OPTIONS = { 'delay' : ('600','delay (in seconds) to look at occupied nick (0 means OFF). It is not recommended to flood the server with /ison requests)'), 'timeout' : ('60','timeout (in seconds) to wait for an answer from server.'), - 'serverlist' : ('','comma separated list of servers to look at. Try to register a nickname on server (see: /msg NickServ help).regular expression are allowed (eg. ".*" = matches ALL server,"freen.*" = matches freenode, freenet....)'), - 'text' : ('Nickstealer left Network: %s!','text that will be displayed if your nick will not be occupied anymore. (\"%s\" is a placeholder for the servername)'), - 'nickserv' : ('/msg -server $server NICKSERV IDENTIFY $passwd','Use SASL authentification, if possible. This command will be used to IDENTIFY you on server (following placeholder can be used: \"$server\" for servername; \"$passwd\" for password. The password will be stored in a separate option for every single server: \"plugins.var.python.%s..password\"). Using the "/secure" function, you\'ll have to add a format described in "/help secure" to password option (eg: ${sec.data.keepnick_freenode_password})' % SCRIPT_NAME), - 'command' : ('/nick %s','This command will be used to rename your nick (\"%s\" will be filled with your nickname for specific server)'), + 'serverlist' : ('','comma separated list of servers to look at. Try to register a nickname on server (see: /msg NickServ help).regular expression are allowed (eg. ".*" = matches ALL server,"freen.*" = matches freenode, freenet....) (this option is evaluated).'), + 'text' : ('Nickstealer left Network: $server!','text to display, when you get your nick back. (\"$server and $nick\" can be used) (this option is evaluated).'), + 'nickserv' : ('/msg -server $server NICKSERV IDENTIFY $passwd','Use SASL authentification, if possible. This command will be used to IDENTIFY you on server (following placeholder can be used: \"$server\" for servername; \"$passwd\" for password). You can create an option for every server to store password: \"plugins.var.python.%s..password\", otherwise the \"irc.server..password\" option will be used (this option is evaluated).' % SCRIPT_NAME), + 'command' : ('/nick %s','This command will be used to rename your nick (\"%s\" will be replaced with your nickname)'), 'debug' : ('off', 'When enabled, will output verbose debugging information during script operation'), } HOOK = { 'timer': '', 'redirect': '', 'quit': '', 'nick': '' } @@ -103,7 +125,7 @@ # calling /ison all x seconds using hook:timer() def ison(servername,nick,nicklist): command = ISON % ' '.join(nicklist) - debug_print("Checking nicks with command: %s" % command) + debug_print("Checking nicks on server %s with command: %s" % (servername, command) ) weechat.hook_hsignal_send('irc_redirect_command', { 'server': servername, 'pattern': 'ison', 'signal': SCRIPT_NAME, 'count': '1', 'string': servername, 'timeout': OPTIONS['timeout'], 'cmd_filter': '' }) weechat.hook_signal_send('irc_input_send', weechat.WEECHAT_HOOK_SIGNAL_STRING, '%s;;;;%s' % (servername,command)) @@ -113,13 +135,13 @@ def redirect_isonhandler(data, signal, hashtable): if hashtable['output'] == '': return weechat.WEECHAT_RC_OK - # ISON_nicks contains nicks that are online on server (separated with space) - # nicks in ISON_nicks are lowercase - message,ISON_nicks = hashtable['output'].split(':')[1:] - ISON_nicks = [nick.lower() for nick in ISON_nicks.split()] + parsed = weechat.info_get_hashtable( "irc_message_parse",dict(message=hashtable['output']) ) + # variable ISON_nicks contains online nicks on server (separated with space) + # nicks in variable ISON_nicks are lowercase and 'text' contains the nick + ISON_nicks = [ nick.lower() for nick in parsed['text'].split() ] for nick in server_nicks(hashtable['server']): - mynick = weechat.info_get('irc_nick',hashtable['server']) + mynick = weechat.info_get('irc_nick',hashtable['server']) # current nick on server if nick.lower() == mynick.lower(): debug_print("I already have nick %s; not changing" % mynick) @@ -134,14 +156,15 @@ def redirect_isonhandler(data, signal, hashtable): def server_nicks(servername): infolist = weechat.infolist_get('irc_server','',servername) weechat.infolist_next(infolist) - nicks = weechat.infolist_string(infolist, 'nicks') + nicks = string_eval_expression( weechat.infolist_string(infolist, 'nicks') ) # nicks in config weechat.infolist_free(infolist) return nicks.split(',') def server_enabled(servername): - serverlist = OPTIONS['serverlist'].split(',') - server_matched = re.search(r"\b({})\b".format("|".join(serverlist)), - servername) + serverlist = string_eval_expression( OPTIONS['serverlist'] ).split(',') + + server_matched = re.search(r"\b({})\b".format("|".join(serverlist)),servername) + if servername in serverlist or server_matched: return True else: @@ -154,11 +177,10 @@ def check_nicks(data, remaining_calls): servername = weechat.infolist_string(infolist, 'name') ptr_buffer = weechat.infolist_pointer(infolist,'buffer') nick = weechat.infolist_string(infolist, 'nick') - ssl_connected = weechat.infolist_integer(infolist,'ssl_connected') is_connected = weechat.infolist_integer(infolist,'is_connected') if server_enabled(servername): - if nick and ssl_connected + is_connected: + if nick and is_connected: ison(servername,nick,server_nicks(servername)) weechat.infolist_free(infolist) return weechat.WEECHAT_RC_OK @@ -196,26 +218,28 @@ def my_nick_on_server(servername): def grabnick_and_auth(servername, nick): global OPTIONS - # get password for given server (evaluated) - if int(version) >= 0x00040200: - password = weechat.string_eval_expression( - weechat.config_get_plugin('%s.password' % servername), {}, - {}, {}) - else: - password = weechat.config_get_plugin( - '%s.password' % servername) + + password = string_eval_expression( weechat.config_get_plugin('%s.password' % servername) ) + if not password: + password = string_eval_expression( weechat.config_string(weechat.config_get("irc.server.%s.password" % servername)) ) grabnick(servername, nick) # get your nick back if password != '' and OPTIONS['nickserv'] != '': # command stored in "keepnick.nickserv" option - t = Template(OPTIONS['nickserv']) + t = Template(string_eval_expression( OPTIONS['nickserv']) ) run_msg = t.safe_substitute(server=servername, passwd=password) weechat.command('', run_msg) +def string_eval_expression(string): + return weechat.string_eval_expression(string,{},{},{}) + def grabnick(servername, nick): if nick and servername: - weechat.prnt(weechat.current_buffer(),OPTIONS['text'] % servername) + if OPTIONS['text']: + t = Template( string_eval_expression(OPTIONS['text']) ) + text = t.safe_substitute(server=servername, nick=nick) + weechat.prnt(weechat.current_buffer(), text) weechat.command(weechat.buffer_search('irc','%s.%s' % ('server',servername)), OPTIONS['command'] % nick) # ================================[ weechat hook ]=============================== @@ -277,16 +301,27 @@ def toggle_refresh(pointer, name, value): remove_hooks() # user switched timer off return weechat.WEECHAT_RC_OK +def print_usage(data, buffer, args): + weechat.prnt(buffer, "%s\t%s: script already running..." % ( string_eval_expression(weechat.config_string(weechat.config_get("weechat.look.prefix_error"))), SCRIPT_NAME) ) + return weechat.WEECHAT_RC_OK + # ================================[ main ]=============================== if __name__ == '__main__': - weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENCE, SCRIPT_DESC, '','') - - version = weechat.info_get("version_number", "") or 0 - if int(version) >= 0x00030400: - if int(OPTIONS['delay'][0]) > 0 and int(OPTIONS['timeout'][0]) > 0: - init_options() - install_hooks() - weechat.hook_config( 'plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '' ) - else: - weechat.prnt('','%s%s %s' % (weechat.prefix('error'),SCRIPT_NAME,': needs version 0.3.4 or higher')) - weechat.command('','/wait 1ms /python unload %s' % SCRIPT_NAME) + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENCE, SCRIPT_DESC, '',''): + weechat.hook_command(SCRIPT_NAME,SCRIPT_DESC, + '', + 'You have to edit options with: /set *keepnick*\n' + 'I suggest using /iset script or /fset plugin.\n', + '', + 'print_usage', + '') + + version = weechat.info_get("version_number", "") or 0 + if int(version) >= 0x01030000: + if int(OPTIONS['delay'][0]) > 0 and int(OPTIONS['timeout'][0]) > 0: + init_options() + install_hooks() + weechat.hook_config( 'plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '' ) + else: + weechat.prnt('','%s%s %s' % (weechat.prefix('error'),SCRIPT_NAME,': needs version 1.3 or higher')) + weechat.command('','/wait 1ms /python unload %s' % SCRIPT_NAME) diff --git a/python/kitty_notifications.py b/python/kitty_notifications.py new file mode 100644 index 00000000..017db99f --- /dev/null +++ b/python/kitty_notifications.py @@ -0,0 +1,146 @@ +# MIT License +# +# Copyright (c) Emma Eilefsen Glenna (https://eilefsen.net) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies +# or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# ATTRIBUTIONS: +# +# This script was made by modifying an older script referenced below: +# +# notifications_center (https://github.com/sindresorhus/weechat-notification-center) +# Copyright (c) Sindre Sorhus (https://sindresorhus.com) +# included under the MIT license (https://opensource.org/license/mit/) + +import datetime +import weechat + + +SCRIPT_NAME = "kitty_notifications" +SCRIPT_AUTHOR = "Emma Eilefsen Glenna " +SCRIPT_VERSION = "1.0.0" +SCRIPT_LICENSE = "MIT" +SCRIPT_DESC = "Pass highlights and private messages as OS notifications via the Kitty terminal (OSC 99)" + +weechat.register( + SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "" +) + +WEECHAT_VERSION = weechat.info_get("version_number", "") or 0 +DEFAULT_OPTIONS = { + "show_highlights": "on", + "show_private_message": "on", + "show_message_text": "on", + "ignore_old_messages": "off", + "ignore_current_buffer_messages": "off", + "channels": "", + "tags": "", +} + +for key, val in DEFAULT_OPTIONS.items(): + if not weechat.config_is_set_plugin(key): + weechat.config_set_plugin(key, val) + +weechat.hook_print( + "", "irc_privmsg," + weechat.config_get_plugin("tags"), "", 1, "notify", "" +) + + +def notify( + data: str, + buffer: str, + date: str, + tags: str, + displayed: int, + highlight: int, + prefix: str, + message: str, +) -> int: + # Ignore if it's yourself + own_nick = weechat.buffer_get_string(buffer, "localvar_nick") + if prefix == own_nick or prefix == ("@%s" % own_nick): + return weechat.WEECHAT_RC_OK + + # Ignore messages from the current buffer + if ( + weechat.config_get_plugin("ignore_current_buffer_messages") == "on" + and buffer == weechat.current_buffer() + ): + return weechat.WEECHAT_RC_OK + + # Ignore messages older than the configured threshold (such as ZNC logs) if enabled + if weechat.config_get_plugin("ignore_old_messages") == "on": + message_time = datetime.datetime.fromtimestamp(int(date)) + now_time = datetime.datetime.now() + + # Ignore if the message is greater than 5 seconds old + if (now_time - message_time).seconds > 5: + return weechat.WEECHAT_RC_OK + + channel_allow_list = [] + if weechat.config_get_plugin("channels") != "": + channel_allow_list = weechat.config_get_plugin("channels").split(",") + channel = weechat.buffer_get_string(buffer, "localvar_channel") + + if channel in channel_allow_list: + if weechat.config_get_plugin("show_message_text") == "on": + print_osc99( + f"{prefix} {channel}", + message, + ) + else: + print_osc99( + "Channel Activity", + f"In {channel} by {prefix}", + ) + elif weechat.config_get_plugin("show_highlights") == "on" and int(highlight): + if weechat.config_get_plugin("show_message_text") == "on": + print_osc99( + f"{prefix} {channel}", + message, + ) + else: + print_osc99( + "Highlighted Message", + f"In {channel} by {prefix}", + ) + elif ( + weechat.config_get_plugin("show_private_message") == "on" + and "irc_privmsg" in tags + and "notify_private" in tags + ): + if weechat.config_get_plugin("show_message_text") == "on": + print_osc99( + f"{prefix} [private]", + message, + ) + else: + print_osc99( + "Private Message", + f"From {prefix}", + ) + return weechat.WEECHAT_RC_OK + + +def print_osc99( + title: str, + body: str, +) -> None: + with open("/dev/tty", "w") as tty: + tty.write(f"\x1b]99;i=1:d=0:p=title;{title}\x1b\\") + tty.write(f"\x1b]99;i=1:d=1:p=body;{body}\x1b\\") diff --git a/python/last_written.py b/python/last_written.py new file mode 100644 index 00000000..b51902d7 --- /dev/null +++ b/python/last_written.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019 by nils_2 +# +# last written: provide item to keep track of last buffer you wrote something +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# 2019-06-02: nils_2, (freenode.#weechat) +# 0.1 : initial release + +try: + import weechat,re + +except Exception: + print('This script must be run under WeeChat.') + print('Get WeeChat now at: https://www.weechat.org/') + quit() + +SCRIPT_NAME = 'last_written' +SCRIPT_AUTHOR = 'nils_2 ' +SCRIPT_VERSION = '0.1' +SCRIPT_LICENSE = 'GPL' +SCRIPT_DESC = 'keep track of last buffer you wrote something' + +item_last_written = 'last_written' +item_last_sent = 'last_sent' +last_written = '' +last_sent = '' +# ==============================[ callback ]============================= +# signal_data = buffer_ptr +def input_text_changed_cb(data, signal, signal_data): + global last_written + buffer_name = weechat.buffer_get_string(signal_data, 'name') + if last_written == buffer_name: + return weechat.WEECHAT_RC_OK + last_written = buffer_name + update_item_cb(data,signal,signal_data,'last_written') + return weechat.WEECHAT_RC_OK + +# signal = buffer_ptr +def input_return_cb(data, signal, signal_data): + global last_sent + buffer_name = weechat.buffer_get_string(signal, 'name') + if last_sent == buffer_name: + return weechat.WEECHAT_RC_OK + last_sent = buffer_name + update_item_cb(data,signal,signal_data,'last_sent') + return weechat.WEECHAT_RC_OK +# ================================[ item ]=============================== +def bar_item_last_written_cb(data, item, window): + global last_written + # check for root input bar! + if not window: + window = weechat.current_window() + + # get current buffer (for example for split windows!) + ptr_buffer = weechat.window_get_pointer(window,'buffer') + if ptr_buffer == '': + return '' + return last_written + +def bar_item_last_sent_cb(data, item, window): + global last_written + # check for root input bar! + if not window: + window = weechat.current_window() + + # get current buffer (for example for split windows!) + ptr_buffer = weechat.window_get_pointer(window,'buffer') + if ptr_buffer == '': + return '' + return last_sent + +def update_item_cb(data, signal, signal_data,item_name): + weechat.bar_item_update(item_name) + return weechat.WEECHAT_RC_OK + +def last_written_info_cb(data, info_name, arguments): + global last_written + return last_written + +def last_sent_info_cb(data, info_name, arguments): + global last_sent + return last_sent + +# ================================[ main ]=============================== +if __name__ == '__main__': + global version + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): + version = weechat.info_get('version_number', '') or 0 + + weechat.hook_command(SCRIPT_NAME, SCRIPT_DESC, 'last_written||last_sent', + 'script provide two items:\n' + '"last_written", will print name of buffer you typed text last time\n' + '"last_sent", will print name of buffer you sent text last time\n\n' + 'You can use both items with /eval or in /key using variable "${info:last_written}" and "${info:last_sent}"\n\n' + 'Example:\n' + 'bind key to jump to last buffer you sent text\n' + ' /key bind meta-# /eval /buffer ${info:last_sent}\n' + 'creates an item for text_item.py script (item name ""ti_last_written"\n' + ' /set plugins.var.python.text_item.ti_last_written "all|input_text_changed ${info:last_written} ${info:last_sent}"' + '', + '','','') + + weechat.bar_item_new(item_last_written, 'bar_item_last_written_cb','') + weechat.bar_item_new(item_last_sent, 'bar_item_last_sent_cb','') + weechat.hook_info('last_written', + 'Return name of last buffer text was written', + '', + 'last_written_info_cb', '') + weechat.hook_info('last_sent', + 'Return name of last buffer text was sent', + '', + 'last_sent_info_cb', '') + + weechat.hook_command_run('/input return', 'input_return_cb', '') + weechat.hook_signal ('input_text_changed', 'input_text_changed_cb', '') diff --git a/python/lastfm.py b/python/lastfm.py index 9e4f974c..3aee335b 100644 --- a/python/lastfm.py +++ b/python/lastfm.py @@ -11,6 +11,12 @@ license: GPLv3 history: + 0.9 - 2022-01-25, Sébastien Helleu + Fix mixed spaces and tabs for indentation + + 0.8 - 2020-03-18, prg + Port to python3 + 0.7 - 2016-01-29, timss Fix UnicodeEncodeError @@ -20,10 +26,10 @@ 0.5 - 2014-05-07, Kromonos fixed some simple bugs - 0.4 - 2011-11-21, Jimmy Zelinskie : + 0.4 - 2011-11-21, Jimmy Zelinskie changed default encoding to utf-8 - 0.3 - 2011-03-11, Sebastien Helleu : + 0.3 - 2011-03-11, Sébastien Helleu get python 2.x binary for hook_process (fix problem when python 3.x is default python version) @@ -37,7 +43,7 @@ import weechat import requests -weechat.register("lastfm", "Adam Saponara", "0.7", "GPL3", "Sends your latest Last.fm track to the current buffer", "", "") +weechat.register("lastfm", "Adam Saponara", "0.9", "GPL3", "Sends your latest Last.fm track to the current buffer", "", "") defaults = { "lastfm_username" : "yourusername", @@ -49,7 +55,7 @@ cmd_stdout = "" cmd_stderr = "" -for k, v in defaults.iteritems(): +for k, v in defaults.items(): if not weechat.config_is_set_plugin(k): weechat.config_set_plugin(k, v) @@ -61,16 +67,16 @@ def lastfm_cmd(data, buffer, args): cmd_buffer = buffer cmd_stdout = "" cmd_stderr = "" - python2_bin = weechat.info_get("python2_bin", "") or "python" + python3_bin = weechat.info_get("python3_bin", "") or "python" cmd_hook_process = weechat.hook_process( - python2_bin + " -c \"\n" + python3_bin + " -c \"\n" "import sys, requests\n" "r = requests.get('https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=%(username)s&api_key=618f9ef38b3d0fed172a88c45ae67f33&format=json&limit=1&extended=0')\n" "if not r.status_code == requests.codes.ok:\n" - " print >>sys.stderr, 'Could not fetch Last.fm RSS feed.',\n" - " exit()\n" + " print >>sys.stderr, 'Could not fetch Last.fm RSS feed.',\n" + " exit()\n" "json = r.json()['recenttracks']['track'][0]\n" - "print('{} – {}'.format(json['artist']['#text'].encode('utf-8'), json['name'].encode('utf-8'))),\n" + "print('{} – {}'.format(json['artist']['#text'], json['name'])),\n" "\"" % {"username" : weechat.config_get_plugin('lastfm_username')}, 10000, "lastfm_cb", "") return weechat.WEECHAT_RC_OK diff --git a/python/lastfm2.py b/python/lastfm2.py new file mode 100644 index 00000000..3b415c82 --- /dev/null +++ b/python/lastfm2.py @@ -0,0 +1,130 @@ +# Copyright (c) 2015 by timss +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +SCRIPT_NAME = 'lastfm2' +SCRIPT_AUTHOR = "timss " +SCRIPT_VERSION = '0.2' +SCRIPT_LICENSE = 'GPL3' +SCRIPT_DESC = "Sends latest played track for a Last.fm user to the current buffer" + +SCRIPT_COMMAND = 'lastfm' +SCRIPT_HELP = \ +"""Sends latest played track for a Last.fm user to the current buffer. + + /lastfm + +By default, the script will use the username set in {SCRIPT_NAME} configuration: + + /set plugins.var.python.{SCRIPT_NAME}.user yourusername + +In addition, an username may be specified as an argument: + + /lastfm anotherusername + +The command which output will be sent to the buffer may be customized as well: + + /set plugins.var.python.{SCRIPT_NAME}.command I'm listening to {{track}} + +Finally, the command when specifying another username can also be set: + + /set plugins.var.python.{SCRIPT_NAME}.command_arg {{user}} is litening to {{track}} + +Inspiration and credit: + - lastfm.py, Adam Saponara + - lastfmnp.py, i7c + - lastfmapi.py, Christophe De Troyer + +""".format(SCRIPT_NAME=SCRIPT_NAME) + +try: + import weechat + import_ok = True +except ImportError: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") + import_ok = False + +import json + +def init_config(): + """Set plugin options to defaults if not already done""" + config = { + 'user': '', + 'command': '/me is listening to {track}', + 'command_arg': '{user} is listening to {track}', + 'api_key': 'ae51c9df97d4e90c35ffd302e987efd2', + 'api_url': 'https://ws.audioscrobbler.com/2.0/?method=user.getRecentTracks&user={user}&limit=1&api_key={api_key}&format=json', + 'timeout': '10000' + } + + for option, default in config.items(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, default) + +def get_recent_track(data, command, rc, out, err): + """Get last track played (artist - name)""" + if rc == weechat.WEECHAT_HOOK_PROCESS_ERROR: + weechat.prnt('', "Error with command '{}'".format(command)) + elif rc > 0: + weechat.prnt('', "rc = {}".format(rc)) + + try: + data = json.loads(out) + + if 'error' in data: + weechat.prnt('', "Last.fm API error: '{}'".format(data['message'])) + else: + artist = data['recenttracks']['track'][0]['artist']['#text'] + name = data['recenttracks']['track'][0]['name'] + track = "{} - {}".format(artist, name) + user = data['recenttracks']['@attr']['user'].lower() + + # print username or not, depending on config/arg + if user == weechat.config_get_plugin('user').lower(): + cmd = weechat.config_get_plugin('command') + else: + cmd = weechat.config_get_plugin('command_arg') + + # format isn't picky, ignores {user} if not present + cmd = cmd.format(user=user, track=track) + + weechat.command(weechat.current_buffer(), cmd) + except (IndexError, KeyError): + weechat.prnt('', "Error parsing Last.fm data") + + return weechat.WEECHAT_RC_OK + +def lastfm_cmd(data, buffer, args): + """Print last track played""" + api_key = weechat.config_get_plugin('api_key') + api_url = weechat.config_get_plugin('api_url') + timeout = weechat.config_get_plugin('timeout') + + # use user in argument, or in config + if args: + user = args + else: + user = weechat.config_get_plugin('user') + + url = 'url:' + api_url.format(user=user.lower(), api_key=api_key) + weechat.hook_process(url, int(timeout), 'get_recent_track', '') + + return weechat.WEECHAT_RC_OK + +if __name__ == '__main__' and import_ok: + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): + init_config() + weechat.hook_command(SCRIPT_COMMAND, SCRIPT_HELP, '', '', '', 'lastfm_cmd', '') + diff --git a/python/latex_unicode.py b/python/latex_unicode.py index 23d902ed..cf14c338 100644 --- a/python/latex_unicode.py +++ b/python/latex_unicode.py @@ -1,6 +1,6 @@ -# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: 2016-2019 Simmo Saan # -# Copyright (c) 2016 by Simmo Saan +# SPDX-License-Identifier: GPL-3.0-or-later # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,6 +19,17 @@ # # History: # +# 2025-04-19, Sébastien Helleu : +# version 1.2.1: replace tabs by spaces, add SPDX copyright and license tags, +# remove coding charset +# 2021-05-02, Sébastien Helleu : +# version 1.2: add compatibility with WeeChat >= 3.2 (XDG directories) +# 2019-06-27, Simmo Saan +# version 1.1: fix completion newlines +# 2018-06-19, Simmo Saan +# version 1.0: add Python 2/3 compatibility +# 2016-11-01, Simmo Saan +# version 0.9: remove ungrouped script replacement # 2016-06-30, Simmo Saan # version 0.8: support vulgar fractions and \sqrt # 2016-06-18, Simmo Saan @@ -45,7 +56,7 @@ SCRIPT_NAME = "latex_unicode" SCRIPT_AUTHOR = "Simmo Saan " -SCRIPT_VERSION = "0.8" +SCRIPT_VERSION = "1.2.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Replace LaTeX with unicode representations" @@ -56,29 +67,35 @@ IMPORT_OK = True try: - import weechat + import weechat except ImportError: - print("This script must be run under WeeChat.") - print("Get WeeChat now at: http://www.weechat.org/") - IMPORT_OK = False + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") + IMPORT_OK = False import os import xml.etree.ElementTree as ET import re +import sys + +PY2 = sys.version_info < (3,) +if PY2: + chr = unichr + range = xrange SETTINGS = { - "input": ( - "on", - "replace LaTeX in input display: off, on", - True), - "send": ( - "on", - "replace LaTeX in input sending: off, on", - True), - "buffer": ( - "on", - "replace LaTeX in buffer: off, on", - True) + "input": ( + "on", + "replace LaTeX in input display: off, on", + True), + "send": ( + "on", + "replace LaTeX in input sending: off, on", + True), + "buffer": ( + "on", + "replace LaTeX in buffer: off, on", + True) } SETTINGS_PREFIX = "plugins.var.python.{}.".format(SCRIPT_NAME) @@ -88,329 +105,335 @@ xml_path = None replacements = [] scripts = { - u"0": (u"⁰", u"₀"), - u"1": (u"¹", u"₁"), - u"2": (u"²", u"₂"), - u"3": (u"³", u"₃"), - u"4": (u"⁴", u"₄"), - u"5": (u"⁵", u"₅"), - u"6": (u"⁶", u"₆"), - u"7": (u"⁷", u"₇"), - u"8": (u"⁸", u"₈"), - u"9": (u"⁹", u"₉"), - - u"A": (u"ᴬ", None), - u"B": (u"ᴮ", None), - u"D": (u"ᴰ", None), - u"E": (u"ᴱ", None), - u"G": (u"ᴳ", None), - u"H": (u"ᴴ", None), - u"I": (u"ᴵ", None), - u"J": (u"ᴶ", None), - u"K": (u"ᴷ", None), - u"L": (u"ᴸ", None), - u"M": (u"ᴹ", None), - u"N": (u"ᴺ", None), - u"O": (u"ᴼ", None), - u"P": (u"ᴾ", None), - u"R": (u"ᴿ", None), - u"T": (u"ᵀ", None), - u"U": (u"ᵁ", None), - u"V": (u"ⱽ", None), - u"W": (u"ᵂ", None), - - u"a": (u"ᵃ", u"ₐ"), - u"b": (u"ᵇ", None), - u"c": (u"ᶜ", None), - u"d": (u"ᵈ", None), - u"e": (u"ᵉ", u"ₑ"), - u"f": (u"ᶠ", None), - u"g": (u"ᵍ", None), - u"h": (u"ʰ", u"ₕ"), - u"i": (u"ⁱ", u"ᵢ"), - u"j": (u"ʲ", u"ⱼ"), - u"k": (u"ᵏ", u"ₖ"), - u"l": (u"ˡ", u"ₗ"), - u"m": (u"ᵐ", u"ₘ"), - u"n": (u"ⁿ", u"ₙ"), - u"o": (u"ᵒ", u"ₒ"), - u"p": (u"ᵖ", u"ₚ"), - u"r": (u"ʳ", u"ᵣ"), - u"s": (u"ˢ", u"ₛ"), - u"t": (u"ᵗ", u"ₜ"), - u"u": (u"ᵘ", u"ᵤ"), - u"v": (u"ᵛ", u"ᵥ"), - u"w": (u"ʷ", None), - u"x": (u"ˣ", u"ₓ"), - u"y": (u"ʸ", None), - u"z": (u"ᶻ", None), - - u"+": (u"⁺", u"₊"), - u"-": (u"⁻", u"₋"), - u"=": (u"⁼", u"₌"), - u"(": (u"⁽", u"₍"), - u")": (u"⁾", u"₎"), - - u"β": (u"ᵝ", u"ᵦ"), - u"γ": (u"ᵞ", u"ᵧ"), - u"δ": (u"ᵟ", None), - u"θ": (u"ᶿ", None), - u"ι": (u"ᶥ", None), - u"ρ": (None, u"ᵨ"), - u"φ": (u"ᵠ", u"ᵩ"), - u"χ": (u"ᵡ", u"ᵪ"), + u"0": (u"⁰", u"₀"), + u"1": (u"¹", u"₁"), + u"2": (u"²", u"₂"), + u"3": (u"³", u"₃"), + u"4": (u"⁴", u"₄"), + u"5": (u"⁵", u"₅"), + u"6": (u"⁶", u"₆"), + u"7": (u"⁷", u"₇"), + u"8": (u"⁸", u"₈"), + u"9": (u"⁹", u"₉"), + + u"A": (u"ᴬ", None), + u"B": (u"ᴮ", None), + u"D": (u"ᴰ", None), + u"E": (u"ᴱ", None), + u"G": (u"ᴳ", None), + u"H": (u"ᴴ", None), + u"I": (u"ᴵ", None), + u"J": (u"ᴶ", None), + u"K": (u"ᴷ", None), + u"L": (u"ᴸ", None), + u"M": (u"ᴹ", None), + u"N": (u"ᴺ", None), + u"O": (u"ᴼ", None), + u"P": (u"ᴾ", None), + u"R": (u"ᴿ", None), + u"T": (u"ᵀ", None), + u"U": (u"ᵁ", None), + u"V": (u"ⱽ", None), + u"W": (u"ᵂ", None), + + u"a": (u"ᵃ", u"ₐ"), + u"b": (u"ᵇ", None), + u"c": (u"ᶜ", None), + u"d": (u"ᵈ", None), + u"e": (u"ᵉ", u"ₑ"), + u"f": (u"ᶠ", None), + u"g": (u"ᵍ", None), + u"h": (u"ʰ", u"ₕ"), + u"i": (u"ⁱ", u"ᵢ"), + u"j": (u"ʲ", u"ⱼ"), + u"k": (u"ᵏ", u"ₖ"), + u"l": (u"ˡ", u"ₗ"), + u"m": (u"ᵐ", u"ₘ"), + u"n": (u"ⁿ", u"ₙ"), + u"o": (u"ᵒ", u"ₒ"), + u"p": (u"ᵖ", u"ₚ"), + u"r": (u"ʳ", u"ᵣ"), + u"s": (u"ˢ", u"ₛ"), + u"t": (u"ᵗ", u"ₜ"), + u"u": (u"ᵘ", u"ᵤ"), + u"v": (u"ᵛ", u"ᵥ"), + u"w": (u"ʷ", None), + u"x": (u"ˣ", u"ₓ"), + u"y": (u"ʸ", None), + u"z": (u"ᶻ", None), + + u"+": (u"⁺", u"₊"), + u"-": (u"⁻", u"₋"), + u"=": (u"⁼", u"₌"), + u"(": (u"⁽", u"₍"), + u")": (u"⁾", u"₎"), + + u"β": (u"ᵝ", u"ᵦ"), + u"γ": (u"ᵞ", u"ᵧ"), + u"δ": (u"ᵟ", None), + u"θ": (u"ᶿ", None), + u"ι": (u"ᶥ", None), + u"ρ": (None, u"ᵨ"), + u"φ": (u"ᵠ", u"ᵩ"), + u"χ": (u"ᵡ", u"ᵪ"), } # https://en.wikipedia.org/wiki/Number_Forms vulgar_fractions = { - (u"1", u"4"): u"¼", - (u"1", u"2"): u"½", - (u"3", u"4"): u"¾", - (u"1", u"4"): u"¼", - (u"1", u"7"): u"⅐", - (u"1", u"9"): u"⅑", - (u"1", u"10"): u"⅒", - (u"1", u"3"): u"⅓", - (u"2", u"3"): u"⅔", - (u"1", u"5"): u"⅕", - (u"2", u"5"): u"⅖", - (u"3", u"5"): u"⅗", - (u"4", u"5"): u"⅘", - (u"1", u"6"): u"⅙", - (u"5", u"6"): u"⅚", - (u"1", u"8"): u"⅛", - (u"3", u"8"): u"⅜", - (u"5", u"8"): u"⅝", - (u"7", u"8"): u"⅞", - (u"0", u"3"): u"↉", + (u"1", u"4"): u"¼", + (u"1", u"2"): u"½", + (u"3", u"4"): u"¾", + (u"1", u"4"): u"¼", + (u"1", u"7"): u"⅐", + (u"1", u"9"): u"⅑", + (u"1", u"10"): u"⅒", + (u"1", u"3"): u"⅓", + (u"2", u"3"): u"⅔", + (u"1", u"5"): u"⅕", + (u"2", u"5"): u"⅖", + (u"3", u"5"): u"⅗", + (u"4", u"5"): u"⅘", + (u"1", u"6"): u"⅙", + (u"5", u"6"): u"⅚", + (u"1", u"8"): u"⅛", + (u"3", u"8"): u"⅜", + (u"5", u"8"): u"⅝", + (u"7", u"8"): u"⅞", + (u"0", u"3"): u"↉", } def log(string): - """Log script's message to core buffer.""" + """Log script's message to core buffer.""" - weechat.prnt("", "{}: {}".format(SCRIPT_NAME, string)) + weechat.prnt("", "{}: {}".format(SCRIPT_NAME, string)) def error(string): - """Log script's error to core buffer.""" + """Log script's error to core buffer.""" - weechat.prnt("", "{}{}: {}".format(weechat.prefix("error"), SCRIPT_NAME, string)) + weechat.prnt("", "{}{}: {}".format(weechat.prefix("error"), SCRIPT_NAME, string)) def setup(): - """Load replacements from available resource.""" + """Load replacements from available resource.""" - global xml_path - xml_path = weechat.string_eval_path_home("%h/latex_unicode.xml", "", "", "") + global xml_path + options = { + "directory": "cache", + } + xml_path = weechat.string_eval_path_home("%h/latex_unicode.xml", {}, {}, options) - if os.path.isfile(xml_path): - setup_from_file() - else: - setup_from_url() + if os.path.isfile(xml_path): + setup_from_file() + else: + setup_from_url() def setup_from_url(): - """Download replacements and store them in weechat home directory.""" + """Download replacements and store them in weechat home directory.""" - log("downloading XML...") - weechat.hook_process_hashtable("url:https://www.w3.org/Math/characters/unicode.xml", - { - "file_out": xml_path - }, - 30000, "download_cb", "") + log("downloading XML...") + weechat.hook_process_hashtable("url:https://www.w3.org/Math/characters/unicode.xml", + { + "file_out": xml_path + }, + 30000, "download_cb", "") def download_cb(data, command, return_code, out, err): - """Load downloaded replacements.""" + """Load downloaded replacements.""" - log("downloaded XML") - setup_from_file() - return weechat.WEECHAT_RC_OK + log("downloaded XML") + setup_from_file() + return weechat.WEECHAT_RC_OK def setup_from_file(): - """Load replacements from file in weechat home directory.""" + """Load replacements from file in weechat home directory.""" + + log("loading XML...") + global replacements - log("loading XML...") - global replacements + root = ET.parse(xml_path) + for character in root.findall("character"): + dec = character.get("dec") + if "-" not in dec: # is not a range of characters + char = chr(int(dec)) - root = ET.parse(xml_path) - for character in root.findall("character"): - dec = character.get("dec") - if "-" not in dec: # is not a range of characters - char = unichr(int(dec)) - - ams = character.find("AMS") - if ams is not None: - replacements.append((ams.text, char)) + ams = character.find("AMS") + if ams is not None: + replacements.append((ams.text, char)) - latex = character.find("latex") - if latex is not None: - latex = latex.text.strip() - if latex[0] == "\\": # only add \commands - replacements.append((latex, char)) + latex = character.find("latex") + if latex is not None: + latex = latex.text.strip() + if latex[0] == "\\": # only add \commands + replacements.append((latex, char)) - replacements = sorted(replacements, key=lambda replacement: len(replacement[0]), reverse=True) # sort by tex string length descendingly + replacements = sorted(replacements, key=lambda replacement: len(replacement[0]), reverse=True) # sort by tex string length descendingly - log("loaded XML") - hook_modifiers() + log("loaded XML") + hook_modifiers() def hook_modifiers(): - """Update modifier hooks to match settings.""" + """Update modifier hooks to match settings.""" - # remove existing modifier hooks - global hooks - for hook in hooks: - weechat.unhook(hook) - hooks = [] + # remove existing modifier hooks + global hooks + for hook in hooks: + weechat.unhook(hook) + hooks = [] - # add hooks according to settings + # add hooks according to settings - input_option = weechat.config_get_plugin("input") - if weechat.config_string_to_boolean(input_option): - hooks.append(weechat.hook_modifier("input_text_display", "modifier_cb", "")) + input_option = weechat.config_get_plugin("input") + if weechat.config_string_to_boolean(input_option): + hooks.append(weechat.hook_modifier("input_text_display", "modifier_cb", "")) - send_option = weechat.config_get_plugin("send") - if weechat.config_string_to_boolean(send_option): - hooks.append(weechat.hook_modifier("input_text_for_buffer", "modifier_cb", "")) + send_option = weechat.config_get_plugin("send") + if weechat.config_string_to_boolean(send_option): + hooks.append(weechat.hook_modifier("input_text_for_buffer", "modifier_cb", "")) - buffer_option = weechat.config_get_plugin("buffer") - if weechat.config_string_to_boolean(buffer_option): - hooks.append(weechat.hook_modifier("weechat_print", "modifier_cb", "")) + buffer_option = weechat.config_get_plugin("buffer") + if weechat.config_string_to_boolean(buffer_option): + hooks.append(weechat.hook_modifier("weechat_print", "modifier_cb", "")) def replace_xml_replacements(string): - """Apply XML replacements to message.""" + """Apply XML replacements to message.""" - for tex, char in replacements: - string = string.replace(tex, char) - return string + for tex, char in replacements: + string = string.replace(tex, char) + return string def latex_ungroup(string): - """Remove grouping curly braces if present.""" + """Remove grouping curly braces if present.""" - grouped = string.startswith("{") and string.endswith("}") - if grouped: - string = string[1:-1] - return string + grouped = string.startswith("{") and string.endswith("}") + if grouped: + string = string[1:-1] + return string def replace_script(string, script): - """Regex substitution function for scripts.""" + """Regex substitution function for scripts.""" + + grouped = string.startswith("{") and string.endswith("}") + if grouped: + string = string[1:-1] + + chars = list(string) + all = True + for i in range(len(chars)): + if chars[i] in scripts and scripts[chars[i]][script] is not None: + chars[i] = scripts[chars[i]][script] + else: + all = False + break + + if all: + return "".join(chars) + else: + return None - grouped = string.startswith("{") and string.endswith("}") - if grouped: - string = string[1:-1] - - if script == 1 and not grouped and string.isalpha(): # if ungrouped letter subscript - return None +def replace_scripts(string): + """Apply super- and subscript replacements to message.""" - chars = list(string) - all = True - for i in xrange(len(chars)): - if chars[i] in scripts and scripts[chars[i]][script] is not None: - chars[i] = scripts[chars[i]][script] - else: - all = False - break + def replace(match, script): + replaced = replace_script(match.group(1), script) + return replaced if replaced is not None else match.group(0) - if all: - return "".join(chars) - else: - return None + string = re.sub(r"\^({[^}]+})", lambda match: replace(match, 0), string, flags=re.UNICODE) + string = re.sub(r"_({[^}]+})", lambda match: replace(match, 1), string, flags=re.UNICODE) -def replace_scripts(string): - """Apply super- and subscript replacements to message.""" + def replace_frac(match): + vulgar_pair = (latex_ungroup(match.group(1)), latex_ungroup(match.group(2))) + if vulgar_pair in vulgar_fractions: + return vulgar_fractions[vulgar_pair] - def replace(match, script): - replaced = replace_script(match.group(1), script) - return replaced if replaced is not None else match.group(0) + replaced1 = replace_script(match.group(1), 0) + replaced2 = replace_script(match.group(2), 1) + if replaced1 is not None and replaced2 is not None: + return replaced1 + u"⁄" + replaced2 + else: + return match.group(0) - string = re.sub(r"\^({[^}]+}|[^{}])", lambda match: replace(match, 0), string, flags=re.UNICODE) - string = re.sub(r"_({[^}]+}|[^{}])", lambda match: replace(match, 1), string, flags=re.UNICODE) + string = re.sub(r"\\frac({[^}]+}|[^{}])({[^}]+}|[^{}])", replace_frac, string, flags=re.UNICODE) - def replace_frac(match): - vulgar_pair = (latex_ungroup(match.group(1)), latex_ungroup(match.group(2))) - if vulgar_pair in vulgar_fractions: - return vulgar_fractions[vulgar_pair] + def replace_sqrt(match): + under = match.group(2) + if under is not None: + under = re.sub(r"(.)", u"\\1\u0305", latex_ungroup(under), flags=re.UNICODE) + else: + under = "" - replaced1 = replace_script(match.group(1), 0) - replaced2 = replace_script(match.group(2), 1) - if replaced1 is not None and replaced2 is not None: - return replaced1 + u"⁄" + replaced2 - else: - return match.group(0) + if match.group(1) is None: + return u"√" + under - string = re.sub(r"\\frac({[^}]+}|[^{}])({[^}]+}|[^{}])", replace_frac, string, flags=re.UNICODE) + replaced = replace_script(match.group(1), 0) + if replaced is not None: + return replaced + u"√" + under + else: + return match.group(0) - def replace_sqrt(match): - under = match.group(2) - if under is not None: - under = re.sub(r"(.)", u"\\1\u0305", latex_ungroup(under), flags=re.UNICODE) - else: - under = "" + string = re.sub(r"\\sqrt(?:\[([^\]]+)\])?({[^}]+}|[^{}])?", replace_sqrt, string, flags=re.UNICODE) - if match.group(1) is None: - return u"√" + under + return string - replaced = replace_script(match.group(1), 0) - if replaced is not None: - return replaced + u"√" + under - else: - return match.group(0) +def latex_unicode_replace(string): + """Apply all latex_unicode replacements.""" - string = re.sub(r"\\sqrt(?:\[([^\]]+)\])?({[^}]+}|[^{}])?", replace_sqrt, string, flags=re.UNICODE) + if PY2: + string = string.decode("utf-8") - return string + string = replace_xml_replacements(string) + string = replace_scripts(string) -def latex_unicode_replace(string): - """Apply all latex_unicode replacements.""" + if PY2: + string = string.encode("utf-8") - string = string.decode("utf-8") - string = replace_xml_replacements(string) - string = replace_scripts(string) - return string.encode("utf-8") + return string def modifier_cb(data, modifier, modifier_data, string): - """Handle modifier hooks.""" + """Handle modifier hooks.""" - return latex_unicode_replace(string) + return latex_unicode_replace(string) def command_cb(data, buffer, args): - """Handle command hook.""" + """Handle command hook.""" - args = args.split() + args = args.split() - if len(args) >= 1: - if args[0] == "reload": - setup_from_file() - return weechat.WEECHAT_RC_OK - elif args[0] == "redownload": - setup_from_url() - return weechat.WEECHAT_RC_OK + if len(args) >= 1: + if args[0] == "reload": + setup_from_file() + return weechat.WEECHAT_RC_OK + elif args[0] == "redownload": + setup_from_url() + return weechat.WEECHAT_RC_OK - error("invalid arguments") - return weechat.WEECHAT_RC_ERROR + error("invalid arguments") + return weechat.WEECHAT_RC_ERROR def config_cb(data, option, value): - """Handle config hooks (option changes).""" + """Handle config hooks (option changes).""" - option = option[len(SETTINGS_PREFIX):] + option = option[len(SETTINGS_PREFIX):] - if SETTINGS[option][2]: # if option requires modifier hooks update - hook_modifiers() + if SETTINGS[option][2]: # if option requires modifier hooks update + hook_modifiers() - return weechat.WEECHAT_RC_OK + return weechat.WEECHAT_RC_OK if __name__ == "__main__" and IMPORT_OK: - if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - weechat.hook_command(SCRIPT_COMMAND, SCRIPT_DESC, + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + weechat.hook_command(SCRIPT_COMMAND, SCRIPT_DESC, """reload || redownload""", """ reload: reload replacements from XML file redownload: redownload replacements XML file and load it""", """reload - || redownload""", - "command_cb", "") + || redownload""".replace("\n", ""), + "command_cb", "") - for option, value in SETTINGS.items(): - if not weechat.config_is_set_plugin(option): - weechat.config_set_plugin(option, value[0]) + for option, value in SETTINGS.items(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value[0]) - weechat.config_set_desc_plugin(option, "%s (default: \"%s\")" % (value[1], value[0])) + weechat.config_set_desc_plugin(option, "%s (default: \"%s\")" % (value[1], value[0])) - weechat.hook_config(SETTINGS_PREFIX + "*", "config_cb", "") + weechat.hook_config(SETTINGS_PREFIX + "*", "config_cb", "") - setup() + setup() diff --git a/python/leet.py b/python/leet.py index 3421cbdb..241a69fc 100644 --- a/python/leet.py +++ b/python/leet.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: 2011 Andy Pilate (Lenoob) # -### -# Copyright (c) 2011, Andy Pilate (Lenoob ) -# All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -36,14 +34,18 @@ # Changelog # 0.1 # First version +# +# 0.2 +# Added support for python3 while retaining python2 support +# import weechat as w import re SCRIPT_NAME = "leet" SCRIPT_AUTHOR = "Lenoob" -SCRIPT_VERSION = "0.1" -SCRIPT_LICENSE = "GPL3" +SCRIPT_VERSION = "0.2.1" +SCRIPT_LICENSE = "BSD-3-Clause" SCRIPT_DESC = "Convert text to leet" settings = {} @@ -103,7 +105,7 @@ if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - for option, default_value in settings.iteritems(): + for option, default_value in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_value) w.hook_command("leet", @@ -124,7 +126,12 @@ def leet_cmd_cb(data, buffer, args): if char in replacements: char = replacements[char] outstring += char - outstring = outstring.encode('UTF-8') + if isinstance(outstring, str): + pass + elif isinstance(outstring, bytes): + outstring = outstring.decode('UTF-8') + elif isinstance(outstring, unicode): + outstring = outstring.encode('UTF-8') w.buffer_set(buffer, 'input', outstring) w.buffer_set(buffer, 'input_pos', '%d' % len(outstring)) return w.WEECHAT_RC_OK diff --git a/python/listbuffer.py b/python/listbuffer.py index 227a3bae..a8843bea 100644 --- a/python/listbuffer.py +++ b/python/listbuffer.py @@ -68,6 +68,10 @@ # * plugins.var.python.listbuffer.channel_min_width # * plugins.var.python.listbuffer.users_min_width # +### 2019-07-05: Sébastien Helleu: +# +# * version 0.8.2: Make script compatible with Python 3. +# ## Acknowledgements: # * Dmitry "troydm" Geurkov, for providing the inverse-sorting patch to the project. # * Sebastien "Flashcode" Helleu, for developing the kick-ass IRC client WeeChat @@ -120,9 +124,12 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # + +from __future__ import print_function + SCRIPT_NAME = "listbuffer" SCRIPT_AUTHOR = "Filip H.F. 'FiXato' Slagter " -SCRIPT_VERSION = "0.8.1" +SCRIPT_VERSION = "0.8.2" SCRIPT_LICENSE = "MIT" SCRIPT_DESC = "A common buffer for /list output." SCRIPT_COMMAND = "listbuffer" @@ -132,7 +139,7 @@ try: import weechat except ImportError: - print "This script must be run under WeeChat." + print("This script must be run under WeeChat.") import_ok = False import re diff --git a/python/lnotify.py b/python/lnotify.py index feb27557..c4289de9 100644 --- a/python/lnotify.py +++ b/python/lnotify.py @@ -1,7 +1,7 @@ # Project: lnotify # Description: A libnotify script for weechat. Uses # subprocess.call to execute notify-send with arguments. -# Author: kevr +# Author: kevr # License: GPL3 # # 0.1.2 @@ -36,13 +36,24 @@ # 0.3.1 # Fix https://github.com/weechat/scripts/issues/114 - where we would get # notifications for messages that we sent +# +# 0.3.2 +# Check if active window is in the ignore_windows_list and skip notification +# +# 0.3.3 +# Fix undefined ignore_windows_list. +# +# 0.3.5 +# Fix ps call generation in Python 3 + +from __future__ import unicode_literals import weechat as weechat import subprocess from os import environ, path lnotify_name = "lnotify" -lnotify_version = "0.3.1" +lnotify_version = "0.3.5" lnotify_license = "GPL3" # convenient table checking for bools @@ -91,13 +102,16 @@ def handle_msg(data, pbuffer, date, tags, displayed, highlight, prefix, message) window_name = "" my_nickname = "nick_" + weechat.buffer_get_string(pbuffer, "localvar_nick") - # Check to make sure we're in X and xdotool exists. - # This is kinda crude, but I'm no X master. + # Check if active window is in the ignore_windows_list and skip notification if (environ.get('DISPLAY') != None) and path.isfile("/bin/xdotool"): - window_name = subprocess.check_output(["xdotool", "getwindowfocus", "getwindowname"]) - - if "WeeChat" in window_name: - x_focus = True + cmd_pid="xdotool getactivewindow getwindowpid".split() + window_pid = subprocess.check_output(cmd_pid).decode("utf-8") + cmd_name=("ps -ho comm -p %s"%(window_pid)).split() + window_name = subprocess.check_output(cmd_name).decode("utf-8") + ignore_windows_list = ["tilda", "gnome-terminal", "xterm"] + if window_name in ignore_windows_list: + x_focus = True + return weechat.WEECHAT_RC_OK if pbuffer == weechat.current_buffer() and x_focus: return weechat.WEECHAT_RC_OK @@ -142,4 +156,3 @@ def notify_user(origin, message): cfg = config() print_hook = weechat.hook_print("", "", "", 1, "handle_msg", "") - diff --git a/python/log.py b/python/log.py new file mode 100644 index 00000000..2ca8a3a8 --- /dev/null +++ b/python/log.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019 by nils_2 +# +# for easy toggling current buffer logging. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# 2019-06-24: nils_2, (freenode.#weechat) +# 0.1 : initial version +# +# requires: WeeChat version 1.0 +# +# Development is currently hosted at +# https://github.com/weechatter/weechat-scripts + +try: + import weechat,re + +except Exception: + print('This script must be run under WeeChat.') + print('Get WeeChat now at: https://www.weechat.org/') + quit() + +SCRIPT_NAME = 'log' +SCRIPT_AUTHOR = 'nils_2 ' +SCRIPT_VERSION = '0.1' +SCRIPT_LICENSE = 'GPL' +SCRIPT_DESC = 'for easy toggling current buffer logging' + +# eval_expression(): to match ${color:nn} tags +regex_color=re.compile('\$\{color:([^\{\}]+)\}') + +# ==========================[ eval_expression() ]======================== +def substitute_colors(text,window): + global version + if int(version) >= 0x00040200: + buffer_ptr = weechat.window_get_pointer(window,"buffer") + return weechat.string_eval_expression(text, {"window": window, "buffer": buffer_ptr}, {}, {}) + # substitute colors in output + return re.sub(regex_color, lambda match: weechat.color(match.group(1)), text) + +# ============================[ subroutines ]============================ +def log_cmd_cb(data, buffer, args): + argv = args.strip().split(' ') + + log_level = infolist_log_buffer(buffer) + + if args == "" or (argv[0].lower() == 'show'): + # no args given. display log level of current buffer + weechat.prnt(buffer,'log level: %s' % log_level) + return weechat.WEECHAT_RC_OK + + if (argv[0].lower() == 'enable') or (argv[0].lower() == 'on'): + if log_level != 'disabled': + return weechat.WEECHAT_RC_OK # buffer already logging! + else: + enable_check(log_level,buffer) + return weechat.WEECHAT_RC_OK + + if (argv[0].lower() == 'disable') or (argv[0].lower() == 'off'): + if log_level == 'disabled': + return weechat.WEECHAT_RC_OK # buffer already disabled! + else: + disable_check(log_level,buffer) + return weechat.WEECHAT_RC_OK + + if (argv[0].lower() == 'toggle'): + if log_level == 'disabled': + enable_check(log_level,buffer) + else: + disable_check(log_level,buffer) + return weechat.WEECHAT_RC_OK + + return weechat.WEECHAT_RC_OK + +# ===============================[ logger() ]============================= +def enable_check(log_level,buffer): + log_level = buffer_get_string_log_level(buffer) + if log_level: + if not str(log_level).isnumeric() or (int(log_level) < 0) or (int(log_level) > 9): + log_level = 9 # invalid log level, set default + weechat.command(buffer,'/logger set %s' % log_level) + buffer_del_log_level(buffer) + else: # no logging and no localvar. + weechat.command(buffer,'/logger set 9') + return weechat.WEECHAT_RC_OK + +def disable_check(log_level,buffer): + buffer_set_string_log_level(buffer,log_level) # store old log level in localvar! + weechat.command(buffer,'/logger disable') + return weechat.WEECHAT_RC_OK + +# =============================[ localvars() ]============================ +def buffer_get_string_log_level(buffer): + return weechat.buffer_get_string(buffer,'localvar_log_level') + +def buffer_set_string_log_level(buffer,log_level): + weechat.buffer_set(buffer, 'localvar_set_log_level', '%s' % log_level) + return weechat.WEECHAT_RC_OK + +def buffer_del_log_level(buffer): + weechat.command(buffer,'/buffer set localvar_del_log_level') + return weechat.WEECHAT_RC_OK + +# =============================[ infolist() ]============================ +def infolist_log_buffer(ptr_buffer): + log_level = None + infolist = weechat.infolist_get('logger_buffer','','') + while weechat.infolist_next(infolist): + bpointer = weechat.infolist_pointer(infolist, 'buffer') + if ptr_buffer == bpointer: + log_enabled = weechat.infolist_integer(infolist, 'log_enabled') + log_level = weechat.infolist_integer(infolist, 'log_level') + + weechat.infolist_free(infolist) # free infolist() + if not log_level: + return 'disabled' + else: + return log_level +# ================================[ main ]=============================== +if __name__ == '__main__': + global version + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): + version = weechat.info_get('version_number', '') or 0 + + # get weechat version (1.0) and store it + if int(version) >= 0x01000000: + + weechat.hook_command(SCRIPT_NAME,SCRIPT_DESC, + 'enable|on||' + 'disable|off||' + 'toggle||' + 'show', + ' enable/on: enable logging on current buffer, with default log-level (note: log-level from localvar will be used, if possible)\n' + 'disable/off: disable logging on current buffer (note: log-level is stored in localvar)\n' + ' toggle: will toggle logging on current buffer\n' + ' show: will print current log-level to buffer (default)\n' + '\n' + 'Examples:\n' + ' /log toggle', + 'enable||' + 'disable||' + 'on||' + 'off||' + 'toggle||' + 'show', + 'log_cmd_cb', '') + + else: + weechat.prnt('','%s%s %s' % (weechat.prefix('error'),SCRIPT_NAME,': needs version 1.0 or higher')) + weechat.command('','/wait 1ms /python unload %s' % SCRIPT_NAME) diff --git a/python/logsize.py b/python/logsize.py index 4043be79..e38fa327 100644 --- a/python/logsize.py +++ b/python/logsize.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2012-2013 by nils_2 +# Copyright (c) 2012-2019 by nils_2 # # Display size of current logfile in item-bar # @@ -17,6 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +# 2019-07-12: nils_2 (freenode.#weechat) +# 0.4 : option "display" is evaluated +# : use hook_process("wc") to not stall weechat anymore +# : new function: refresh only on buffer/window switch +# : make script compatible with Python 3.x # 2013-01-07: nils_2 (freenode.#weechat) # 0.3 : missing logfile caused a crash (thanks swimmer) # : add support of more than one window @@ -29,42 +34,45 @@ # How to use: # add item "logsize" to option "weechat.bar.status.items" # -# CAVE: -# USE OPTION: plugins.var.python.logsize.display "lines" VERY CAREFULLY -# Very large logfiles will stall script and weechat. -# # Development is currently hosted at # https://github.com/weechatter/weechat-scripts +from __future__ import print_function +from builtins import str + try: - import weechat - import os, os.path, stat, time - from datetime import date, timedelta + import weechat, re except Exception: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org") quit() SCRIPT_NAME = "logsize" SCRIPT_AUTHOR = "nils_2 " -SCRIPT_VERSION = "0.3" +SCRIPT_VERSION = "0.4" SCRIPT_LICENSE = "GPL" SCRIPT_DESC = "display size of current logfile in item-bar" -OPTIONS = { "refresh" : ("60","refresh timer (in seconds)"), +OPTIONS = { "refresh" : ("0","refresh timer (in seconds). 0 = refresh only on buffer or window switch. this is the default setting"), "size" : ("KB","display length in KB/MB/GB/TB. Leave option empty for byte"), - "display" : ("length","could be \"length\", \"lines\" or \"both\". CAVE: Use display option \"lines\" very carefully, large logfiles can stall the script and weechat!!!"), + "display" : ("%L","possible item: %W = words, %L = lines or %F = file length. (content is evaluated, e.g. you can use colors with format \"${color:xxx}\", see /help eval)"), "log_disabled" : ("","displays a text in item, when logger is disabled for buffer"), "file_not_found": ("","displays a text in item, when logfile wasn't found"), } hooks = { "timer": "", "bar_item": "" } +hook_process_out = [] +output = "" + +# regexp to match ${color} tags +regex_color=re.compile('\$\{([^\{\}]+)\}') -# ================================[ dos ]=============================== -def sizecheck(logfile): - if not os.path.isfile(logfile): - return OPTIONS["file_not_found"] - filesize = float(os.path.getsize(logfile)) # filesize in bytes +# regexp to match ${optional string} tags +regex_optional_tags=re.compile('%\{[^\{\}]+\}') + +# ================================[ size ]=============================== +def sizecheck(filesize): + filesize = int(filesize) if OPTIONS["size"].lower() == "kb": filesize = "%.2f" % (filesize / 1024) size = "K" @@ -82,45 +90,53 @@ def sizecheck(logfile): size = "b" return "%s%s" % (filesize,size) -def read_lines(logfile): - if os.path.isfile(logfile): - f = open(logfile,'r') - lines = 0L - for line in f.xreadlines(): - lines += 1L - f.close() - return "%s %s" % (lines,"lines") - else: - return OPTIONS["file_not_found"] - # ================================[ weechat item ]=============================== def show_item (data, item, window): + global output + return output - (logfile,log_enabled) = get_logfile(window) +def get_file_information(ptr_buffer): + global hook_process_out, output + + (logfile,log_enabled) = get_logfile(ptr_buffer) if not log_enabled: - return OPTIONS["log_disabled"] + output = OPTIONS["log_disabled"] + return output = '' if logfile != '': - if OPTIONS["display"] == 'lines': # get number of lines in logfile - output = str(read_lines(logfile)) - elif OPTIONS["display"] == 'length': # get lenght of logfile - output = str(sizecheck(logfile)) - elif OPTIONS["display"] == 'both': # get lines and lenght of log_filename - output = "%s/%s" % ( str(read_lines(logfile)),str(sizecheck(logfile)) ) - return "%s" % output # this line will be printed to item-bar - -def get_logfile(window): - current_buffer = weechat.window_get_pointer(window,"buffer") - if current_buffer == "": - return "" + # newline / word / bytes / filename + weechat.hook_process("wc %s" % logfile, 50000, "my_hook_process_cb", "") + if hook_process_out: + lines = hook_process_out[0] + words = hook_process_out[1] + flength = sizecheck(hook_process_out[2]) + + tags = {'%L': str(lines), + '%W': str(words), + '%F': str(flength)} + + output = substitute_colors(OPTIONS['display']) + # replace mandatory tags + for tag in list(tags.keys()): + # for tag in tags.keys(): + output = output.replace(tag, tags[tag]) + weechat.bar_item_update(SCRIPT_NAME) + return + +def substitute_colors(text): + if int(version) >= 0x00040200: + return weechat.string_eval_expression(text,{},{},{}) + # substitute colors in output + return re.sub(regex_color, lambda match: weechat.color(match.group(1)), text) +def get_logfile(ptr_buffer): log_filename = "" log_enabled = 0 infolist = weechat.infolist_get('logger_buffer','','') while weechat.infolist_next(infolist): bpointer = weechat.infolist_pointer(infolist, 'buffer') - if current_buffer == bpointer: + if ptr_buffer == bpointer: log_filename = weechat.infolist_string(infolist, 'log_filename') log_enabled = weechat.infolist_integer(infolist, 'log_enabled') log_level = weechat.infolist_integer(infolist, 'log_level') @@ -133,7 +149,41 @@ def item_update(data, remaining_calls): weechat.bar_item_update(SCRIPT_NAME) return weechat.WEECHAT_RC_OK +# ================================[ hook process]=============================== +def my_hook_process_cb(data, command, return_code, out, err): + global hook_process_out + if return_code == weechat.WEECHAT_HOOK_PROCESS_ERROR: + weechat.prnt("", "Error with command '%s'" % command) + return weechat.WEECHAT_RC_OK +# if return_code >= 0: +# weechat.prnt("", "return_code = %d" % return_code) + if out != "": + hook_process_out = out.split() + if err != "": + weechat.prnt("", "stderr: %s" % err) + return weechat.WEECHAT_RC_OK + # ================================[ weechat hook ]=============================== +def window_switch_cb(data, signal, signal_data): + window = signal_data + window = weechat.current_window() + ptr_buffer = weechat.window_get_pointer(window,"buffer") + get_file_information(ptr_buffer) + return weechat.WEECHAT_RC_OK + +def buffer_switch_cb(data, signal, signal_data): + ptr_buffer = signal_data + window = weechat.current_window() + ptr_buffer = weechat.window_get_pointer(window,'buffer') + if ptr_buffer == '': + return '' + get_file_information(ptr_buffer) + return weechat.WEECHAT_RC_OK + +def hook_timer_refresh_item_cb(data, remaining_calls): + weechat.bar_item_update(SCRIPT_NAME) + return weechat.WEECHAT_RC_OK + def unhook_timer(): global hooks if hooks["timer"] != "": @@ -176,19 +226,26 @@ def toggle_refresh(pointer, name, value): def init_options(): global OPTIONS - for option,value in OPTIONS.items(): + + for option, value in list(OPTIONS.items()): + weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value[0]) - weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) OPTIONS[option] = value[0] else: OPTIONS[option] = weechat.config_get_plugin(option) # ================================[ main ]=============================== if __name__ == "__main__": if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): + version = weechat.info_get("version_number", "") or 0 + init_options() - if OPTIONS["refresh"] != 0: + if OPTIONS["refresh"] != "0": hook_timer() - weechat.hook_config( 'plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '' ) - + else: + weechat.hook_signal("buffer_switch","buffer_switch_cb","") + weechat.hook_signal("window_switch","window_switch_cb","") + weechat.bar_item_new(SCRIPT_NAME, 'show_item','') + weechat.bar_item_update(SCRIPT_NAME) + weechat.hook_config( 'plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '' ) diff --git a/python/lossage.py b/python/lossage.py new file mode 100644 index 00000000..8cc2f804 --- /dev/null +++ b/python/lossage.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Germain Z. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# +# Inspired by Emacs's view-lossage, this script displays a history of the last +# keystrokes you performed and the commands invoked. +# + + +import collections +import dataclasses +import re +from typing import Deque, Dict, Iterable, Tuple + +import weechat # type: ignore # pylint: disable=import-error + + +SCRIPT_NAME = "lossage" +SCRIPT_AUTHOR = "GermainZ " +SCRIPT_VERSION = "0.1" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = ( + "Displays the last few input keystrokes and the commands run, " + "inspired by Emacs's view-lossage." +) + + +HISTORY_SIZE: int = 300 +REGEX_COMBO_REPL: Iterable[Tuple[re.Pattern, str]] = ( + (re.compile(r"\x01\[\["), "meta2-"), + (re.compile(r"\x01\["), "meta-"), + (re.compile(r"\x01"), "ctrl-"), +) +REGEX_AREA_STRIP = re.compile(r"^@[^:]+:") +HEADER: Tuple[str, str, str] = ("context", "combo", "command") + + +@dataclasses.dataclass +class HistoryItem: + context: str + combo: str + command: str + + +@dataclasses.dataclass +class Data: + history: Deque[HistoryItem] = dataclasses.field( + default_factory=lambda: collections.deque(maxlen=HISTORY_SIZE) + ) + key_bindings: Dict[str, Dict[str, str]] = dataclasses.field( + default_factory=lambda: collections.defaultdict(dict) + ) + + +DATA = Data() + + +def cb_key_combo(context: str, signal: str, signal_data: str): + mode = signal.split("_")[-1] + combo = signal_data + + for regex, repl in REGEX_COMBO_REPL: + combo = regex.sub(repl, combo) + + command = DATA.key_bindings[context].get(combo, "") + DATA.history.append(HistoryItem(mode, combo, command)) + return weechat.WEECHAT_RC_OK + + +def cb_lossage_cmd(*_): + buffer = weechat.buffer_search("python", SCRIPT_NAME) + if not buffer: + buffer = weechat.buffer_new(SCRIPT_NAME, "", "", "", "") + weechat.buffer_set(buffer, "localvar_set_no_log", "1") + weechat.buffer_set(buffer, "time_for_each_line", "0") + weechat.buffer_set(buffer, "nicklist", "0") + weechat.command(buffer, f"/buffer {SCRIPT_NAME}") + weechat.command(buffer, "/buffer clear") + + weechat.prnt( + buffer, + f"{weechat.color('bold')}{HEADER[0]:10} {HEADER[1]:20} {HEADER[2]}", + ) + for item in DATA.history: + if item is None: + break + weechat.prnt( + buffer, f"{item.context:10} {item.combo:20} {item.command}" + ) + + return weechat.WEECHAT_RC_OK + + +def cb_key_bindings_changed(*_): + populate_key_bindings() + + +def populate_key_bindings(): + for context in ("default", "search", "cursor"): + infolist = weechat.infolist_get("key", "", context) + + while weechat.infolist_next(infolist): + key = weechat.infolist_string(infolist, "key") + command = weechat.infolist_string(infolist, "command") + + # In the cursor context, bindings of the form `@area:key` can be + # created. When the binding is used, the area is not passed to the + # key_combo_* signal, so the best we can do is print a helpful + # warning if there are several possible matches. + if context == "cursor": + key = REGEX_AREA_STRIP.sub("", key) + if DATA.key_bindings[context].get(key, None): + command = ( + f"ambiguous; see `/key list {context}` -> " + f"`@…:{key}` key bindings" + ) + + DATA.key_bindings[context][key] = command + + weechat.infolist_free(infolist) + + +if __name__ == "__main__": + weechat.register( + SCRIPT_NAME, + SCRIPT_AUTHOR, + SCRIPT_VERSION, + SCRIPT_LICENSE, + SCRIPT_DESC, + "", + "", + ) + + populate_key_bindings() + + weechat.hook_signal("key_combo_default", "cb_key_combo", "default") + weechat.hook_signal("key_combo_search", "cb_key_combo", "search") + weechat.hook_signal("key_combo_cursor", "cb_key_combo", "cursor") + weechat.hook_command( + "lossage", SCRIPT_DESC, "", "test", "", "cb_lossage_cmd", "" + ) + weechat.hook_signal("key_bind", "cb_key_bindings_changed", "") + weechat.hook_signal("key_unbind", "cb_key_bindings_changed", "") diff --git a/python/maze.py b/python/maze.py new file mode 100644 index 00000000..0ebc5acd --- /dev/null +++ b/python/maze.py @@ -0,0 +1,435 @@ +# +# Copyright (C) 2012-2022 Sébastien Helleu +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# +# Interactive maze generator and solver for WeeChat. +# +# History: +# +# 2022-02-20, Sébastien Helleu : +# version 1.0.0: first public version +# 2012-10-10, Sébastien Helleu : +# version 0.0.1: initial release +# + +"""Maze generator and solver for WeeChat.""" + +from dataclasses import dataclass, field +from typing import ClassVar, Dict, List, Optional, Tuple + +import random + +try: + import weechat + IMPORT_OK = True +except ImportError: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org/") + IMPORT_OK = False + +SCRIPT_NAME = "maze" +SCRIPT_AUTHOR = "Sébastien Helleu " +SCRIPT_VERSION = "1.0.0" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = "Interactive maze generator and solver" + +SCRIPT_COMMAND = "maze" + +MAZE_KEYS: Dict[str, Tuple[str, str]] = { + "n": ("", "new maze"), + "d": ("default", "default size"), + "+": ("+", "bigger"), + "-": ("-", "smaller"), + "s": ("solve", "solve"), + "i": ("isolve", "solve interactively"), + "r": ("reset", "reset solution"), +} + + +@dataclass +class Maze: + """Maze.""" + width: int = 0 + height: int = 0 + + cells: List[int] = field(default_factory=list, init=False) + solution: List[tuple[int, int, bool]] = field(default_factory=list, + init=False) + buffer: str = field(init=False) + timer: str = field(default="", init=False) + + # cell status + VISITED: ClassVar[int] = 1 # visited cell + TOP: ClassVar[int] = 2 # door opened on top + BOTTOM: ClassVar[int] = 4 # door opened on bottom + LEFT: ClassVar[int] = 8 # door opened on the left + RIGHT: ClassVar[int] = 16 # door opened on the right + SOLUTION: ClassVar[int] = 32 # cell part of the solution + SOLUTION_INT: ClassVar[int] = 64 # cell displayed for solution + + def __post_init__(self) -> None: + """Initialize maze with the given size.""" + self.cells = [0] * (self.width * self.height) + self.cells[0] |= self.LEFT + self.cells[-1] |= self.RIGHT + self.solution = [] + self.buffer = Maze.open_buffer() + if self.buffer: + keys = ", ".join([ + f"alt+\"{key}\": {value[1]}" + for key, value in MAZE_KEYS.items() + ]) + weechat.buffer_set( + self.buffer, + "title", + f"Maze {self.width}x{self.height} | {keys}", + ) + + @staticmethod + def open_buffer() -> str: + """Open maze buffer.""" + buf: str = weechat.buffer_search("python", "maze") + if not buf: + buf = weechat.buffer_new( + "maze", + "maze_input_buffer", + "", + "maze_close_buffer", + "", + ) + if buf: + weechat.buffer_set(buf, "type", "free") + for key, value in MAZE_KEYS.items(): + weechat.buffer_set( + buf, + f"key_bind_meta-{key}", + f"/{SCRIPT_COMMAND} {value[0]}".strip(), + ) + weechat.buffer_set(buf, "display", "1") + return buf + + def get_adjacent_cells( + self, col: int, line: int + ) -> List[Tuple[int, int, int, int]]: + """Get adjacent cells.""" + list_cells = [] + if col < self.width - 1: # right + list_cells.append((col + 1, line, self.RIGHT, self.LEFT)) + if line < self.height - 1: # bottom + list_cells.append((col, line + 1, self.BOTTOM, self.TOP)) + if line > 0: # top + list_cells.append((col, line - 1, self.TOP, self.BOTTOM)) + if col > 0: # left + list_cells.append((col - 1, line, self.LEFT, self.RIGHT)) + return list_cells + + def generate(self) -> None: + """Generate a new random maze.""" + if not self.cells: + return + col: int = 0 + line: int = 0 + self.cells[0] |= self.VISITED + stack: List[Tuple[int, int]] = [] + while True: + pos: int = (line * self.width) + col + list_cells: List[Tuple[int, int, int, int]] = ( + self.get_adjacent_cells(col, line) + ) + random.shuffle(list_cells) + for col2, line2, to_neighbor, from_neighbor in list_cells: + pos2 = (line2 * self.width) + col2 + # neighbor not visited? + if not self.cells[pos2] & self.VISITED: + # open door from (x, y) to neighbor + self.cells[pos] |= to_neighbor + # open door from neighbor to (x, y) (reverse) + self.cells[pos2] |= from_neighbor | self.VISITED + stack.append((col, line)) + col, line = col2, line2 + break + else: + col, line = stack.pop() + if not stack: + break + + def solve(self, interactive: bool = False) -> None: + """Solve a maze: find path from entry (0,0) to exit (n,n).""" + self.remove_timer() + for index, cell in enumerate(self.cells): + self.cells[index] = cell & ~(self.VISITED | self.SOLUTION + | self.SOLUTION_INT) + col: int = 0 + line: int = 0 + self.cells[0] |= self.SOLUTION + self.solution = [(col, line, True)] + stack: List[Tuple[int, int]] = [] + visited_solution = self.VISITED | self.SOLUTION + while not self.cells[-1] & self.SOLUTION: + pos: int = (line * self.width) + col + self.cells[pos] |= self.VISITED + list_cells: List[Tuple[int, int, int, int]] = ( + self.get_adjacent_cells(col, line) + ) + for col2, line2, to_neighbor, _ in list_cells: + pos2: int = (line2 * self.width) + col2 + # door opened and neighbor not visited/solution? + if self.cells[pos] & to_neighbor \ + and not self.cells[pos2] & visited_solution: + self.cells[pos2] |= visited_solution + self.solution.append((col2, line2, True)) + stack.append((col, line)) + col, line = col2, line2 + break + else: + if not self.cells[-1] & self.SOLUTION: + self.cells[pos] &= ~(self.SOLUTION) + self.solution.append((col, line, False)) + col, line = stack.pop() + if not stack: + break + self.display() + if interactive: + self.show_interactive_solution() + + def reset(self) -> None: + """Remove solution.""" + self.remove_timer() + for index, cell in enumerate(self.cells): + self.cells[index] = cell & ~(self.SOLUTION | self.SOLUTION_INT) + self.display() + + def display_line(self, line: int) -> None: + """Display a line of maze.""" + str_line: str + if line >= self.height: + str_line = ( + weechat.color("white") + + ("▀" * ((self.width * 2) + 1)) + ) + else: + both_sol: int = self.SOLUTION | self.SOLUTION_INT + cell: int = self.cells[line * self.width] + color: str = ( + "lightred" if cell & self.SOLUTION_INT + else "blue" if cell & self.SOLUTION + else "black" + ) + str_line = ( + weechat.color(f"{color},white") + + ("▄" if cell & self.LEFT else " ") + ) + for col in range(self.width): + cell = self.cells[(line * self.width) + col] + color = ( + "lightred" if cell & self.SOLUTION_INT + else "blue" if cell & self.SOLUTION + else "black" + ) + # door opened on top? + if cell & self.TOP: + pos: int = ((line - 1) * self.width) + col + if line > 0 and self.cells[pos] & both_sol: + str_line += weechat.color(f"white,{color}") + " " + else: + str_line += weechat.color(f"{color},black") + "▄" + else: + str_line += weechat.color(f"{color},white") + "▄" + # door opened on the right? + str_line += ( + weechat.color(f"{color},white") + + ("▄" if cell & self.RIGHT else " ") + ) + str_line += weechat.color("reset") + weechat.prnt_y(self.buffer, line, str_line) + + def display(self) -> None: + """Display maze.""" + if not self.buffer: + return + weechat.buffer_clear(self.buffer) + for line in range(self.height + 1): + self.display_line(line) + + def remove_timer(self) -> None: + """Reset the timer used to show interactive solution.""" + if self.timer: + weechat.unhook(self.timer) + self.timer = "" + + def show_interactive_solution(self) -> None: + """Show solution.""" + self.remove_timer() + self.timer = weechat.hook_timer(1, 0, 0, "maze_timer_cb", "") + + def show_next_solution(self) -> None: + """Show next solution step.""" + col: int + line: int + show: bool + col, line, show = self.solution.pop(0) + pos: int = (line * self.width) + col + if show: + self.cells[pos] |= self.SOLUTION_INT + else: + self.cells[pos] &= ~(self.SOLUTION_INT) + self.display_line(line) + if not self.solution: + self.remove_timer() + + def __del__(self) -> None: + """Destructor.""" + self.remove_timer() + + +maze: Optional[Maze] = None + + +def maze_input_buffer(data: str, buffer: str, str_input: str) -> int: + """Input data in maze buffer.""" + # pylint: disable=unused-argument + if str_input.lower() == "q": + weechat.buffer_close(buffer) + else: + weechat.command("", f"/{SCRIPT_COMMAND} {str_input}") + return weechat.WEECHAT_RC_OK + + +def maze_close_buffer(data: str, buffer: str) -> int: + """Called when maze buffer is closed.""" + # pylint: disable=unused-argument + global maze + maze = None + return weechat.WEECHAT_RC_OK + + +def maze_timer_cb(data: str, remaining_calls: int) -> int: + """Timer used to show solution, one cell by one cell.""" + global maze + if maze: + maze.show_next_solution() + return weechat.WEECHAT_RC_OK + + +def maze_get_size(args: str = "") -> Tuple[int, int]: + """Get maze size with args, defaulting to current maze or window size.""" + global maze + width: int = 0 + height: int = 0 + if args in ("d", "default"): + args = "" + elif maze: + width, height = maze.width, maze.height + if args: + # size given by user + try: + items = args.split() + if len(items) > 1: + width, height = int(items[0]), int(items[1]) + else: + width, height = int(args), int(args) + width = max(width, 2) + height = max(height, 2) + except ValueError: + width, height = 0, 0 + if not width or not height: + # automatic size with size of window + win_width: int = weechat.window_get_integer(weechat.current_window(), + "win_chat_width") - 1 + win_height: int = weechat.window_get_integer(weechat.current_window(), + "win_chat_height") - 1 + size: int = min(win_width, win_height) + width, height = size, size + return width, height + + +def maze_get_other_size(pct_diff: int) -> Tuple[int, int]: + """Get another size using a percent of size to add or subtract.""" + factor: int = pct_diff // abs(pct_diff) + width: int + height: int + width, height = maze_get_size() + add_width: int = max(2, (width * abs(pct_diff)) // 100) + add_height: int = max(2, (height * abs(pct_diff)) // 100) + width += factor * add_width + height += factor * add_height + return max(width, 2), max(height, 2) + + +def maze_new(width: int, height: int) -> None: + """Create a new maze.""" + global maze + maze = Maze(width=width, height=height) + maze.generate() + maze.display() + + +def maze_cmd_cb(data: str, buffer: str, args: str) -> int: + """The /maze command.""" + global maze + width: int + height: int + if args in ("s", "solve"): + if maze: + maze.solve() + elif args in ("i", "isolve"): + if maze: + maze.solve(interactive=True) + elif args in ("r", "reset"): + if maze: + maze.reset() + else: + if args == "+": + width, height = maze_get_other_size(30) + elif args == "-": + width, height = maze_get_other_size(-30) + elif args in ("d", "default"): + width, height = maze_get_size(args) + elif not args or args in ("n", "new"): + width, height = maze_get_size() + else: + error = weechat.prefix("error") + weechat.prnt("", f"{error}maze error: unknown option \"{args}\"") + return weechat.WEECHAT_RC_OK + maze_new(width, height) + return weechat.WEECHAT_RC_OK + + +if __name__ == "__main__" and IMPORT_OK: + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, + SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + weechat.hook_command( + SCRIPT_COMMAND, + "Generate and solve a maze.", + "size || n|new || d|default || s|solve || i|isolve || r|reset " + "|| +|-", + """\ + size: one size (square) or width and height separated by spaces + new: regenerate another maze +default: regenerate another maze with default size + solve: solve the maze and show solution (in blue) + isolve: solve then show interactively how solution was found (in red) + reset: hide solution + +: show a bigger maze + -: show a smaller maze + +All options shown above can be given in input of maze buffer. +""", + "", + "maze_cmd_cb", + "", + ) diff --git a/python/mnotify.py b/python/mnotify.py index 692c77d0..d15849dc 100644 --- a/python/mnotify.py +++ b/python/mnotify.py @@ -10,22 +10,50 @@ # based on: # growl.py # Copyright (c) 2011 Sorin Ionescu - +# +# Changelog: +# Ver: 0.4 by Antonin Skala tony762@gmx.com 3.2018 +# +# Changed dcc regex to match new notify appears (weechat notify now contain IP) +# Added dcc send offer and dcc send start notify +# Setting for notify is divided to off (don't send), on (always send), +# away (send only when away). +# Changed default settings to match new scheme +# DCC get request show name with ip, network and size of file. +# +# Ver: 0.5 by Antonin Skala tony762@gmx.com 3.2019 +# Repaired DCC Get FAILED message. +# +# Ver: 0.6 by Antonin Skala tony762@gmx.com 3.2019 +# Support Python 2 and 3 +# +# Help: +# Install and configure msmtp first (msmtp.sourceforge.net/) +# List and Change plugin settings by /set plugins.var.python.mnotify.* +# Change language to english -otherwise this will not work +# /set env LANG en_US.UTF-8 +# /save +# /upgrade +# +# If running from TMux: +# /usr/bin/tmux -S /tmp/ircmux -2 new-session -d -s irc bash +# /usr/bin/tmux -S /tmp/ircmux -2 send -t irc "export LANG=en_US.UTF-8" ENTER +# /usr/bin/tmux -S /tmp/ircmux -2 send -t irc weechat ENTER # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- + import re -import os +import sys import subprocess from email.mime.text import MIMEText -#import smtplib import weechat SCRIPT_NAME = 'mnotify' SCRIPT_AUTHOR = 'maker' -SCRIPT_VERSION = '0.3' +SCRIPT_VERSION = '0.6' SCRIPT_LICENSE = 'Beerware License' SCRIPT_DESC = 'Sends mail notifications upon events.' @@ -34,25 +62,22 @@ # ----------------------------------------------------------------------------- SETTINGS = { 'show_public_message': 'off', - 'show_private_message': 'on', + 'show_private_message': 'away', 'show_public_action_message': 'off', - 'show_private_action_message': 'on', + 'show_private_action_message': 'away', 'show_notice_message': 'off', - 'show_invite_message': 'on', - 'show_highlighted_message': 'on', - 'show_server': 'on', - 'show_channel_topic': 'on', + 'show_invite_message': 'away', + 'show_highlighted_message': 'off', + 'show_server': 'away', + 'show_channel_topic': 'off', 'show_dcc': 'on', - 'show_upgrade_ended': 'on', - 'sticky': 'off', - 'sticky_away': 'on', - 'sendmail': 'msmtp', - 'email_to': '', - 'email_from': 'irc ' + 'show_upgrade_ended': 'off', + 'sendmail': '/usr/bin/msmtp', + 'email_to': 'somebody@somwhere.xx', + 'email_from': 'irc@somwhere.xx' } - # ----------------------------------------------------------------------------- # Globals # ----------------------------------------------------------------------------- @@ -62,7 +87,6 @@ 'notice message': set(['irc_notice', 'notify_private']), 'invite message': set(['irc_invite', 'notify_highlight']), 'channel topic': set(['irc_topic', ]), - #'away status': set(['away_info', ]), } @@ -70,23 +94,27 @@ 'away status': re.compile(r'^You ((\w+).){2,3}marked as being away', re.UNICODE), 'dcc chat request': - re.compile(r'^xfer: incoming chat request from (\w+)', re.UNICODE), + re.compile(r'^xfer: incoming chat request from ([^\s]+) ', re.UNICODE), 'dcc chat closed': - re.compile(r'^xfer: chat closed with (\w+)', re.UNICODE), + re.compile(r'^xfer: chat closed with ([^\s]+) \(', re.UNICODE), 'dcc get request': re.compile( - r'^xfer: incoming file from (\w+) [^:]+: ((?:,\w|[^,])+),', + r'^xfer: incoming file from (^\s|.+), name: ((?:,\w|[^,])+), (\d+) bytes', re.UNICODE), 'dcc get completed': - re.compile(r'^xfer: file ([^\s]+) received from \w+: OK', re.UNICODE), + re.compile(r'^xfer: file ((?:,\w|[^,])+) received from ([^\s]+) ((?:,\w|[^,]+)): OK$', re.UNICODE), 'dcc get failed': re.compile( - r'^xfer: file ([^\s]+) received from \w+: FAILED', + r'^xfer: file ((?:,\w|[^,])+) received from ([^\s]+) ((?:,\w|[^,]+)): FAILED$', re.UNICODE), + 'dcc send offer': + re.compile(r'^xfer: offering file to ([^\s]+) ((?:,\w|[^,])+), name: ((?:,\w|[^,])+) \(local', re.UNICODE), + 'dcc send start': + re.compile(r'^xfer: sending file to ([^\s]+) ((?:,\w|.)+), name: ((?:,\w|[^,])+) \(local', re.UNICODE), 'dcc send completed': - re.compile(r'^xfer: file ([^\s]+) sent to \w+: OK', re.UNICODE), + re.compile(r'^xfer: file ((?:,\w|[^,])+) sent to ([^\s]+) ((?:,\w|[^,]+)): OK$', re.UNICODE), 'dcc send failed': - re.compile(r'^xfer: file ([^\s]+) sent to \w+: FAILED', re.UNICODE), + re.compile(r'^xfer: file ((?:,\w|[^,])+) sent to ([^\s]+) ((?:,\w|[^,]+)): FAILED$', re.UNICODE), } @@ -102,6 +130,8 @@ 'dcc get request': 'notify_dcc_get_request', 'dcc get completed': 'notify_dcc_get_completed', 'dcc get failed': 'notify_dcc_get_failed', + 'dcc send offer': 'notify_dcc_send_offer', + 'dcc send start': 'notify_dcc_send_start', 'dcc send completed': 'notify_dcc_send_completed', 'dcc send failed': 'notify_dcc_send_failed', } @@ -118,7 +148,9 @@ # ----------------------------------------------------------------------------- def cb_irc_server_connected(data, signal, signal_data): '''Notify when connected to IRC server.''' - if weechat.config_get_plugin('show_server') == 'on': + if (weechat.config_get_plugin('show_server') == 'on' + or (weechat.config_get_plugin('show_server') == "away" + and STATE['is_away'])): a_notify( 'Server', 'Server Connected', @@ -128,7 +160,9 @@ def cb_irc_server_connected(data, signal, signal_data): def cb_irc_server_disconnected(data, signal, signal_data): '''Notify when disconnected to IRC server.''' - if weechat.config_get_plugin('show_server') == 'on': + if (weechat.config_get_plugin('show_server') == 'on' + or (weechat.config_get_plugin('show_server') == "away" + and STATE['is_away'])): a_notify( 'Server', 'Server Disconnected', @@ -138,7 +172,9 @@ def cb_irc_server_disconnected(data, signal, signal_data): def cb_notify_upgrade_ended(data, signal, signal_data): '''Notify on end of WeeChat upgrade.''' - if weechat.config_get_plugin('show_upgrade_ended') == 'on': + if (weechat.config_get_plugin('show_upgrade_ended') == 'on' + or (weechat.config_get_plugin('show_upgrade_ended') == "away" + and STATE['is_away'])): a_notify( 'WeeChat', 'WeeChat Upgraded', @@ -148,7 +184,9 @@ def cb_notify_upgrade_ended(data, signal, signal_data): def notify_highlighted_message(buffername, prefix, message): '''Notify on highlighted message.''' - if weechat.config_get_plugin("show_highlighted_message") == "on": + if (weechat.config_get_plugin("show_highlighted_message") == "on" + or (weechat.config_get_plugin("show_highlighted_message") == "away" + and STATE['is_away'])): a_notify( 'Highlight', 'Highlighted on {0} by {1}'.format(buffername, prefix), @@ -169,7 +207,9 @@ def notify_public_message_or_action(buffername, prefix, message, highlighted): else: if highlighted: notify_highlighted_message(buffername, prefix, message) - elif weechat.config_get_plugin("show_public_message") == "on": + elif (weechat.config_get_plugin("show_public_message") == "on" + or (weechat.config_get_plugin("show_public_message") == "away" + and STATE['is_away'])): a_notify( 'Public', 'Public Message on {0}'.format(buffername), @@ -195,7 +235,9 @@ def notify_private_message_or_action(buffername, prefix, message, highlighted): else: if highlighted: notify_highlighted_message(buffername, prefix, message) - elif weechat.config_get_plugin("show_private_message") == "on": + elif (weechat.config_get_plugin("show_private_message") == "on" + or (weechat.config_get_plugin("show_private_message") == "away" + and STATE['is_away'])): a_notify( 'Private', 'Private Message', @@ -206,7 +248,9 @@ def notify_public_action_message(buffername, prefix, message, highlighted): '''Notify on public action message.''' if highlighted: notify_highlighted_message(buffername, prefix, message) - elif weechat.config_get_plugin("show_public_action_message") == "on": + elif (weechat.config_get_plugin("show_public_action_message") == "on" + or (weechat.config_get_plugin("show_public_action_message") == "away" + and STATE['is_away'])): a_notify( 'Action', 'Public Action Message on {0}'.format(buffername), @@ -218,13 +262,16 @@ def notify_private_action_message(buffername, prefix, message, highlighted): '''Notify on private action message.''' if highlighted: notify_highlighted_message(buffername, prefix, message) - elif weechat.config_get_plugin("show_private_action_message") == "on": + elif (weechat.config_get_plugin("show_private_action_message") == "on" + or (weechat.config_get_plugin("show_private_action_message") == "away" + and STATE['is_away'])): a_notify( 'Action', 'Private Action Message', '{0}: {1}'.format(prefix, message), ) + def notify_notice_message(buffername, prefix, message, highlighted): '''Notify on notice message.''' regex = re.compile(r'^([^\s]*) [^:]*: (.+)$', re.UNICODE) @@ -234,7 +281,9 @@ def notify_notice_message(buffername, prefix, message, highlighted): message = match.group(2) if highlighted: notify_highlighted_message(buffername, prefix, message) - elif weechat.config_get_plugin("show_notice_message") == "on": + elif (weechat.config_get_plugin("show_notice_message") == "on" + or (weechat.config_get_plugin("show_notice_message") == "away" + and STATE['is_away'])): a_notify( 'Notice', 'Notice Message', @@ -243,7 +292,9 @@ def notify_notice_message(buffername, prefix, message, highlighted): def notify_invite_message(buffername, prefix, message, highlighted): '''Notify on channel invitation message.''' - if weechat.config_get_plugin("show_invite_message") == "on": + if (weechat.config_get_plugin("show_invite_message") == "on" + or (weechat.config_get_plugin("show_invite_message") == "away" + and STATE['is_away'])): regex = re.compile( r'^You have been invited to ([^\s]+) by ([^\s]+)$', re.UNICODE) match = regex.match(message) @@ -258,10 +309,12 @@ def notify_invite_message(buffername, prefix, message, highlighted): def notify_channel_topic(buffername, prefix, message, highlighted): '''Notify on channel topic change.''' - if weechat.config_get_plugin("show_channel_topic") == "on": + if (weechat.config_get_plugin("show_channel_topic") == "on" + or (weechat.config_get_plugin("show_channel_topic") == "away" + and STATE['is_away'])): regex = re.compile( r'^\w+ has (?:changed|unset) topic for ([^\s]+)' + - '(?:(?: from "(?:.+)")? to "(.+)")?', + r'(?:(?: from "(?:.+)")? to "(.+)")?', re.UNICODE) match = regex.match(message) if match: @@ -275,7 +328,9 @@ def notify_channel_topic(buffername, prefix, message, highlighted): def notify_dcc_chat_request(match): '''Notify on DCC chat request.''' - if weechat.config_get_plugin("show_dcc") == "on": + if (weechat.config_get_plugin("show_dcc") == "on" + or (weechat.config_get_plugin("show_dcc") == "away" + and STATE['is_away'])): nick = match.group(1) a_notify( 'DCC', @@ -285,7 +340,9 @@ def notify_dcc_chat_request(match): def notify_dcc_chat_closed(match): '''Notify on DCC chat termination.''' - if weechat.config_get_plugin("show_dcc") == "on": + if (weechat.config_get_plugin("show_dcc") == "on" + or (weechat.config_get_plugin("show_dcc") == "away" + and STATE['is_away'])): nick = match.group(1) a_notify( 'DCC', @@ -295,41 +352,94 @@ def notify_dcc_chat_closed(match): def notify_dcc_get_request(match): 'Notify on DCC get request.' - if weechat.config_get_plugin("show_dcc") == "on": + if (weechat.config_get_plugin("show_dcc") == "on" + or (weechat.config_get_plugin("show_dcc") == "away" + and STATE['is_away'])): nick = match.group(1) file_name = match.group(2) + file_size = int(match.group(3)) a_notify( 'DCC', 'File Transfer Request', - '{0} wants to send you {1}.'.format(nick, file_name)) + '{0} wants to send you {1} and size is {2}.'.format(nick, file_name, humanbytes(file_size))) def notify_dcc_get_completed(match): 'Notify on DCC get completion.' - if weechat.config_get_plugin("show_dcc") == "on": + if (weechat.config_get_plugin("show_dcc") == "on" + or (weechat.config_get_plugin("show_dcc") == "away" + and STATE['is_away'])): + nick = match.group(2) file_name = match.group(1) - a_notify('DCC', 'Download Complete', file_name) + a_notify( + 'DCC', + 'Download Complete', + 'Downloading {1} from {0} completed.'.format(nick, file_name)) def notify_dcc_get_failed(match): 'Notify on DCC get failure.' - if weechat.config_get_plugin("show_dcc") == "on": + if (weechat.config_get_plugin("show_dcc") == "on" + or (weechat.config_get_plugin("show_dcc") == "away" + and STATE['is_away'])): + nick = match.group(2) file_name = match.group(1) - a_notify('DCC', 'Download Failed', file_name) + a_notify( + 'DCC', + 'Download Failed', + 'Downloading {1} from {0} failed.'.format(nick, file_name)) + + +def notify_dcc_send_offer(match): + 'Notify on DCC send offer.' + if (weechat.config_get_plugin("show_dcc") == "on" + or (weechat.config_get_plugin("show_dcc") == "away" + and STATE['is_away'])): + nick = match.group(1) + file_name = match.group(3) + a_notify( + 'DCC', + 'Offering File Upload', + 'Offering {1} to {0}.'.format(nick, file_name)) + + +def notify_dcc_send_start(match): + 'Notify on DCC send start.' + if (weechat.config_get_plugin("show_dcc") == "on" + or (weechat.config_get_plugin("show_dcc") == "away" + and STATE['is_away'])): + nick = match.group(1) + file_name = match.group(3) + a_notify( + 'DCC', + 'Start File Upload', + 'Uploading {1} to {0}.'.format(nick, file_name)) def notify_dcc_send_completed(match): 'Notify on DCC send completion.' - if weechat.config_get_plugin("show_dcc") == "on": + if (weechat.config_get_plugin("show_dcc") == "on" + or (weechat.config_get_plugin("show_dcc") == "away" + and STATE['is_away'])): + nick = match.group(2) file_name = match.group(1) - a_notify('DCC', 'Upload Complete', file_name) + a_notify( + 'DCC', + 'Upload Complete', + 'Upload {1} to {0} completed.'.format(nick, file_name)) def notify_dcc_send_failed(match): 'Notify on DCC send failure.' - if weechat.config_get_plugin("show_dcc") == "on": + if (weechat.config_get_plugin("show_dcc") == "on" + or (weechat.config_get_plugin("show_dcc") == "away" + and STATE['is_away'])): + nick = match.group(2) file_name = match.group(1) - a_notify('DCC', 'Upload Failed', file_name) + a_notify( + 'DCC', + 'Upload Failed', + 'Upload {1} to {0} failed.'.format(nick, file_name)) # ----------------------------------------------------------------------------- @@ -384,12 +494,25 @@ def cb_process_message( return weechat.WEECHAT_RC_OK -def a_notify(notification, subject, message): - if STATE['is_away'] and weechat.config_get_plugin('sticky_away') == 'off': - return - if not STATE['is_away'] and weechat.config_get_plugin('sticky') == 'off': - return +def humanbytes(B): + B = float(B) + KB = float(1024) + MB = float(KB ** 2) # 1,048,576 + GB = float(KB ** 3) # 1,073,741,824 + TB = float(KB ** 4) # 1,099,511,627,776 + if B < KB: + return '{0} {1}'.format(B, 'Bytes' if 0 == B > 1 else 'Byte') + elif KB <= B < MB: + return '{0:.2f} KB'.format(B/KB) + elif MB <= B < GB: + return '{0:.2f} MB'.format(B/MB) + elif GB <= B < TB: + return '{0:.2f} GB'.format(B/GB) + elif TB <= B: + return '{0:.2f} TB'.format(B/TB) + +def a_notify(notification, subject, message): msg = MIMEText(message) msg['From'] = weechat.config_get_plugin('email_from') msg['To'] = weechat.config_get_plugin('email_to') @@ -401,7 +524,10 @@ def a_notify(notification, subject, message): stdin=subprocess.PIPE, ) - p.communicate(input=str(msg)) + if sys.version_info[0] > 2: + p.communicate(input=str.encode(msg.as_string())) + else: + p.communicate(input=str(msg)) # ----------------------------------------------------------------------------- @@ -413,19 +539,6 @@ def main(): for option, value in SETTINGS.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value) - # Initialize. - notifications = [ - 'Public', - 'Private', - 'Action', - 'Notice', - 'Invite', - 'Highlight', - 'Server', - 'Channel', - 'DCC', - 'WeeChat' - ] # Register hooks. weechat.hook_signal( 'irc_server_connected', diff --git a/python/moc_control.py b/python/moc_control.py index 3f4f2d4c..1a499cd7 100644 --- a/python/moc_control.py +++ b/python/moc_control.py @@ -20,6 +20,10 @@ # # History: # +# 2020-06-21, Sebastien Helleu : +# version 1.9: make call to bar_new compatible with WeeChat >= 2.9 +# 2019-10-16, Benjamin Neff : +# version 1.8: - add python 3 support # 2009-10-26, Benjamin Neff : # version 1.7.3: - Bugfix ( "/me" --> "output_nothing" ) 2 # 2009-10-25, Benjamin Neff : @@ -37,9 +41,11 @@ # version 1.5: initial release / port to weechat 0.3.0 # +from __future__ import print_function + SCRIPT_NAME = "moc_control" SCRIPT_AUTHOR = "SuperTux88 (Benjamin Neff) " -SCRIPT_VERSION = "1.7.3" +SCRIPT_VERSION = "1.9" SCRIPT_LICENSE = "GPL2" SCRIPT_DESC = "moc control and now playing script for Weechat" @@ -50,16 +56,16 @@ try: import weechat except ImportError: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org/") import_ok = False try: import os import subprocess import traceback -except ImportError, message: - print "Missing package(s) for %s: %s" % (SCRIPT_NAME, message) +except ImportError as message: + print('Missing package(s) for {}: {}'.format(SCRIPT_NAME, message)) import_ok = False # =================================[ config ]================================= @@ -193,7 +199,7 @@ def moc_infobar_update(data, buffer, args): else: song = _get_song_info() return _format_np(infobar['format'], song, 'infobar') - + def moc_infobar_updater(data,cals): """Update the bar item""" if infobar['enabled']: @@ -202,8 +208,12 @@ def moc_infobar_updater(data,cals): def _add_infobar(): """add the infobar for moc_control""" - weechat.bar_new(SCRIPT_NAME, "off", "750", "window", "", "bottom", "horizontal", "vertical", "1", "0", "default", "blue", "cyan", "off", "[moc_infobar]") - + version = int(weechat.info_get('version_number', '')) or 0 + if version >= 0x02090000: + weechat.bar_new(SCRIPT_NAME, "off", "750", "window", "", "bottom", "horizontal", "vertical", "1", "0", "default", "blue", "cyan", "cyan", "off", "[moc_infobar]") + else: + weechat.bar_new(SCRIPT_NAME, "off", "750", "window", "", "bottom", "horizontal", "vertical", "1", "0", "default", "blue", "cyan", "off", "[moc_infobar]") + def _remove_infobar(): """remove the infobar for moc_control""" weechat.bar_remove(weechat.bar_search(SCRIPT_NAME)) @@ -316,13 +326,13 @@ def _execute_command(cmd): """execute a command""" from subprocess import PIPE proc = subprocess.Popen(cmd, shell = True, stderr = PIPE, stdout = PIPE, close_fds = True) - error = proc.stderr.read() + error = proc.stderr.read().decode('utf-8') if error != '': for line in error.split('\n'): if line == 'FATAL_ERROR: The server is not running': return STATUS_NOT_RUNNING - - output = proc.stdout.read() + + output = proc.stdout.read().decode('utf-8') proc.wait() return output @@ -374,7 +384,7 @@ def _execute_command(cmd): ) # ==================================[ end ]=================================== - + def moc_unload(): """Unload the plugin from weechat""" if infobar['enabled']: diff --git a/python/mplayer.py b/python/mplayer.py deleted file mode 100644 index 74ba17ba..00000000 --- a/python/mplayer.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Ported to weechat by llua (Two & a half LoC. AWWWWWW YEA, 1337) -Contact: llua at irc://irc.freenode.net ----------------------------------------------------------------------------- -xchat_mplayer by Csigaa -Prints 'NICK is playing FILENAME [MPlayer version]' action to the active channel/dialog. - -This version is entirely based on procfs, so finally works on -all POSIX-compilant OS (*BSD, Solaris, Linux, etc) without calling any commands -(except for version number, but it is now optional) - -NOTE FOR PASTEBIN: save the file as xchat_mplayer.py to your xchat directory - - -Contact: csigaa@gmail.com -License: Beer-ware (by phk) ;p ----------------------------------------------------------------------------- - "THE BEER-WARE LICENSE" (Revision 42): - Csigaa wrote this file. As long as you retain this notice you - can do whatever you want with this stuff. If we meet some day, and you think - this stuff is worth it, you can buy me a beer in return ----------------------------------------------------------------------------- - -""" - - -import weechat -import os -import re - -weechat.register("mplayer", "llua", "0.1", "The Beer-ware License", "Now Playing for MPlayer", "", "") - -if os.name != 'posix': - raise NotImplementedError,'non-POSIX systems are not supported' - -def getFilename(): - # Get the video file name (identified by naming pattern) from procfs - - # list&walk the directory contents - proclist = sorted(os.listdir('/proc')) - for proc in os.listdir('/proc'): - # if it is a directory with a name of digits only - if os.path.isdir('/proc/'+proc) and re.match('[0-9]+',proc): - try: - exe = os.readlink('/proc/'+proc+'/exe').split('/') - # if executable name is 'mplayer' - if exe.pop() == 'mplayer': - filelist = os.listdir('/proc/'+proc+'/fd') - # walk the list of open files - for file in filelist: - try: - path = os.readlink('/proc/'+proc+'/fd/'+file) - if re.match('.*(avi|mpg|mkv|mp4|nuv|ogg|ogm|wmv|iso|img)$',path,re.I): - # if video filename found, return - return path.split('/').pop() - except: - # if link not readable, skip - continue - except: - # if process directory not readable, skip - continue - return None - -def getVersion(): - try: - import commands - try: - # release version, begins with number (incl. optional rc sign with maximum 2 digits); if no match, exception occurs (empty list - 0 index is out of range) - ver = re.findall('^MPlayer\s\d[.]\d+\w{0,4}',commands.getoutput('mplayer'))[0] - except: - # SVN version, begins with 'SVN' (revision number maximum 6 digits) - ver = re.findall('^MPlayer\sSVN-r\d{0,6}',commands.getoutput('mplayer'))[0] - return ver - except: - # if any error occured above, return only the player name - return 'MPlayer' - -def mplayer_msg(world,world_eol,userdata): - fn = getFilename() - ver = getVersion() - if type(fn) == str: - # we've got a string for fn - all = '/me is now watching: ' + fn + ' [' + ver + ']' - weechat.command(weechat.current_buffer(), all) - return 0 - else: - # we've got None (or something went very-very wrong) - return 1 - -weechat.hook_command("mplayer", "Now Watching", "", "/mplayer", "", "mplayer_msg", "") diff --git a/python/mpv.py b/python/mpv.py index dff553fc..7c953422 100644 --- a/python/mpv.py +++ b/python/mpv.py @@ -46,14 +46,14 @@ def mpv_msg(world,world_eol,userdata): metadata_short = loads(output_short.decode('utf8')) if 'album' not in metadata['data']: title = metadata_short['data'].encode('utf-8') - all = '%s' % MPV['message'] + title + all = '%s' % MPV['message'] + str(title) weechat.command(weechat.current_buffer(), all) return weechat.WEECHAT_RC_OK if 'album' in metadata['data']: title = metadata['data']['title'].encode('utf-8') artist = metadata['data']['artist'].encode('utf-8') - np = artist + ' ' + title + np = str(artist) + ' ' + str(title) all = '%s' % MPV['message'] + np weechat.command(weechat.current_buffer(), all) return weechat.WEECHAT_RC_OK @@ -63,7 +63,7 @@ def mpv_msg(world,world_eol,userdata): weechat.prnt('', '%s: mpv socket not properly configurated or mpv is not running' % MPV['SCRIPT_NAME']) return weechat.WEECHAT_RC_ERROR -weechat.register(MPV['SCRIPT_NAME'], "llua", "0.1", "The Beer-ware License", "Now Playing for mpv", "", "") +weechat.register(MPV['SCRIPT_NAME'], "llua", "0.2", "The Beer-ware License", "Now Playing for mpv", "", "") set_default_options() load_options() weechat.hook_config('plugins.var.python.' + MPV['SCRIPT_NAME'] + '.*', 'reload_options_cb', '') diff --git a/python/mqtt_notify.py b/python/mqtt_notify.py index b1540bb2..b312bf6b 100644 --- a/python/mqtt_notify.py +++ b/python/mqtt_notify.py @@ -1,56 +1,148 @@ # -*- coding: utf-8 -*- # vim: ai ts=4 sts=4 et sw=4 nu - +# +# History +# +# 2018-05-06, Serge van GInderachter +# v0.2: - fix: correct a typo causing AttributeError +# 2016-10-30, Guillaume Subiron +# v0.1: New script mqtt_notify.py: +# send notifications using the MQTT protocol +# from __future__ import (unicode_literals, absolute_import, division, print_function) +try: + import weechat + import_ok = True +except ImportError: + weechat.prnt('', 'mqtt_notify: this script must be run under WeeChat.') + weechat.prnt('', 'Get WeeChat now at: http://www.weechat.org/') + import_ok = False + +try: + import paho.mqtt.client as paho + import json + import socket +except ImportError as message: + weechat.prnt('', 'mqtt_notify: missing package(s): %s' % (message)) + import_ok = False +import sys -import weechat as w -import paho.mqtt.client as mqtt -import json +# @srgvg on Github: +SCRIPT_MAINTAINER = 'Serge van Ginderachter ' SCRIPT_NAME = 'mqtt_notify' SCRIPT_AUTHOR = 'Guillaume Subiron ' -SCRIPT_VERSION = '0.1' +SCRIPT_VERSION = '0.5' SCRIPT_LICENSE = 'WTFPL' SCRIPT_DESC = 'Sends notifications using MQTT' -w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, '', '') - DEFAULT_OPTIONS = { 'mqtt_host': 'localhost', 'mqtt_port': '1883', - 'mqtt_timeout': '60', + 'mqtt_keepalive': '60', 'mqtt_user': '', 'mqtt_password': '', 'mqtt_channel': 'weechat', + 'mqtt_client_name': 'weechat_mqtt_notify', + 'mqtt_message_data': '', # string passed in the data field of the callback + 'mqtt_private_data': 'private', } -for key, val in DEFAULT_OPTIONS.items(): - if not w.config_is_set_plugin(key): - w.config_set_plugin(key, val) -w.hook_print("", "notify_message", "", 1, "on_msg", "") -w.hook_print("", "notify_private", "", 1, "on_msg", "private") -w.hook_print("", "notify_highlight", "", 1, "on_msg", "") # Not sure if needed +def mqtt_on_connect(client, userdata, flags, rc): + + if rc == 0: + weechat.prnt('', 'mqtt_notify: connected successfully') + else: + weechat.prnt( + '', + 'mqtt_notify: failed connecting - return code %s' % + rc) + + +def mqtt_on_disconnect(client, userdata, rc): + if rc != 0: + weechat.prnt( + '', + 'mqtt_notify: unexpected disconnection - return code %s' % + rc) + else: + weechat.prnt('', 'mqtt_notify: disconnected') + + +def weechat_on_msg_cb(*a): -def on_msg(*a): keys = ['data', 'buffer', 'timestamp', 'tags', 'displayed', 'highlight', 'sender', 'message'] msg = dict(zip(keys, a)) - msg['buffer'] = w.buffer_get_string(msg['buffer'], 'short_name') - - cli = mqtt.Client() - if w.config.get_plugin('mqtt_user'): - cli.username_pw_set(w.config_get_plugin('mqtt_user'), - password=w.config_get_plugin('mqtt_password')) - cli.connect(w.config_get_plugin('mqtt_host'), - int(w.config_get_plugin('mqtt_port')), - int(w.config_get_plugin('mqtt_timeout'))) - cli.publish(w.config_get_plugin('mqtt_channel'), - json.dumps(msg), retain=True) - - return w.WEECHAT_RC_OK + + msg['buffer_long'] = weechat.buffer_get_string(msg['buffer'], 'name') + msg['buffer_full'] = weechat.buffer_get_string(msg['buffer'], 'full_name') + msg['buffer'] = weechat.buffer_get_string(msg['buffer'], 'short_name') + + mqttclient.publish(weechat.config_get_plugin('mqtt_channel'), + json.dumps(msg), retain=True) + + return weechat.WEECHAT_RC_OK + + +def mqtt_notify_script_unload(): + + mqttclient.loop_stop() + mqttclient.disconnect() + return weechat.WEECHAT_RC_OK + + +if import_ok: + + weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, + SCRIPT_LICENSE, SCRIPT_DESC, 'mqtt_notify_script_unload', + '') + + for key, val in DEFAULT_OPTIONS.items(): + if not weechat.config_is_set_plugin(key): + weechat.config_set_plugin(key, val) + + # Setup the MQTT client + mqttclient = paho.Client(client_id=weechat.config_get_plugin( + 'mqtt_client_name'), clean_session=False) + mqttclient.on_connect = mqtt_on_connect + mqttclient.on_disconnect = mqtt_on_disconnect + + if weechat.config_get_plugin('mqtt_user'): + mqttclient.username_pw_set(weechat.config_get_plugin('mqtt_user'), + password=weechat.config_get_plugin( + 'mqtt_password')) + try: + mqttclient.connect_async(weechat.config_get_plugin('mqtt_host'), + int(weechat.config_get_plugin('mqtt_port')), + int(weechat.config_get_plugin( + 'mqtt_keepalive'))) + mqttclient.loop_start() + except socket.error as err: + # mqttclient loop runs in background thread + # and wil keep trying to reconnect + pass + + weechat.hook_print("", "notify_message", "", 1, "weechat_on_msg_cb", + weechat.config_get_plugin("mqtt_message_data")) + weechat.hook_print("", "notify_private", "", 1, "weechat_on_msg_cb", + weechat.config_get_plugin("mqtt_private_data")) diff --git a/python/noirccolors.py b/python/noirccolors.py index 3af95689..a1d91b2f 100644 --- a/python/noirccolors.py +++ b/python/noirccolors.py @@ -2,18 +2,25 @@ SCRIPT_NAME = "noirccolors" SCRIPT_AUTHOR = "Fredrick Brennan " -SCRIPT_VERSION = "0.2" +SCRIPT_VERSION = "0.4" SCRIPT_LICENSE = "Public domain" -SCRIPT_DESC = "Remove IRC colors from buffers with the localvar 'noirccolors' set. To disable IRC colors in the current buffer, type /buffer set localvar_set_noirccolors true. You can also set this with autosetbuffer. :)" +SCRIPT_DESC = "Remove IRC colors from buffers with the localvar 'noirccolors' set. To disable IRC colors in the current buffer, type /buffer set localvar_set_noirccolors true. You can also set this with script buffer_autoset.py. :)" w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', '') def my_modifier_cb(data, modifier, modifier_data, string): - if w.buffer_get_string(w.buffer_search('irc',modifier_data.split(";")[1]),"localvar_noirccolors") == "true": + if modifier_data.startswith('0x'): + # WeeChat >= 2.9 + buffer, tags = modifier_data.split(';', 1) + else: + # WeeChat <= 2.8 + plugin, buffer_name, tags = modifier_data.split(';', 2) + buffer = w.buffer_search(plugin, buffer_name) + if w.buffer_get_string(buffer, "localvar_noirccolors") == "true": try: nick, message = string.split("\t") - except ValueError, e: + except ValueError as e: return string return "%s\t%s" % (nick, w.string_remove_color(message,"")) else: diff --git a/python/notification.py b/python/notification.py new file mode 100644 index 00000000..a814548d --- /dev/null +++ b/python/notification.py @@ -0,0 +1,866 @@ +# +# Copyright (C) 2014 Guido Berhoerster +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import os +import sys +import time +import re +import select +import signal +import errno +import fcntl +import cgi +import multiprocessing + + +SCRIPT_NAME = 'notification' +APPLICATION = 'Weechat' +VERSION = '1' +AUTHOR = 'Guido Berhoerster' +COPYRIGHT = '(C) 2014 Guido Berhoerster' +SUBTITLE = 'Notification Plugin for Weechat' +HOMEPAGE = 'https://code.guido-berhoerster.org/addons/weechat-scripts/weechat-notification-script/' +EMAIL = 'guido+weechat@berhoerster.name' +DESCRIPTION = 'Notifies of a number of events through desktop notifications ' \ + 'and an optional status icon' +DEFAULT_SETTINGS = { + 'status_icon': ('weechat', 'path or name of the status icon'), + 'notification_icon': ('weechat', 'path or name of the icon shown in ' + 'notifications'), + 'preferred_toolkit': ('', 'preferred UI toolkit'), + 'notify_on_displayed_only': ('on', 'only notify of messages that are ' + 'actually displayed'), + 'notify_on_privmsg': ('on', 'notify when receiving a private message'), + 'notify_on_highlight': ('on', 'notify when a messages is highlighted'), + 'notify_on_dcc_request': ('on', 'notify on DCC requests') +} +BUFFER_SIZE = 1024 + + +class NetstringParser(object): + """Netstring Stream Parser""" + + IN_LENGTH = 0 + IN_STRING = 1 + + def __init__(self, on_string_complete): + self.on_string_complete = on_string_complete + self.length = 0 + self.input_buffer = '' + self.state = self.IN_LENGTH + + def parse(self, data): + self.input_buffer += data + ret = True + while ret: + if self.state == self.IN_LENGTH: + ret = self.parse_length() + else: + ret = self.parse_string() + + def parse_length(self): + length, delimiter, self.input_buffer = self.input_buffer.partition(':') + if not delimiter: + return False + try: + self.length = int(length) + except ValueError: + raise SyntaxError('Invalid length: %s' % length) + self.state = self.IN_STRING + return True + + def parse_string(self): + input_buffer_len = len(self.input_buffer) + if input_buffer_len < self.length + 1: + return False + string = self.input_buffer[0:self.length] + if self.input_buffer[self.length] != ',': + raise SyntaxError('Missing delimiter') + self.input_buffer = self.input_buffer[self.length + 1:] + self.length = 0 + self.state = self.IN_LENGTH + self.on_string_complete(string) + return True + + +def netstring_encode(*args): + return ''.join(['%d:%s,' % (len(element), element) for element in + args]) + +def netstring_decode(netstring): + result = [] + def append_result(data): + result.append(data) + np = NetstringParser(append_result) + np.parse(netstring) + return result + +def dispatch_weechat_callback(*args): + return weechat_callbacks[args[0]](*args) + +def create_weechat_callback(method): + global weechat_callbacks + + method_id = str(id(method)) + weechat_callbacks[method_id] = method + return method_id + + +class Notifier(object): + """Simple notifier which discards all notifications, base class for all + other notifiers + """ + + def __init__(self, icon): + flags = fcntl.fcntl(sys.stdin, fcntl.F_GETFL) + fcntl.fcntl(sys.stdin, fcntl.F_SETFL, flags | os.O_NONBLOCK) + + self.parser = NetstringParser(self.on_command_received) + + def on_command_received(self, raw_command): + command_args = netstring_decode(raw_command) + if len(command_args) > 1: + command = command_args[0] + args = netstring_decode(command_args[1]) + else: + command = command_args[0] + args = [] + getattr(self, command)(*args) + + def notify(self, summary, message, icon): + pass + + def reset(self): + pass + + def run(self): + poll = select.poll() + poll.register(sys.stdin, select.POLLIN | select.POLLPRI) + + while True: + try: + events = poll.poll() + except select.error as e: + if e.args and e.args[0] == errno.EINTR: + continue + else: + raise e + for fd, event in events: + if event & (select.POLLIN | select.POLLPRI): + buffer_ = os.read(fd, BUFFER_SIZE) + if buffer_ != '': + self.parser.parse(buffer_) + if event & (select.POLLERR | select.POLLHUP | select.POLLNVAL): + sys.exit(1) + + +class Gtk2Notifier(Notifier): + """GTK 2 notifier based on pygtk and pynotify""" + + def __init__(self, icon): + super(Gtk2Notifier, self).__init__(icon) + + pynotify.init(APPLICATION) + + gobject.io_add_watch(sys.stdin, gobject.IO_IN | gobject.IO_PRI, + self.on_input) + + if not icon: + icon_name = None + icon_pixbuf = None + elif icon.startswith('/'): + icon_name = None + try: + icon_pixbuf = gtk.gdk.Pixbuf.new_from_file(icon) + except gobject.GError: + icon_pixbuf = None + else: + icon_name = icon + icon_pixbuf = None + + if icon_name or icon_pixbuf: + self.status_icon = gtk.StatusIcon() + self.status_icon.set_title(APPLICATION) + self.status_icon.set_tooltip_text(APPLICATION) + self.status_icon.connect('activate', self.on_activate) + if icon_name: + self.status_icon.set_from_icon_name(icon_name) + elif icon_pixbuf: + self.status_icon.set_from_pixbuf(icon_pixbuf) + else: + self.status_icon = None + + def on_input(self, fd, cond): + if cond & (gobject.IO_IN | gobject.IO_PRI): + try: + buffer_ = os.read(fd.fileno(), BUFFER_SIZE) + if buffer_ != '': + self.parser.parse(buffer_) + except EOFError: + gtk.main_quit() + return False + + if cond & (gobject.IO_ERR | gobject.IO_HUP): + gtk.main_quit() + return False + + return True + + def on_activate(self, widget): + self.reset() + + def on_notification_closed(self, notification): + if notification.get_closed_reason() == 2: + self.reset() + + def notify(self, summary, message, icon): + if self.status_icon: + self.status_icon.set_tooltip_text('%s: %s' % (APPLICATION, + summary)) + self.status_icon.set_blinking(True) + + if icon and icon.startswith('/'): + icon_name = None + try: + icon_pixbuf = gtk.gdk.Pixbuf.new_from_file(icon) + except gobject.GError: + icon_pixbuf = None + else: + icon_name = icon + icon_pixbuf = None + + if 'body-markup' in pynotify.get_server_caps(): + body = cgi.escape(message) + else: + body = message + + notification = pynotify.Notification(summary, body, icon_name) + if icon_pixbuf is not None: + notification.set_image_from_pixbuf(icon_pixbuf) + notification.connect('closed', self.on_notification_closed) + notification.show() + + def reset(self): + if self.status_icon: + self.status_icon.set_tooltip_text(APPLICATION) + self.status_icon.set_blinking(False) + + def run(self): + gtk.main() + + +class Gtk3Notifier(Notifier): + """GTK3 notifier based on GObject Introspection Bindings for GTK 3 and + libnotify + """ + + def __init__(self, icon): + super(Gtk3Notifier, self).__init__(icon) + + Notify.init(APPLICATION) + + GLib.io_add_watch(sys.stdin, GLib.IO_IN | GLib.IO_PRI, self.on_input) + + if not icon: + self.icon_name = None + self.icon_pixbuf = None + elif icon.startswith('/'): + self.icon_name = None + try: + self.icon_pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon) + except GLib.GError: + self.icon_pixbuf = None + else: + self.icon_name = icon + self.icon_pixbuf = None + + if self.icon_name or self.icon_pixbuf: + # create blank, fully transparent pixbuf in order to simulate + # blinking + self.blank_pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, + True, 8, 22, 22) + self.blank_pixbuf.fill(0x00) + + self.blink_on = True + self.blink_timeout_id = None + + self.status_icon = Gtk.StatusIcon.new() + self.status_icon.set_title(APPLICATION) + self.status_icon.set_tooltip_text(APPLICATION) + self.status_icon.connect('activate', self.on_activate) + self.update_icon() + else: + self.status_icon = None + + def on_input(self, fd, cond): + if cond & (GLib.IO_IN | GLib.IO_PRI): + try: + self.parser.parse(os.read(fd.fileno(), BUFFER_SIZE)) + except EOFError: + Gtk.main_quit() + return False + + if cond & (GLib.IO_ERR | GLib.IO_HUP): + Gtk.main_quit() + return False + + return True + + def on_activate(self, widget): + self.reset() + + def update_icon(self): + if not self.blink_on: + self.status_icon.set_from_pixbuf(self.blank_pixbuf) + elif self.icon_name: + self.status_icon.set_from_icon_name(self.icon_name) + elif self.icon_pixbuf: + self.status_icon.set_from_pixbuf(self.icon_pixbuf) + + def on_blink_timeout(self): + self.blink_on = not self.blink_on + self.update_icon() + return True + + def on_notification_closed(self, notification): + if notification.get_closed_reason() == 2: + self.reset() + + def notify(self, summary, message, icon): + if self.status_icon: + self.status_icon.set_tooltip_text('%s: %s' % (APPLICATION, + summary)) + if self.blink_timeout_id is None: + self.blink_timeout_id = GLib.timeout_add(500, + self.on_blink_timeout) + + if icon and icon.startswith('/'): + icon_name = None + try: + icon_pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon) + except GLib.GError: + icon_pixbuf = None + else: + icon_name = icon + icon_pixbuf = None + + if 'body-markup' in Notify.get_server_caps(): + body = cgi.escape(message) + else: + body = message + + notification = Notify.Notification.new(summary, body, icon_name) + if icon_pixbuf is not None: + notification.set_image_from_pixbuf(icon_pixbuf) + notification.connect('closed', self.on_notification_closed) + notification.show() + + def reset(self): + if self.status_icon: + self.status_icon.set_tooltip_text(APPLICATION) + if self.blink_timeout_id is not None: + GLib.source_remove(self.blink_timeout_id) + self.blink_timeout_id = None + self.blink_on = True + self.update_icon() + + def run(self): + Gtk.main() + + +class Qt4Notifier(Notifier): + """Qt 4 notifier""" + + def __init__(self, icon): + super(Qt4Notifier, self).__init__(icon) + + signal.signal(signal.SIGINT, self.on_sigint) + + self.qapplication = QtGui.QApplication([]) + + self.readable_notifier = QtCore.QSocketNotifier(sys.stdin.fileno(), + QtCore.QSocketNotifier.Read) + self.readable_notifier.activated.connect(self.on_input) + self.readable_notifier.setEnabled(True) + + if not icon: + self.icon = None + elif icon.startswith('/'): + self.icon = QtGui.QIcon(icon) + else: + self.icon = QtGui.QIcon.fromTheme(icon) + + if self.icon: + # create blank, fully transparent pixbuf in order to simulate + # blinking + self.blank_icon = QtGui.QIcon() + + self.blink_on = True + self.blinking_timer = QtCore.QTimer() + self.blinking_timer.setInterval(500) + self.blinking_timer.timeout.connect(self.on_blink_timeout) + + self.status_icon = QtGui.QSystemTrayIcon() + self.status_icon.setToolTip(APPLICATION) + self.update_icon() + self.status_icon.setVisible(True) + self.status_icon.activated.connect(self.on_activated) + self.status_icon.messageClicked.connect(self.on_message_clicked) + else: + self.status_icon = None + + def on_sigint(self, signo, frame): + self.qapplication.exit(0) + + def on_input(self, fd): + try: + self.parser.parse(os.read(fd, BUFFER_SIZE)) + except EOFError: + self.qapplication.exit(1) + + def on_activated(self, reason): + self.reset() + + def on_message_clicked(self): + self.reset() + + def on_blink_timeout(self): + self.blink_on = not self.blink_on + self.update_icon() + + def update_icon(self): + if not self.blink_on: + self.status_icon.setIcon(self.blank_icon) + else: + self.status_icon.setIcon(self.icon) + + def notify(self, summary, message, icon): + if self.status_icon: + self.status_icon.setToolTip('%s: %s' % (APPLICATION, + cgi.escape(summary))) + self.blinking_timer.start() + if self.status_icon.supportsMessages(): + self.status_icon.showMessage(summary, message, + QtGui.QSystemTrayIcon.NoIcon) + + def reset(self): + if self.status_icon: + self.blinking_timer.stop() + self.blink_on = True + self.update_icon() + self.status_icon.setToolTip(APPLICATION) + + def run(self): + sys.exit(self.qapplication.exec_()) + + +class KDE4Notifier(Notifier): + """KDE 4 notifier based on PyKDE4""" + + def __init__(self, icon): + super(KDE4Notifier, self).__init__(icon) + + signal.signal(signal.SIGINT, self.on_sigint) + + aboutData = kdecore.KAboutData(APPLICATION.lower(), '', + kdecore.ki18n(APPLICATION), VERSION, kdecore.ki18n(SUBTITLE), + kdecore.KAboutData.License_GPL_V3, kdecore.ki18n(COPYRIGHT), + kdecore.ki18n (''), HOMEPAGE, EMAIL) + kdecore.KCmdLineArgs.init(aboutData) + self.kapplication = kdeui.KApplication() + + self.readable_notifier = QtCore.QSocketNotifier(sys.stdin.fileno(), + QtCore.QSocketNotifier.Read) + self.readable_notifier.activated.connect(self.on_input) + self.readable_notifier.setEnabled(True) + + if not icon: + icon_qicon = None + icon_name = None + elif icon.startswith('/'): + icon_qicon = QtGui.QIcon(icon) + icon_name = None + else: + icon_qicon = None + icon_name = icon + + if icon_name or icon_pixmap: + self.status_notifier = kdeui.KStatusNotifierItem(self.kapplication) + self.status_notifier.setCategory( + kdeui.KStatusNotifierItem.Communications) + if icon_name: + self.status_notifier.setIconByName(icon_name) + self.status_notifier.setToolTip(icon_name, APPLICATION, + SUBTITLE) + else: + self.status_notifier.setIconByPixmap(icon_qicon) + self.status_notifier.setToolTip(icon_qicon, APPLICATION, + SUBTITLE) + self.status_notifier.setStandardActionsEnabled(False) + self.status_notifier.setStatus(kdeui.KStatusNotifierItem.Active) + self.status_notifier.setTitle(APPLICATION) + self.status_notifier.activateRequested.connect( + self.on_activate_requested) + else: + self.status_notifier = None + + def on_sigint(self, signo, frame): + self.kapplication.exit(0) + + def on_input(self, fd): + try: + self.parser.parse(os.read(fd, BUFFER_SIZE)) + except EOFError: + self.kapplication.exit(1) + + def on_activate_requested(self, active, pos): + self.reset() + + def notify(self, summary, message, icon): + if self.status_notifier: + self.status_notifier.setToolTipSubTitle(cgi.escape(summary)) + self.status_notifier.setStatus( + kdeui.KStatusNotifierItem.NeedsAttention) + + if icon: + if icon.startswith('/'): + pixmap = QtGui.QPixmap.load(icon) + else: + pixmap = kdeui.KIcon(icon).pixmap(kdeui.KIconLoader.SizeHuge, + kdeui.KIconLoader.SizeHuge) + else: + pixmap = QtGui.QPixmap() + kdeui.KNotification.event(kdeui.KNotification.Notification, summary, + cgi.escape(message), pixmap) + + def reset(self): + if self.status_notifier: + self.status_notifier.setStatus(kdeui.KStatusNotifierItem.Active) + self.status_notifier.setToolTipTitle(APPLICATION) + self.status_notifier.setToolTipSubTitle(SUBTITLE) + + def run(self): + sys.exit(self.kapplication.exec_()) + + +class NotificationProxy(object): + """Proxy object for interfacing with the notifier process""" + + def __init__(self, preferred_toolkit, status_icon): + self.script_file = os.path.realpath(__file__) + self._status_icon = status_icon + self._preferred_toolkit = preferred_toolkit + self.notifier_process_hook = None + self.spawn_timer_hook = None + self.next_spawn_time = 0.0 + + self.spawn_notifier_process() + + @property + def status_icon(self): + return self._status_icon + + @status_icon.setter + def status_icon(self, value): + self._status_icon = value + self.terminate_notifier_process() + self.spawn_notifier_process() + + @property + def preferred_toolkit(self): + return self._preferred_toolkit + + @preferred_toolkit.setter + def preferred_toolkit(self, value): + self._preferred_toolkit = value + self.terminate_notifier_process() + self.spawn_notifier_process() + + def on_notifier_process_event(self, data, command, return_code, output, + error_output): + if return_code != weechat.WEECHAT_HOOK_PROCESS_RUNNING: + if return_code == weechat.WEECHAT_HOOK_PROCESS_ERROR: + error = '%sfailed to run notifier' % weechat.prefix("error") + else: + error = '%snotifier exited with exit status %d' % \ + (weechat.prefix("error"), return_code) + if output: + error += '\nstdout:%s' % output + if error_output: + error += '\nstderr:%s' % error_output + weechat.prnt('', error) + self.notifier_process_hook = None + self.spawn_notifier_process() + return weechat.WEECHAT_RC_OK + + def on_spawn_timer(self, data, remaining): + self.spawn_timer_hook = None + if not self.notifier_process_hook: + self.spawn_notifier_process() + return weechat.WEECHAT_RC_OK + + def spawn_notifier_process(self): + if self.notifier_process_hook or self.spawn_timer_hook: + return + + # do not try to respawn a notifier more than once every ten seconds + now = time.time() + if long(self.next_spawn_time - now) > 0: + self.spawn_timer_hook = \ + weechat.hook_timer(long((self.next_spawn_time - now) * + 1000), 0, 1, 'dispatch_weechat_callback', + create_weechat_callback(self.on_spawn_timer)) + return + + self.next_spawn_time = now + 10 + self.notifier_process_hook = \ + weechat.hook_process_hashtable(sys.executable, {'arg1': + self.script_file, 'arg2': self.preferred_toolkit, 'arg3': + self.status_icon, 'stdin': '1'}, 0, + 'dispatch_weechat_callback', + create_weechat_callback(self.on_notifier_process_event)) + + def terminate_notifier_process(self): + if self.spawn_timer_hook: + weechat.unhook(self.spawn_timer_hook) + self.spawn_timer_hook = None + if self.notifier_process_hook: + weechat.unhook(self.notifier_process_hook) + self.notifier_process_hook = None + self.next_spawn_time = 0.0 + + def send(self, command, *args): + if self.notifier_process_hook: + if args: + weechat.hook_set(self.notifier_process_hook, 'stdin', + netstring_encode(netstring_encode(command, + netstring_encode(*args)))) + else: + weechat.hook_set(self.notifier_process_hook, 'stdin', + netstring_encode(netstring_encode(command))) + + def notify(self, summary, message, icon): + self.send('notify', summary, message, icon) + + def reset(self): + self.send('reset') + + +class NotificationPlugin(object): + """Weechat plugin""" + + def __init__(self): + self.DCC_SEND_RE = re.compile(r':(?P\S+) PRIVMSG \S+ :' + r'\x01DCC SEND (?P\S+) \d+ \d+ (?P\d+)') + self.DCC_CHAT_RE = re.compile(r':(?P\S+) PRIVMSG \S+ :' + r'\x01DCC CHAT ') + + weechat.register(SCRIPT_NAME, AUTHOR, VERSION, 'GPL3', DESCRIPTION, '', + '') + + for option, (value, description) in DEFAULT_SETTINGS.iteritems(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value) + weechat.config_set_desc_plugin(option, '%s (default: "%s")' % + (description, value)) + + self.notification_proxy = NotificationProxy( + weechat.config_get_plugin('preferred_toolkit'), + weechat.config_get_plugin('status_icon')) + + weechat.hook_print('', 'irc_privmsg', '', 1, + 'dispatch_weechat_callback', + create_weechat_callback(self.on_message)) + weechat.hook_signal('key_pressed', 'dispatch_weechat_callback', + create_weechat_callback(self.on_key_pressed)) + weechat.hook_signal('irc_dcc', 'dispatch_weechat_callback', + create_weechat_callback(self.on_dcc)) + weechat.hook_config('plugins.var.python.%s.*' % SCRIPT_NAME, + 'dispatch_weechat_callback', + create_weechat_callback(self.on_config_changed)) + + def on_message(self, data, buffer, date, tags, displayed, highlight, + prefix, message): + if weechat.config_get_plugin('notify_on_displayed_only') == 'on' and \ + int(displayed) != 1: + return weechat.WEECHAT_RC_OK + + formatted_date = time.strftime('%H:%M', time.localtime(float(date))) + if 'notify_private' in tags.split(',') and \ + weechat.config_get_plugin('notify_on_privmsg') == 'on': + summary = 'Private message from %s at %s' % (prefix, + formatted_date) + self.notification_proxy.notify(summary, message, + weechat.config_get_plugin('notification_icon')) + elif int(highlight) == 1 and \ + weechat.config_get_plugin('notify_on_highlight') == 'on': + summary = 'Highlighted message from %s at %s' % (prefix, + formatted_date) + self.notification_proxy.notify(summary, message, + weechat.config_get_plugin('notification_icon')) + + return weechat.WEECHAT_RC_OK + + def on_dcc(self, data, signal, signal_data): + if weechat.config_get_plugin('notify_on_dcc') != 'on': + return weechat.WEECHAT_RC_OK + + matches = self.DCC_SEND_RE.match(signal_data) + if matches: + summary = 'DCC send request from %s' % matches.group('sender') + message = 'Filname: %s, Size: %d bytes' % \ + (matches.group('filename'), int(matches.group('size'))) + self.notification_proxy.notify(summary, message, + weechat.config_get_plugin('notification_icon')) + return weechat.WEECHAT_RC_OK + + matches = self.DCC_CHAT_RE.match(signal_data) + if matches: + summary = 'DCC chat request from %s' % matches.group('sender') + message = '' + self.notification_proxy.notify(summary, message, + weechat.config_get_plugin('notification_icon')) + return weechat.WEECHAT_RC_OK + + return weechat.WEECHAT_RC_OK + + def on_key_pressed(self, data, signal, signal_data): + self.notification_proxy.reset() + return weechat.WEECHAT_RC_OK + + def on_config_changed(self, data, option, value): + if option.endswith('.preferred_toolkit'): + self.notification_proxy.preferred_toolkit = value + elif option.endswith('.status_icon'): + self.notification_proxy.status_icon = value + return weechat.WEECHAT_RC_OK + + +def import_modules(modules): + for module_name, fromlist in modules: + if fromlist: + module = __import__(module_name, fromlist=fromlist) + for identifier in fromlist: + globals()[identifier] = getattr(module, identifier) + else: + globals()[module_name] = __import__(module_name) + +def try_import_modules(modules): + try: + import_modules(modules) + except ImportError: + sys.exit(1) + sys.exit(0) + + +if __name__ == '__main__': + if sys.argv[0] == '__weechat_plugin__': + # running as Weechat plugin + import weechat + + weechat_callbacks = {} + + plugin = NotificationPlugin() + elif len(sys.argv) == 3: + # running as the notifier process + preferred_toolkit = sys.argv[1] + icon = sys.argv[2] + + # required modules for each toolkit + toolkits_modules = { + 'gtk3': [ + ('gi.repository', [ + 'GLib', + 'GdkPixbuf', + 'Gtk', + 'Notify' + ]) + ], + 'gtk2': [ + ('pygtk', []), + ('gobject', []), + ('gtk', []), + ('pynotify', []) + ], + 'qt4': [ + ('PyQt4', [ + 'QtGui', + 'QtCore' + ]) + ], + 'kde4': [ + ('PyQt4', [ + 'QtGui', + 'QtCore' + ]), + ('PyKDE4', [ + 'kdecore', + 'kdeui' + ]) + ], + '': [] + } + available_toolkits = [] + selected_toolkit = '' + + # find available toolkits by spawning a process for each toolkit which + # tries to import all required modules and returns an exit status of 1 + # in case of an import error + for toolkit in toolkits_modules: + process = multiprocessing.Process(target=try_import_modules, + args=(toolkits_modules[toolkit],)) + process.start() + process.join(3) + if process.is_alive(): + process.terminate() + process.join() + if process.exitcode == 0: + available_toolkits.append(toolkit) + + # select toolkit based on either explicit preference or the + # availability of modules and the used desktop environment + if preferred_toolkit: + if preferred_toolkit in available_toolkits: + selected_toolkit = preferred_toolkit + else: + if 'KDE_FULL_SESSION' in os.environ: + # preferred order if running KDE4 + toolkits = ['kde4', 'qt4', 'gtk3', 'gtk2'] + else: + # preferred order for all other desktop environments + toolkits = ['gtk3', 'gtk2', 'qt4', 'kde4'] + for toolkit in toolkits: + if toolkit in available_toolkits: + selected_toolkit = toolkit + break + + # import required toolkit modules + import_modules(toolkits_modules[selected_toolkit]) + + # run selected notifier + if selected_toolkit == 'gtk3': + notifier = Gtk3Notifier(icon) + elif selected_toolkit == 'gtk2': + notifier = Gtk2Notifier(icon) + elif selected_toolkit == 'qt4': + notifier = Qt4Notifier(icon) + elif selected_toolkit == 'kde4': + notifier = KDE4Notifier(icon) + else: + notifier = Notifier(icon) + notifier.run() + else: + sys.exit(1) diff --git a/python/notification_center.py b/python/notification_center.py index 368f0fd4..257ba0a2 100644 --- a/python/notification_center.py +++ b/python/notification_center.py @@ -9,13 +9,18 @@ SCRIPT_NAME = 'notification_center' SCRIPT_AUTHOR = 'Sindre Sorhus ' -SCRIPT_VERSION = '1.3.0' +SCRIPT_VERSION = '1.5.2' SCRIPT_LICENSE = 'MIT' -SCRIPT_DESC = 'Pass highlights and private messages to the OS X 10.8+ Notification Center' -WEECHAT_ICON = os.path.expanduser('~/.weechat/weechat.png') +SCRIPT_DESC = 'Pass highlights and private messages to the macOS Notification Center' weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', '') +WEECHAT_VERSION = weechat.info_get('version_number', '') or 0 +if int(WEECHAT_VERSION) >= 0x03020000: + WEECHAT_ICON = os.path.join(weechat.info_get('weechat_config_dir', ''), 'weechat.png') +else: + WEECHAT_ICON = os.path.join(weechat.info_get('weechat_dir', ''), 'weechat.png') + DEFAULT_OPTIONS = { 'show_highlights': 'on', 'show_private_message': 'on', @@ -24,39 +29,56 @@ 'sound_name': 'Pong', 'activate_bundle_id': 'com.apple.Terminal', 'ignore_old_messages': 'off', + 'ignore_current_buffer_messages': 'off', + 'channels': '', + 'tags': '', } for key, val in DEFAULT_OPTIONS.items(): if not weechat.config_is_set_plugin(key): weechat.config_set_plugin(key, val) -weechat.hook_print('', 'irc_privmsg', '', 1, 'notify', '') +weechat.hook_print('', 'irc_privmsg,' + weechat.config_get_plugin('tags'), '', 1, 'notify', '') def notify(data, buffer, date, tags, displayed, highlight, prefix, message): - # ignore if it's yourself + # Ignore if it's yourself own_nick = weechat.buffer_get_string(buffer, 'localvar_nick') if prefix == own_nick or prefix == ('@%s' % own_nick): return weechat.WEECHAT_RC_OK - # ignore messages older than the configured theshold (such as ZNC logs) if enabled + # Ignore messages from the current buffer + if weechat.config_get_plugin('ignore_current_buffer_messages') == 'on' and buffer == weechat.current_buffer(): + return weechat.WEECHAT_RC_OK + + # Ignore messages older than the configured theshold (such as ZNC logs) if enabled if weechat.config_get_plugin('ignore_old_messages') == 'on': message_time = datetime.datetime.utcfromtimestamp(int(date)) now_time = datetime.datetime.utcnow() - # ignore if the message is greater than 5 seconds old + # Ignore if the message is greater than 5 seconds old if (now_time - message_time).seconds > 5: return weechat.WEECHAT_RC_OK - # passing `None` or `''` still plays the default sound so we pass a lambda instead + # Passing `None` or `''` still plays the default sound so we pass a lambda instead sound = weechat.config_get_plugin('sound_name') if weechat.config_get_plugin('sound') == 'on' else lambda:_ activate_bundle_id = weechat.config_get_plugin('activate_bundle_id') - if weechat.config_get_plugin('show_highlights') == 'on' and int(highlight): - channel = weechat.buffer_get_string(buffer, 'localvar_channel') + + channel_allow_list = [] + if weechat.config_get_plugin('channels') != "": + channel_allow_list = weechat.config_get_plugin('channels').split(',') + channel = weechat.buffer_get_string(buffer, 'localvar_channel') + + if channel in channel_allow_list: + if weechat.config_get_plugin('show_message_text') == 'on': + Notifier.notify(message, title='%s %s' % (prefix, channel), sound=sound, appIcon=WEECHAT_ICON, activate=activate_bundle_id) + else: + Notifier.notify('In %s by %s' % (channel, prefix), title='Channel Activity', sound=sound, appIcon=WEECHAT_ICON, activate=activate_bundle_id) + elif weechat.config_get_plugin('show_highlights') == 'on' and int(highlight): if weechat.config_get_plugin('show_message_text') == 'on': Notifier.notify(message, title='%s %s' % (prefix, channel), sound=sound, appIcon=WEECHAT_ICON, activate=activate_bundle_id) else: Notifier.notify('In %s by %s' % (channel, prefix), title='Highlighted Message', sound=sound, appIcon=WEECHAT_ICON, activate=activate_bundle_id) - elif weechat.config_get_plugin('show_private_message') == 'on' and 'notify_private' in tags: + elif weechat.config_get_plugin('show_private_message') == 'on' and 'irc_privmsg' in tags and 'notify_private' in tags: if weechat.config_get_plugin('show_message_text') == 'on': Notifier.notify(message, title='%s [private]' % prefix, sound=sound, appIcon=WEECHAT_ICON, activate=activate_bundle_id) else: diff --git a/python/notifo.py b/python/notifo.py deleted file mode 100644 index 95b64dba..00000000 --- a/python/notifo.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- -# Author: ochameau -# Homepage: https://github.com/ochameau/weechat-notifo -# Derived from: notify -# Author: lavaramano -# Improved by: BaSh - -# Ported to Weechat 0.3.0 by: Sharn - -# Homepage: http://bitbucket.org/laclefyoshi/weechat/ -# This plugin send push notifications to your iPhone or Android smartphone -# by using Notifo.com mobile application/services -# Requires Weechat 0.3.0 -# Released under GNU GPL v2 -# -# 2014-05-10, Sébastien Helleu -# version 0.2: change hook_print callback argument type of -# displayed/highlight (WeeChat >= 1.0) -# -# 2011-08-27, ochameau : -# version 0.1: merge notify.py and notifo_notify.py in order to avoid -# sending notifications when channel or private buffer is -# already opened - -import weechat, string, urllib, urllib2 - -weechat.register("notifo", "ochameau", "0.2", "GPL", "notifo: Send push notifications to your iPhone/Android about your private messages and highlights.", "", "") - -credentials = { - "username": "", - "api_secret": "" -} - -for option, default_value in credentials.items(): - if weechat.config_get_plugin(option) == "": - weechat.prnt("", weechat.prefix("error") + "notifo: Please set option: %s" % option) - weechat.prnt("", "notifo: /set plugins.var.python.notifo.%s STRING" % option) - -# Hook privmsg/hilights -weechat.hook_print("", "irc_privmsg", "", 1, "notify_show", "") - -# Functions -def notify_show(data, bufferp, uber_empty, tagsn, isdisplayed, - ishilight, prefix, message): - - if (bufferp == weechat.current_buffer()): - pass - - elif weechat.buffer_get_string(bufferp, "localvar_type") == "private": - show_notification(prefix, message) - - elif int(ishilight): - buffer = (weechat.buffer_get_string(bufferp, "short_name") or - weechat.buffer_get_string(bufferp, "name")) - show_notification(buffer, prefix + ": " + message) - - return weechat.WEECHAT_RC_OK - -def show_notification(chan, message): - NOTIFO_USER = weechat.config_get_plugin("username") - NOTIFO_API_SECRET = weechat.config_get_plugin("api_secret") - if NOTIFO_USER != "" and NOTIFO_API_SECRET != "": - url = "https://api.notifo.com/v1/send_notification" - opt_dict = { - "msg": message, - "label": "weechat", - "title": chan - } - opt = urllib.urlencode(opt_dict) - basic = "Basic %s" % ":".join([NOTIFO_USER, NOTIFO_API_SECRET]).encode("base64").strip() - python2_bin = weechat.info_get("python2_bin", "") or "python" - weechat.hook_process( - python2_bin + " -c \"import urllib2\n" - "req = urllib2.Request('" + url + "', '" + opt + "')\n" - "req.add_header('Authorization', '" + basic + "')\n" - "res = urllib2.urlopen(req)\n\"", - 30 * 1000, "", "") - -# vim: autoindent expandtab smarttab shiftwidth=4 diff --git a/python/notify.py b/python/notify.py index 7c583f4e..1fbeedf5 100644 --- a/python/notify.py +++ b/python/notify.py @@ -6,6 +6,8 @@ # To make it work, you may need to download: python-notify2 (and libnotify - libgtk) # Requires Weechat 0.3.0 # Released under GNU GPL v2 +# 2020-05-10, Balint Reczey +# version 0.0.9: make script compatible with Python 3 # 2014-05-10, Sébastien Helleu # version 0.0.8: change hook_print callback argument type of # displayed/highlight (WeeChat >= 1.0) @@ -35,10 +37,12 @@ # 2009-05-02, FlashCode : # version 0.0.2.1: sync with last API changes +from __future__ import print_function + # script variables SCRIPT_NAME = "notify" SCRIPT_AUTHOR = "lavaramano" -SCRIPT_VERSION = "0.0.8" +SCRIPT_VERSION = "0.0.9" SCRIPT_LICENSE = "GPL" SCRIPT_DESC = "notify: A real time notification system for weechat" @@ -47,15 +51,15 @@ try: import weechat except ImportError: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") import_ok = False # make sure we have notify2. try: import notify2 -except ImportError, message: - print "Missing package(s) for %s: %s" % (SCRIPT_NAME, message) - print "You must have notify2 installed." +except ImportError as message: + print("Missing package(s) for %s: %s" % (SCRIPT_NAME, message)) + print("You must have notify2 installed.") import_ok = False # script options @@ -113,7 +117,7 @@ def show_notification(chan, message): try: wn.show() return None - except Exception, e: + except Exception as e: return "Exception trying to show notification: {0}".format(e) if __name__ == "__main__": diff --git a/python/openbsd_privdrop.py b/python/openbsd_privdrop.py new file mode 100644 index 00000000..fd6ca2b4 --- /dev/null +++ b/python/openbsd_privdrop.py @@ -0,0 +1,164 @@ +# Copyright (c) 2022, 2024 Alvar Penning +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +# The openbsd_privdrop.py script tries to achieve the principle of least +# privilege for a WeeChat running on OpenBSD by restricting both the available +# system operations as well as the available file system paths by pledge(2) and +# unveil(2). As those functions are OpenBSD-specific, this script does not work +# on any other operating system. +# +# The respective filters are set by configuration variables. On first run or in +# the absence of configuration entries, sane defaults are set. These should be +# sufficient for a normal WeeChat installation, but would have to be adjusted, +# for example, when using other scripts or plugins. So the default does not +# allow the execution of other programs and assumes a home directory under +# /home/$USERNAME for unveil(2). +# +# - https://man.openbsd.org/pledge.2 +# - https://man.openbsd.org/unveil.2 +# +# The config options for the SETTINGS below are: +# - plugins.var.python.openbsd_privdrop.pledge_promises +# - plugins.var.python.openbsd_privdrop.pledge_execpromises +# - plugins.var.python.openbsd_privdrop.unveil + +# History: +# +# 2024-08-19, Alvar Penning +# version 0.1.2: add fattr to pledge_promises and a bit more documentation +# +# 2022-11-09, Alvar Penning +# version 0.1.1: sane defaults for unveil +# +# 2022-09-18, Alvar Penning +# version 0.1.0: initial release + + +import ctypes +import os +import sys +import weechat + + +SCRIPT_NAME = "openbsd_privdrop" +SCRIPT_AUTHOR = "Alvar Penning " +SCRIPT_VERSION = "0.1.2" +SCRIPT_LICENSE = "ISC" +SCRIPT_DESC = "Drop WeeChat's privileges through OpenBSD's pledge(2) and unveil(2)." + +SETTINGS = { + "pledge_promises": ( + "stdio rpath wpath cpath dpath inet fattr flock unix dns sendfd recvfd tty proc error", + "List of promises for pledge(2).", + ), + "pledge_execpromises": ( + "", + "List of promises to executed processes; requires exec in pledge_promises.", + ), + "unveil": ( + ";".join([ + # Full read/write access (no exec!) to the user's home directory. + # This may be tightened, especially if WeeChat is not run as a separate user. + "~:rwc", + # WeeChat `stat`s /home while building the path to /home/$USER/... + # Might be changed if the home directory lies somewhere else. + # This happens by weechat_mkdir_parents calls, e.g., from logger_create_directory. + "/home:r", + # Other scripts might load some library or a third-party Python modules later. + "/usr/local/lib:r", + # Necessary for HTTPS validation, e.g., when downloading WeeChat scripts. + "/etc/ssl/cert.pem:r", + ]), + "List of path and permissions for unveil(2). Format: /a/path:rwc;/another/path:rw", + ), +} + + +def libc_func(name): + """ Returns a libc function, e.g., pledge or unveil. + Inspired by https://nullprogram.com/blog/2021/09/15/ + """ + f = ctypes.CDLL(None, use_errno=True)[name] + + def _call_f(*args): + weechat.prnt("", f"*\t{name}{args}") + if f(*args) == -1: + errno = ctypes.get_errno() + raise OSError(errno, os.strerror(errno)) + + return _call_f + + +def config_get(key): + """ Fetch a stored configuration value and normalize the returned string + for libc usage by replacing empty strings through None and converting + non-empty strings to bytes. + """ + value = weechat.config_get_plugin(key) + return value.encode() if value != "" else None + + +def weechat_pledge(): + """ Execute pledge(2) for the configured promise. + """ + pledge = libc_func("pledge") + + promises = config_get("pledge_promises") + execpromises = config_get("pledge_execpromises") + + pledge(promises, execpromises) + + +def weechat_unveil(): + """ Execute unveil(2) for the configured paths. + Unveil should be called before pledge unless "unveil" is promised. + """ + unveil = libc_func("unveil") + + for path_part in config_get("unveil").split(b";"): + path, permissions = path_part.split(b":") + path = weechat.string_eval_path_home(path.decode(), {}, {}, {}).encode() + unveil(path, permissions) + + unveil(None, None) + + +def main(): + """ Main function to load the script and apply the restrictions. + """ + reg = weechat.register( + SCRIPT_NAME, + SCRIPT_AUTHOR, + SCRIPT_VERSION, + SCRIPT_LICENSE, + SCRIPT_DESC, + "", "") + if not reg: + return + + if not sys.platform.startswith("openbsd"): + weechat.prnt("", f"{SCRIPT_NAME} is only supported on OpenBSD") + return + + for key, value in SETTINGS.items(): + if not weechat.config_is_set_plugin(key): + weechat.config_set_plugin(key, value[0]) + weechat.config_set_desc_plugin(key, f"{value[1]} (default: \"{value[0]}\")") + + weechat_unveil() + weechat_pledge() + + +if __name__ == "__main__": + main() diff --git a/python/otr.py b/python/otr.py index 404e96ab..7bfe6586 100644 --- a/python/otr.py +++ b/python/otr.py @@ -1,29 +1,32 @@ # -*- coding: utf-8 -*- -# otr - WeeChat script for Off-the-Record IRC messaging -# -# DISCLAIMER: To the best of my knowledge this script securely provides OTR -# messaging in WeeChat, but I offer no guarantee. Please report any security -# holes you find. -# -# Copyright (c) 2012-2015 Matthew M. Boedicker -# Nils Görs -# Daniel "koolfy" Faucon -# Felix Eckhofer -# -# Report issues at https://github.com/mmb/weechat-otr -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +"""otr - WeeChat script for Off-the-Record IRC messaging + +DISCLAIMER: To the best of my knowledge this script securely provides OTR +messaging in WeeChat, but I offer no guarantee. Please report any security +holes you find. + +Copyright (c) 2012-2015 Matthew M. Boedicker + Nils Görs + Daniel "koolfy" Faucon + Felix Eckhofer + +Report issues at https://github.com/mmb/weechat-otr + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +# pylint: disable=too-many-lines from __future__ import unicode_literals @@ -38,6 +41,9 @@ import shutil import sys +import potr +import weechat + class PythonVersion2(object): """Python 2 version of code that must differ between Python 2 and 3.""" @@ -68,8 +74,7 @@ def to_unicode(self, strng): """Convert a utf-8 encoded string to a Unicode.""" if isinstance(strng, unicode): return strng - else: - return strng.decode('utf-8', 'replace') + return strng.decode('utf-8', 'replace') def to_str(self, strng): """Convert a Unicode to a utf-8 encoded string.""" @@ -87,7 +92,7 @@ def __init__(self, minor): import html.parser self.html_parser = html.parser if self.minor >= 4: - self.html_parser_init_kwargs = { 'convert_charrefs' : True } + self.html_parser_init_kwargs = {'convert_charrefs' : True} else: self.html_parser_init_kwargs = {} @@ -110,22 +115,18 @@ def to_unicode(self, strng): """Convert a utf-8 encoded string to unicode.""" if isinstance(strng, bytes): return strng.decode('utf-8', 'replace') - else: - return strng + return strng def to_str(self, strng): """Convert a Unicode to a utf-8 encoded string.""" return strng -if sys.version_info.major >= 3: +# We cannot use version_info.major as this is only supported on python >= 2.7 +if sys.version_info[0] >= 3: PYVER = PythonVersion3(sys.version_info.minor) else: PYVER = PythonVersion2() -import weechat - -import potr - SCRIPT_NAME = 'otr' SCRIPT_DESC = 'Off-the-Record messaging for IRC' SCRIPT_HELP = """{description} @@ -163,7 +164,7 @@ def to_str(self, strng): SCRIPT_AUTHOR = 'Matthew M. Boedicker' SCRIPT_LICENCE = 'GPL3' -SCRIPT_VERSION = '1.8.0' +SCRIPT_VERSION = '1.9.3' OTR_DIR_NAME = 'otr' @@ -171,9 +172,9 @@ def to_str(self, strng): POLICIES = { 'allow_v2' : 'allow OTR protocol version 2, effectively enable OTR ' - 'since v2 is the only supported version', + 'since v2 is the only supported version', 'require_encryption' : 'refuse to send unencrypted messages when OTR is ' - 'enabled', + 'enabled', 'log' : 'enable logging of OTR conversations', 'send_tag' : 'advertise your OTR capability using the whitespace tag', 'html_escape' : 'escape HTML special characters in outbound messages', @@ -190,9 +191,6 @@ def to_str(self, strng): IRC_SANITIZE_TABLE = dict((ord(char), None) for char in '\n\r\x00') -global otr_debug_buffer -otr_debug_buffer = None - # Patch potr.proto.TaggedPlaintext to not end plaintext tags in a space. # # When POTR adds OTR tags to plaintext it puts them at the end of the message. @@ -204,6 +202,7 @@ def to_str(self, strng): # The patched version also skips OTR tagging for CTCP messages because it # breaks the CTCP format. def patched__bytes__(self): + """Patched potr.proto.TaggedPlainText.__bytes__.""" # Do not tag CTCP messages. if self.msg.startswith(b'\x01') and \ self.msg.endswith(b'\x01'): @@ -270,7 +269,7 @@ def print_buffer(buf, message, level='info'): using color according to level.""" prnt(buf, '{prefix}\t{msg}'.format( prefix=get_prefix(), - msg=colorize(message, 'buffer.{}'.format(level)))) + msg=colorize(message, 'buffer.{0}'.format(level)))) def get_prefix(): """Returns configured message prefix.""" @@ -280,25 +279,19 @@ def get_prefix(): def debug(msg): """Send a debug message to the OTR debug buffer.""" - debug_option = weechat.config_get(config_prefix('general.debug')) - global otr_debug_buffer - - if weechat.config_boolean(debug_option): - if not otr_debug_buffer: - otr_debug_buffer = weechat.buffer_new("OTR Debug", "", "", - "debug_buffer_close_cb", "") - weechat.buffer_set(otr_debug_buffer, 'title', 'OTR Debug') - weechat.buffer_set(otr_debug_buffer, 'localvar_set_no_log', '1') - prnt(otr_debug_buffer, ('{script} debug\t{text}'.format( - script=SCRIPT_NAME, - text=PYVER.unicode(msg) - ))) - -def debug_buffer_close_cb(data, buf): - """Set the OTR debug buffer to None.""" - global otr_debug_buffer - otr_debug_buffer = None - return weechat.WEECHAT_RC_OK + debug_option = config_get_prefixed('general.debug') + + if not weechat.config_boolean(debug_option): + return + + debug_buffer = weechat.buffer_search('python', 'OTR Debug') + if not debug_buffer: + debug_buffer = weechat.buffer_new('OTR Debug', '', '', '', '') + weechat.buffer_set(debug_buffer, 'title', 'OTR Debug') + weechat.buffer_set(debug_buffer, 'localvar_set_no_log', '1') + + prnt(debug_buffer, ('{script} debug\t{text}'.format( + script=SCRIPT_NAME, text=PYVER.unicode(msg)))) def current_user(server_name): """Get the nick and server of the current user on a server.""" @@ -307,8 +300,8 @@ def current_user(server_name): def irc_user(nick, server): """Build an IRC user string from a nick and server.""" return '{nick}@{server}'.format( - nick=nick.lower(), - server=server) + nick=nick.lower(), + server=server) def isupport_value(server, feature): """Get the value of an IRC server feature.""" @@ -328,8 +321,8 @@ def is_a_channel(channel, server): return channel.startswith(prefixes) -# Exception class for PRIVMSG parsing exceptions. class PrivmsgParseException(Exception): + """Exception class for PRIVMSG parsing exceptions.""" pass def parse_irc_privmsg(message, server): @@ -396,18 +389,26 @@ def first_instance(objs, klass): def config_prefix(option): """Add the config prefix to an option and return the full option name.""" return '{script}.{option}'.format( - script=SCRIPT_NAME, - option=option) + script=SCRIPT_NAME, + option=option) def config_color(option): """Get the color of a color config option.""" - return weechat.color(weechat.config_color(weechat.config_get( - config_prefix('color.{}'.format(option))))) + return weechat.color(weechat.config_color(config_get_prefixed( + 'color.{0}'.format(option)))) def config_string(option): """Get the string value of a config option with utf-8 decode.""" return PYVER.to_unicode(weechat.config_string( - weechat.config_get(config_prefix(option)))) + config_get_prefixed(option))) + +def config_get(option): + """Get the value of a WeeChat config option.""" + return weechat.config_get(PYVER.to_str(option)) + +def config_get_prefixed(option): + """Get the value of a script prefixed WeeChat config option.""" + return config_get(config_prefix(option)) def buffer_get_string(buf, prop): """Wrap weechat.buffer_get_string() with utf-8 encode/decode.""" @@ -466,9 +467,9 @@ def format_default_policies(): for policy, desc in sorted(POLICIES.items()): buf.write(' {policy} ({desc}) : {value}\n'.format( - policy=policy, - desc=desc, - value=config_string('policy.default.{}'.format(policy)))) + policy=policy, + desc=desc, + value=config_string('policy.default.{0}'.format(policy)))) buf.write('Change default policies with: /otr policy default NAME on|off') @@ -504,8 +505,8 @@ def show_account_fingerprints(): table_formatter = TableFormatter() for account in accounts(): table_formatter.add_row([ - account.name, - str(account.getPrivkey())]) + account.name, + str(account.getPrivkey())]) print_buffer('', table_formatter.format()) def show_peer_fingerprints(grep=None): @@ -524,16 +525,16 @@ def show_peer_fingerprints(grep=None): for fingerprint, trust in sorted(peer_data.items()): if grep is None or grep in peer: table_formatter.add_row([ - peer, - account.name, - potr.human_hash(fingerprint), - trust_descs[trust], + peer, + account.name, + potr.human_hash(fingerprint), + trust_descs[trust], ]) print_buffer('', table_formatter.format()) def private_key_file_path(account_name): """Return the private key file path for an account.""" - return os.path.join(OTR_DIR, '{}.key3'.format(account_name)) + return os.path.join(OTR_DIR, '{0}.key3'.format(account_name)) def read_private_key(key_file_path): """Return the private key in a private key file.""" @@ -542,6 +543,14 @@ def read_private_key(key_file_path): with open(key_file_path, 'rb') as key_file: return potr.crypt.PK.parsePrivateKey(key_file.read())[0] +def get_context(account_name, context_name): + """Return a context from an account.""" + return ACCOUNTS[account_name].getContext(context_name) + +def get_server_context(server, peer_nick): + """Return the context for the current server user and peer.""" + return get_context(current_user(server), irc_user(peer_nick, server)) + class AccountDict(collections.defaultdict): """Dictionary that adds missing keys as IrcOtrAccount instances.""" @@ -600,8 +609,8 @@ def __init__(self, account, peername): def policy_config_option(self, policy): """Get the option name of a policy option for this context.""" return config_prefix('.'.join([ - 'policy', self.peer_server, self.user.nick, self.peer_nick, - policy.lower()])) + 'policy', self.peer_server, self.user.nick, self.peer_nick, + policy.lower()])) def getPolicy(self, key): """Get the value of a policy option for this context.""" @@ -612,20 +621,18 @@ def getPolicy(self, key): elif key_lower == 'send_tag' and self.no_send_tag(): result = False else: - option = weechat.config_get( - PYVER.to_str(self.policy_config_option(key))) + option = config_get(self.policy_config_option(key)) if option == '': - option = weechat.config_get( - PYVER.to_str(self.user.policy_config_option(key))) + option = config_get(self.user.policy_config_option(key)) if option == '': - option = weechat.config_get(config_prefix('.'.join( - ['policy', self.peer_server, key_lower]))) + option = config_get_prefixed('.'.join( + ['policy', self.peer_server, key_lower])) if option == '': - option = weechat.config_get( - config_prefix('policy.default.{}'.format(key_lower))) + option = config_get_prefixed( + 'policy.default.{0}'.format(key_lower)) result = bool(weechat.config_boolean(option)) @@ -640,7 +647,7 @@ def inject(self, msg, appdata=None): else: msg = PYVER.to_unicode(msg) - debug(('inject', msg, 'len {}'.format(len(msg)), appdata)) + debug(('inject', msg, 'len {0}'.format(len(msg)), appdata)) privmsg(self.peer_server, self.peer_nick, msg) @@ -678,7 +685,7 @@ def setState(self, newstate): if trust is None: fpr = str(self.getCurrentKey()) - self.print_buffer('New fingerprint: {}'.format(fpr), 'warning') + self.print_buffer('New fingerprint: {0}'.format(fpr), 'warning') self.setCurrentTrust('') if bool(trust): @@ -705,7 +712,7 @@ def maxMessageSize(self, appdata=None): """Return the max message size for this context.""" # remove 'PRIVMSG :' from max message size result = self.user.maxMessageSize - 10 - len(self.peer_nick) - debug('max message size {}'.format(result)) + debug('max message size {0}'.format(result)) return result @@ -725,14 +732,14 @@ def print_buffer(self, msg, level='info'): # add [nick] prefix if we have only a server buffer for the query if self.peer_nick and not buffer_is_private(buf): msg = '[{nick}] {msg}'.format( - nick=self.peer_nick, - msg=msg) + nick=self.peer_nick, + msg=msg) print_buffer(buf, msg, level) def hint(self, msg): """Print a message to the buffer but only when hints are enabled.""" - hints_option = weechat.config_get(config_prefix('general.hints')) + hints_option = config_get_prefixed('general.hints') if weechat.config_boolean(hints_option): self.print_buffer(msg, 'hint') @@ -774,7 +781,7 @@ def handle_tlvs(self, tlvs): self.print_buffer( """Peer has requested SMP verification: {msg} Respond with: /otr smp respond """.format( - msg=PYVER.to_unicode(smp1q.msg))) + msg=PYVER.to_unicode(smp1q.msg))) elif first_instance(tlvs, potr.proto.SMP2TLV): if not self.in_smp: debug('Received unexpected SMP2') @@ -791,23 +798,24 @@ def handle_tlvs(self, tlvs): if self.smpIsSuccess(): if self.smp_question: - self.smp_finish('SMP verification succeeded.', - 'success') + self.smp_finish( + 'SMP verification succeeded.', 'success') if not self.is_verified: self.print_buffer( - """You may want to authenticate your peer by asking your own question: -/otr smp ask <'question'> 'secret'""") - + 'You may want to authenticate your peer by ' + 'asking your own question:\n' + "/otr smp ask <'question'> 'secret'") else: - self.smp_finish('SMP verification succeeded.', - 'success') + self.smp_finish( + 'SMP verification succeeded.', 'success') else: self.smp_finish('SMP verification failed.', 'error') def verify_instructions(self): """Generate verification instructions for user.""" - return """You can verify that this contact is who they claim to be in one of the following ways: + return """You can verify that this contact is who they claim to be in +one of the following ways: 1) Verify each other's fingerprints using a secure channel: Your fingerprint : {your_fp} @@ -824,13 +832,11 @@ def verify_instructions(self): executing these commands from the appropriate conversation buffer """.format( - your_fp=self.user.getPrivkey(), - peer=self.peer, - peer_nick=self.peer_nick, - peer_server=self.peer_server, - peer_fp=potr.human_hash( - self.crypto.theirPubkey.cfingerprint()), - ) + your_fp=self.user.getPrivkey(), + peer=self.peer, + peer_nick=self.peer_nick, + peer_server=self.peer_server, + peer_fp=potr.human_hash(self.crypto.theirPubkey.cfingerprint())) def is_encrypted(self): """Return True if the conversation with this context's peer is @@ -851,9 +857,9 @@ def format_policies(self): for policy, desc in sorted(POLICIES.items()): buf.write(' {policy} ({desc}) : {value}\n'.format( - policy=policy, - desc=desc, - value='on' if self.getPolicy(policy) else 'off')) + policy=policy, + desc=desc, + value='on' if self.getPolicy(policy) else 'off')) buf.write('Change policies with: /otr policy NAME on|off') @@ -884,7 +890,7 @@ def get_log_level(self): buf = self.buffer() - if not weechat.config_get(self.get_logger_option_name(buf)): + if not config_get(self.get_logger_option_name()): result = -1 else: result = 0 @@ -898,8 +904,9 @@ def get_log_level(self): return result - def get_logger_option_name(self, buf): - """Returns the logger config option for the specified buffer.""" + def get_logger_option_name(self): + """Returns the logger config option for this context's buffer.""" + buf = self.buffer() name = buffer_get_string(buf, 'name') plugin = buffer_get_string(buf, 'plugin') @@ -917,7 +924,8 @@ def disable_logging(self): if self.is_logged(): weechat.command(self.buffer(), '/mute logger disable') self.print_buffer( - 'Logs have been temporarily disabled for the session. They will be restored upon finishing the OTR session.') + 'Logs have been temporarily disabled for the session. ' + 'They will be restored upon finishing the OTR session.') return previous_log_level @@ -934,16 +942,16 @@ def restore_logging(self, previous_log_level): if (previous_log_level >= 0) and (previous_log_level < 10): self.print_buffer( - 'Restoring buffer logging value to: {}'.format( + 'Restoring buffer logging value to: {0}'.format( previous_log_level), 'warning') - weechat.command(buf, '/mute logger set {}'.format( + weechat.command(buf, '/mute logger set {0}'.format( previous_log_level)) if previous_log_level == -1: - logger_option_name = self.get_logger_option_name(buf) + logger_option_name = self.get_logger_option_name() self.print_buffer( 'Restoring buffer logging value to default', 'warning') - weechat.command(buf, '/mute unset {}'.format( + weechat.command(buf, '/mute unset {0}'.format( logger_option_name)) del self.previous_log_level @@ -990,11 +998,11 @@ def no_send_tag(self): debug(('no_send_tag', no_send_tag_regex, self.peer_nick)) if no_send_tag_regex: return re.match(no_send_tag_regex, self.peer_nick, re.IGNORECASE) - + def __repr__(self): - return PYVER.to_str(('<{} {:x} peer_nick={c.peer_nick} ' - 'peer_server={c.peer_server}>').format( - self.__class__.__name__, id(self), c=self)) + return PYVER.to_str(( + '<{0} {1:x} peer_nick={c.peer_nick} peer_server={c.peer_server}>' + ).format(self.__class__.__name__, id(self), c=self)) class IrcOtrAccount(potr.context.Account): """Account class for OTR over IRC.""" @@ -1015,7 +1023,7 @@ def __init__(self, name): self.defaultQuery = self.defaultQuery.replace("\n", ' ') self.key_file_path = private_key_file_path(name) - self.fpr_file_path = os.path.join(OTR_DIR, '{}.fpr'.format(name)) + self.fpr_file_path = os.path.join(OTR_DIR, '{0}.fpr'.format(name)) self.load_trusts() @@ -1067,8 +1075,7 @@ def saveTrusts(self): debug(('trust write', uid, self.name, IrcOtrAccount.PROTOCOL, fpr, trust)) fpr_file.write(PYVER.to_str('\t'.join( - (uid, self.name, IrcOtrAccount.PROTOCOL, fpr, - trust)))) + (uid, self.name, IrcOtrAccount.PROTOCOL, fpr, trust)))) fpr_file.write('\n') def end_all_private(self): @@ -1080,7 +1087,7 @@ def end_all_private(self): def policy_config_option(self, policy): """Get the option name of a policy option for this account.""" return config_prefix('.'.join([ - 'policy', self.server, self.nick, policy.lower()])) + 'policy', self.server, self.nick, policy.lower()])) class IrcHTMLParser(PYVER.html_parser.HTMLParser): """A simple HTML parser that throws away anything but newlines and links""" @@ -1118,7 +1125,7 @@ def handle_endtag(self, tag): if self.result[self.linkstart:] == self.linktarget: self.result += ']' else: - self.result += ']({})'.format(self.linktarget) + self.result += ']({0})'.format(self.linktarget) self.linktarget = '' def handle_data(self, data): @@ -1131,7 +1138,7 @@ def handle_entityref(self, name): self.result += PYVER.unichr( PYVER.html_entities.name2codepoint[name]) except KeyError: - self.result += '&{};'.format(name) + self.result += '&{0};'.format(name) def handle_charref(self, name): """Called for character references, such as '""" @@ -1141,7 +1148,7 @@ def handle_charref(self, name): else: self.result += PYVER.unichr(int(name)) except ValueError: - self.result += '&#{};'.format(name) + self.result += '&#{0};'.format(name) class TableFormatter(object): """Format lists of string into aligned tables.""" @@ -1153,7 +1160,7 @@ def __init__(self): def add_row(self, row): """Add a row to the table.""" self.rows.append(row) - row_widths = [ len(s) for s in row ] + row_widths = [len(s) for s in row] if self.max_widths is None: self.max_widths = row_widths else: @@ -1161,12 +1168,12 @@ def add_row(self, row): def format(self): """Return the formatted table as a string.""" - return '\n'.join([ self.format_row(row) for row in self.rows ]) + return '\n'.join([self.format_row(row) for row in self.rows]) def format_row(self, row): """Format a single row as a string.""" return ' |'.join( - [ s.ljust(self.max_widths[i]) for i, s in enumerate(row) ]) + [s.ljust(self.max_widths[i]) for i, s in enumerate(row)]) def message_in_cb(data, modifier, modifier_data, string): """Incoming message callback""" @@ -1182,10 +1189,7 @@ def message_in_cb(data, modifier, modifier_data, string): server = PYVER.to_unicode(modifier_data) - from_user = irc_user(parsed['from_nick'], server) - local_user = current_user(server) - - context = ACCOUNTS[local_user].getContext(from_user) + context = get_server_context(server, parsed['from_nick']) context.in_assembler.add(parsed['text']) @@ -1206,8 +1210,8 @@ def message_in_cb(data, modifier, modifier_data, string): context.handle_tlvs(tlvs) except potr.context.ErrorReceived as err: - context.print_buffer('Received OTR error: {}'.format( - PYVER.to_unicode(err.args[0].error)), 'error') + context.print_buffer('Received OTR error: {0}'.format( + PYVER.to_unicode(err.args[0])), 'error') except potr.context.NotEncryptedError: context.print_buffer( 'Received encrypted data but no private session established.', @@ -1245,10 +1249,7 @@ def message_out_cb(data, modifier, modifier_data, string): server = PYVER.to_unicode(modifier_data) - to_user = irc_user(parsed['to_nick'], server) - local_user = current_user(server) - - context = ACCOUNTS[local_user].getContext(to_user) + context = get_server_context(server, parsed['to_nick']) is_query = OTR_QUERY_RE.search(parsed['text']) parsed_text_bytes = to_bytes(parsed['text']) @@ -1277,12 +1278,12 @@ def message_out_cb(data, modifier, modifier_data, string): not is_query and \ context.getPolicy('require_encryption'): context.print_buffer( - 'Your message will not be sent, because policy requires an ' - 'encrypted connection.', 'error') + 'Your message will not be sent, because policy requires an ' + 'encrypted connection.', 'error') context.hint( - 'Wait for the OTR connection or change the policy to allow ' - 'clear-text messages:\n' - '/policy set require_encryption off') + 'Wait for the OTR connection or change the policy to allow ' + 'clear-text messages:\n' + '/otr policy require_encryption off') try: ret = context.sendMessage( @@ -1299,12 +1300,14 @@ def message_out_cb(data, modifier, modifier_data, string): except potr.context.NotEncryptedError as err: if err.args[0] == potr.context.EXC_FINISHED: context.print_buffer( - """Your message was not sent. End your private conversation:\n/otr finish""", + 'Your message was not sent. End your private ' + 'conversation:\n/otr finish', 'error') else: raise weechat.bar_item_update(SCRIPT_NAME) + # pylint: disable=bare-except except: try: print_buffer('', traceback.format_exc(), 'error') @@ -1313,6 +1316,7 @@ def message_out_cb(data, modifier, modifier_data, string): context.print_buffer( 'Failed to send message. See core buffer for traceback.', 'error') + # pylint: disable=bare-except except: pass @@ -1337,18 +1341,13 @@ def command_cb(data, buf, args): """Parse and dispatch WeeChat OTR commands.""" result = weechat.WEECHAT_RC_ERROR - try: - arg_parts = [PYVER.to_unicode(arg) for arg in shlex.split(args)] - except: - debug("Command parsing error.") - return result + arg_parts = [PYVER.to_unicode(arg) for arg in shlex.split(args)] if len(arg_parts) in (1, 3) and arg_parts[0] in ('start', 'refresh'): nick, server = default_peer_args(arg_parts[1:3], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) # We need to wall disable_logging() here so that no OTR-related # buffer messages get logged at any point. disable_logging() will # be called again when effectively switching to encrypted, but @@ -1359,11 +1358,13 @@ def command_cb(data, buf, args): else: context.previous_log_level = context.get_log_level() - context.hint('Sending OTR query... Please await confirmation of the OTR session being started before sending a message.') + context.hint( + 'Sending OTR query... Please await confirmation of the OTR ' + 'session being started before sending a message.') if not context.getPolicy('send_tag'): context.hint( - 'To try OTR on all conversations with {peer}: /otr policy send_tag on'.format( - peer=context.peer)) + 'To try OTR on all conversations with {peer}: /otr ' + 'policy send_tag on'.format(peer=context.peer)) privmsg(server, nick, '?OTR?') @@ -1372,8 +1373,7 @@ def command_cb(data, buf, args): nick, server = default_peer_args(arg_parts[1:3], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) context.disconnect() result = weechat.WEECHAT_RC_OK @@ -1382,14 +1382,13 @@ def command_cb(data, buf, args): nick, server = default_peer_args(arg_parts[1:3], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if context.is_encrypted(): context.print_buffer( 'This conversation is encrypted.', 'success') - context.print_buffer("Your fingerprint is: {}".format( + context.print_buffer("Your fingerprint is: {0}".format( context.user.getPrivkey())) - context.print_buffer("Your peer's fingerprint is: {}".format( + context.print_buffer("Your peer's fingerprint is: {0}".format( potr.human_hash(context.crypto.theirPubkey.cfingerprint()))) if context.is_verified(): context.print_buffer( @@ -1423,8 +1422,7 @@ def command_cb(data, buf, args): if secret: secret = PYVER.to_str(secret) - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) context.smpGotSecret(secret) result = weechat.WEECHAT_RC_OK @@ -1455,8 +1453,7 @@ def command_cb(data, buf, args): else: return weechat.WEECHAT_RC_ERROR - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if secret: secret = PYVER.to_str(secret) @@ -1467,7 +1464,7 @@ def command_cb(data, buf, args): context.smpInit(secret, question) except potr.context.NotEncryptedError: context.print_buffer( - 'There is currently no encrypted session with {}.'.format( + 'There is currently no encrypted session with {0}.'.format( context.peer), 'error') else: if question: @@ -1487,16 +1484,15 @@ def command_cb(data, buf, args): else: return weechat.WEECHAT_RC_ERROR - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if context.in_smp: try: context.smpAbort() except potr.context.NotEncryptedError: context.print_buffer( - 'There is currently no encrypted session with {}.'.format( - context.peer), 'error') + 'There is currently no encrypted session with {0}.' + .format(context.peer), 'error') else: debug('SMP aborted') context.smp_finish('SMP aborted.') @@ -1506,8 +1502,7 @@ def command_cb(data, buf, args): nick, server = default_peer_args(arg_parts[1:3], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if context.crypto.theirPubkey is not None: context.setCurrentTrust('verified') @@ -1517,16 +1512,16 @@ def command_cb(data, buf, args): weechat.bar_item_update(SCRIPT_NAME) else: context.print_buffer( - 'No fingerprint for {peer}. Start an OTR conversation first: /otr start'.format( - peer=context.peer), 'error') + 'No fingerprint for {peer}. Start an OTR conversation ' + 'first: /otr start'.format(peer=context.peer), + 'error') result = weechat.WEECHAT_RC_OK elif len(arg_parts) in (1, 3) and arg_parts[0] == 'distrust': nick, server = default_peer_args(arg_parts[1:3], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if context.crypto.theirPubkey is not None: context.setCurrentTrust('') @@ -1537,8 +1532,8 @@ def command_cb(data, buf, args): weechat.bar_item_update(SCRIPT_NAME) else: context.print_buffer( - 'No fingerprint for {peer}. Start an OTR conversation first: /otr start'.format( - peer=context.peer), 'error') + 'No fingerprint for {peer}. Start an OTR conversation ' + 'first: /otr start'.format(peer=context.peer), 'error') result = weechat.WEECHAT_RC_OK @@ -1546,8 +1541,7 @@ def command_cb(data, buf, args): nick, server = default_peer_args([], buf) if len(arg_parts) == 1: if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if context.is_encrypted(): if context.is_logged(): @@ -1571,15 +1565,20 @@ def command_cb(data, buf, args): if len(arg_parts) == 2: if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if arg_parts[1] == 'start' and \ not context.is_logged() and \ context.is_encrypted(): if context.previous_log_level is None: context.previous_log_level = context.get_log_level() - context.print_buffer('From this point on, this conversation will be logged. Please keep in mind that by doing so you are potentially putting yourself and your interlocutor at risk. You can disable this by doing /otr log stop', 'warning') + context.print_buffer( + 'From this point on, this conversation will be ' + 'logged. Please keep in mind that by doing so you ' + 'are potentially putting yourself and your ' + 'interlocutor at risk. You can disable this by doing ' + '/otr log stop', + 'warning') weechat.command(buf, '/mute logger set 9') result = weechat.WEECHAT_RC_OK @@ -1589,7 +1588,9 @@ def command_cb(data, buf, args): if context.previous_log_level is None: context.previous_log_level = context.get_log_level() weechat.command(buf, '/mute logger set 0') - context.print_buffer('From this point on, this conversation will NOT be logged ANYMORE.') + context.print_buffer( + 'From this point on, this conversation will NOT be ' + 'logged ANYMORE.') result = weechat.WEECHAT_RC_OK elif not context.is_encrypted(): @@ -1609,8 +1610,7 @@ def command_cb(data, buf, args): nick, server = default_peer_args([], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) context.print_buffer(context.format_policies()) else: @@ -1622,8 +1622,7 @@ def command_cb(data, buf, args): nick, server = default_peer_args([], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) context.print_buffer(format_default_policies()) else: @@ -1635,8 +1634,7 @@ def command_cb(data, buf, args): nick, server = default_peer_args([], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) policy_var = context.policy_config_option(arg_parts[1].lower()) @@ -1660,8 +1658,7 @@ def command_cb(data, buf, args): value=arg_parts[3])) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) context.print_buffer(format_default_policies()) else: @@ -1690,72 +1687,87 @@ def otr_statusbar_cb(data, item, window): # will be empty. buf = weechat.current_buffer() - result = '' + if not buffer_is_private(buf): + return '' - if buffer_is_private(buf): - local_user = irc_user( - buffer_get_string(buf, 'localvar_nick'), - buffer_get_string(buf, 'localvar_server')) + local_user = irc_user( + buffer_get_string(buf, 'localvar_nick'), + buffer_get_string(buf, 'localvar_server')) - remote_user = irc_user( - buffer_get_string(buf, 'localvar_channel'), - buffer_get_string(buf, 'localvar_server')) + remote_user = irc_user( + buffer_get_string(buf, 'localvar_channel'), + buffer_get_string(buf, 'localvar_server')) - context = ACCOUNTS[local_user].getContext(remote_user) + context = get_context(local_user, remote_user) - encrypted_str = config_string('look.bar.state.encrypted') - unencrypted_str = config_string('look.bar.state.unencrypted') - authenticated_str = config_string('look.bar.state.authenticated') - unauthenticated_str = config_string('look.bar.state.unauthenticated') - logged_str = config_string('look.bar.state.logged') - notlogged_str = config_string('look.bar.state.notlogged') + encrypted_str = config_string('look.bar.state.encrypted') + unencrypted_str = config_string('look.bar.state.unencrypted') + authenticated_str = config_string('look.bar.state.authenticated') + unauthenticated_str = config_string('look.bar.state.unauthenticated') + logged_str = config_string('look.bar.state.logged') + notlogged_str = config_string('look.bar.state.notlogged') - bar_parts = [] + bar_parts = [] - if context.is_encrypted(): - if encrypted_str: - bar_parts.append(''.join([ - config_color('status.encrypted'), - encrypted_str, - config_color('status.default')])) - - if context.is_verified(): - if authenticated_str: - bar_parts.append(''.join([ - config_color('status.authenticated'), - authenticated_str, - config_color('status.default')])) - elif unauthenticated_str: - bar_parts.append(''.join([ - config_color('status.unauthenticated'), - unauthenticated_str, - config_color('status.default')])) - - if context.is_logged(): - if logged_str: - bar_parts.append(''.join([ - config_color('status.logged'), - logged_str, - config_color('status.default')])) - elif notlogged_str: + if context.is_encrypted(): + if encrypted_str: + bar_parts.append(''.join([ + config_color('status.encrypted'), + encrypted_str, + config_color('status.default')])) + + if context.is_verified(): + if authenticated_str: bar_parts.append(''.join([ - config_color('status.notlogged'), - notlogged_str, - config_color('status.default')])) + config_color('status.authenticated'), + authenticated_str, + config_color('status.default')])) + elif unauthenticated_str: + bar_parts.append(''.join([ + config_color('status.unauthenticated'), + unauthenticated_str, + config_color('status.default')])) - elif unencrypted_str: + if context.is_logged(): + if logged_str: + bar_parts.append(''.join([ + config_color('status.logged'), + logged_str, + config_color('status.default')])) + elif notlogged_str: bar_parts.append(''.join([ - config_color('status.unencrypted'), - unencrypted_str, - config_color('status.default')])) + config_color('status.notlogged'), + notlogged_str, + config_color('status.default')])) + + elif unencrypted_str: + bar_parts.append(''.join([ + config_color('status.unencrypted'), + unencrypted_str, + config_color('status.default')])) + + result = config_string('look.bar.state.separator').join(bar_parts) + + if result: + result = '{color}{prefix}{result}'.format( + color=config_color('status.default'), + prefix=config_string('look.bar.prefix'), + result=result) + + if context.is_encrypted(): + weechat.buffer_set(buf, 'localvar_set_otr_encrypted', 'true') + else: + weechat.buffer_set(buf, 'localvar_set_otr_encrypted', 'false') - result = config_string('look.bar.state.separator').join(bar_parts) + if context.is_verified(): + weechat.buffer_set(buf, 'localvar_set_otr_authenticated', 'true') + else: + weechat.buffer_set(buf, 'localvar_set_otr_authenticated', 'false') - if result: - result = '{color}{prefix}{result}'.format( - color=config_color('status.default'), - prefix=config_string('look.bar.prefix'), - result=result) + if context.is_logged(): + weechat.buffer_set(buf, 'localvar_set_otr_logged', 'true') + else: + weechat.buffer_set(buf, 'localvar_set_otr_logged', 'false') return result @@ -1806,8 +1818,7 @@ def buffer_closing_cb(data, signal, signal_data): nick, server = default_peer_args([], signal_data) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) context.disconnect() result = weechat.WEECHAT_RC_OK @@ -1816,7 +1827,7 @@ def buffer_closing_cb(data, signal, signal_data): def init_config(): """Set up configuration options and load config file.""" global CONFIG_FILE - CONFIG_FILE = weechat.config_new(SCRIPT_NAME, 'config_reload_cb', '') + CONFIG_FILE = weechat.config_new(SCRIPT_NAME, '', '') global CONFIG_SECTIONS CONFIG_SECTIONS = {} @@ -1825,14 +1836,24 @@ def init_config(): CONFIG_FILE, 'general', 0, 0, '', '', '', '', '', '', '', '', '', '') for option, typ, desc, default in [ - ('debug', 'boolean', 'OTR script debugging', 'off'), - ('hints', 'boolean', 'Give helpful hints how to use this script and how to stay secure while using OTR (recommended)', 'on'), - ('defaultkey', 'string', - 'default private key to use for new accounts (nick@server)', ''), - ('no_send_tag_regex', 'string', - 'do not OTR whitespace tag messages to nicks matching this regex ' - '(case insensitive)', - '^(alis|chanfix|global|.+serv|\*.+)$'), + ('debug', + 'boolean', + 'OTR script debugging', + 'off'), + ('hints', + 'boolean', + 'Give helpful hints how to use this script and how to stay ' + 'secure while using OTR (recommended)', + 'on'), + ('defaultkey', + 'string', + 'default private key to use for new accounts (nick@server)', + ''), + ('no_send_tag_regex', + 'string', + 'do not OTR whitespace tag messages to nicks matching this regex ' + '(case insensitive)', + '^(alis|chanfix|global|.+serv|\*.+)$'), ]: weechat.config_new_option( CONFIG_FILE, CONFIG_SECTIONS['general'], option, typ, desc, '', 0, @@ -1842,25 +1863,54 @@ def init_config(): CONFIG_FILE, 'color', 0, 0, '', '', '', '', '', '', '', '', '', '') for option, desc, default, update_cb in [ - ('status.default', 'status bar default color', 'default', - 'bar_config_update_cb'), - ('status.encrypted', 'status bar encrypted indicator color', 'green', - 'bar_config_update_cb'), - ('status.unencrypted', 'status bar unencrypted indicator color', - 'lightred', 'bar_config_update_cb'), - ('status.authenticated', 'status bar authenticated indicator color', - 'green', 'bar_config_update_cb'), - ('status.unauthenticated', 'status bar unauthenticated indicator color', - 'lightred', 'bar_config_update_cb'), - ('status.logged', 'status bar logged indicator color', 'lightred', - 'bar_config_update_cb'), - ('status.notlogged', 'status bar not logged indicator color', - 'green', 'bar_config_update_cb'), - ('buffer.hint', 'text color for hints', 'lightblue', ''), - ('buffer.info', 'text color for informational messages', 'default', ''), - ('buffer.success', 'text color for success messages', 'lightgreen', ''), - ('buffer.warning', 'text color for warnings', 'yellow', ''), - ('buffer.error', 'text color for errors', 'lightred', ''), + ('status.default', + 'status bar default color', + 'default', + 'bar_config_update_cb'), + ('status.encrypted', + 'status bar encrypted indicator color', + 'green', + 'bar_config_update_cb'), + ('status.unencrypted', + 'status bar unencrypted indicator color', + 'lightred', + 'bar_config_update_cb'), + ('status.authenticated', + 'status bar authenticated indicator color', + 'green', + 'bar_config_update_cb'), + ('status.unauthenticated', + 'status bar unauthenticated indicator color', + 'lightred', + 'bar_config_update_cb'), + ('status.logged', + 'status bar logged indicator color', + 'lightred', + 'bar_config_update_cb'), + ('status.notlogged', + 'status bar not logged indicator color', + 'green', + 'bar_config_update_cb'), + ('buffer.hint', + 'text color for hints', + 'lightblue', + ''), + ('buffer.info', + 'text color for informational messages', + 'default', + ''), + ('buffer.success', + 'text color for success messages', + 'lightgreen', + ''), + ('buffer.warning', + 'text color for warnings', + 'yellow', + ''), + ('buffer.error', + 'text color for errors', + 'lightred', + ''), ]: weechat.config_new_option( CONFIG_FILE, CONFIG_SECTIONS['color'], option, 'color', desc, '', 0, @@ -1870,32 +1920,45 @@ def init_config(): CONFIG_FILE, 'look', 0, 0, '', '', '', '', '', '', '', '', '', '') for option, desc, default, update_cb in [ - ('bar.prefix', 'prefix for OTR status bar item', 'OTR:', - 'bar_config_update_cb'), - ('bar.state.encrypted', - 'shown in status bar when conversation is encrypted', 'SEC', - 'bar_config_update_cb'), - ('bar.state.unencrypted', - 'shown in status bar when conversation is not encrypted', '!SEC', - 'bar_config_update_cb'), - ('bar.state.authenticated', - 'shown in status bar when peer is authenticated', 'AUTH', - 'bar_config_update_cb'), - ('bar.state.unauthenticated', - 'shown in status bar when peer is not authenticated', '!AUTH', - 'bar_config_update_cb'), - ('bar.state.logged', - 'shown in status bar when peer conversation is being logged to disk', - 'LOG', - 'bar_config_update_cb'), - ('bar.state.notlogged', - 'shown in status bar when peer conversation is not being logged to disk', - '!LOG', - 'bar_config_update_cb'), - ('bar.state.separator', 'separator for states in the status bar', ',', - 'bar_config_update_cb'), - ('prefix', 'prefix used for messages from otr (note: content is evaluated, see /help eval)', - '${color:default}:! ${color:brown}otr${color:default} !:', ''), + ('bar.prefix', + 'prefix for OTR status bar item', + 'OTR:', + 'bar_config_update_cb'), + ('bar.state.encrypted', + 'shown in status bar when conversation is encrypted', + 'SEC', + 'bar_config_update_cb'), + ('bar.state.unencrypted', + 'shown in status bar when conversation is not encrypted', + '!SEC', + 'bar_config_update_cb'), + ('bar.state.authenticated', + 'shown in status bar when peer is authenticated', + 'AUTH', + 'bar_config_update_cb'), + ('bar.state.unauthenticated', + 'shown in status bar when peer is not authenticated', + '!AUTH', + 'bar_config_update_cb'), + ('bar.state.logged', + 'shown in status bar when peer conversation is being logged to ' + 'disk', + 'LOG', + 'bar_config_update_cb'), + ('bar.state.notlogged', + 'shown in status bar when peer conversation is not being logged ' + 'to disk', + '!LOG', + 'bar_config_update_cb'), + ('bar.state.separator', + 'separator for states in the status bar', + ',', + 'bar_config_update_cb'), + ('prefix', + 'prefix used for messages from otr (note: content is evaluated, ' + 'see /help eval)', + '${color:default}:! ${color:brown}otr${color:default} !:', + ''), ]: weechat.config_new_option( CONFIG_FILE, CONFIG_SECTIONS['look'], option, 'string', desc, '', @@ -1906,13 +1969,24 @@ def init_config(): 'policy_create_option_cb', '', '', '') for option, desc, default in [ - ('default.allow_v2', 'default allow OTR v2 policy', 'on'), - ('default.require_encryption', 'default require encryption policy', - 'off'), - ('default.log', 'default enable logging to disk', 'off'), - ('default.send_tag', 'default send tag policy', 'off'), - ('default.html_escape', 'default HTML escape policy', 'off'), - ('default.html_filter', 'default HTML filter policy', 'on'), + ('default.allow_v2', + 'default allow OTR v2 policy', + 'on'), + ('default.require_encryption', + 'default require encryption policy', + 'off'), + ('default.log', + 'default enable logging to disk', + 'off'), + ('default.send_tag', + 'default send tag policy', + 'off'), + ('default.html_escape', + 'default HTML escape policy', + 'off'), + ('default.html_filter', + 'default HTML filter policy', + 'on'), ]: weechat.config_new_option( CONFIG_FILE, CONFIG_SECTIONS['policy'], option, 'boolean', desc, '', @@ -1920,13 +1994,6 @@ def init_config(): weechat.config_read(CONFIG_FILE) -def config_reload_cb(data, config_file): - """/reload callback to reload config from file.""" - free_all_config() - init_config() - - return weechat.WEECHAT_CONFIG_READ_OK - def free_all_config(): """Free all config options, sections and config file.""" for section in CONFIG_SECTIONS.values(): @@ -1939,7 +2006,7 @@ def create_dir(): """Create the OTR subdirectory in the WeeChat config directory if it does not exist.""" if not os.path.exists(OTR_DIR): - weechat.mkdir_home(OTR_DIR_NAME, 0o700) + weechat.mkdir_parents(OTR_DIR, 0o700) def git_info(): """If this script is part of a git repository return the repo state.""" @@ -1949,13 +2016,19 @@ def git_info(): if os.path.isdir(git_dir): import subprocess try: - result = PYVER.to_unicode(subprocess.check_output([ + # We can't use check_output here without breaking compatibility + # for Python 2.6, but we ignore the return value anyway, so Popen + # is only slightly more complicated: + process = subprocess.Popen([ 'git', '--git-dir', git_dir, '--work-tree', script_dir, 'describe', '--dirty', '--always', - ])).lstrip('v').rstrip() - except (OSError, subprocess.CalledProcessError): + ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output = process.communicate()[0] + if output: + result = PYVER.to_unicode(output).lstrip('v').rstrip() + except OSError: pass return result @@ -1971,46 +2044,50 @@ def weechat_version_ok(): error_message = ( '{script_name} requires WeeChat version >= 0.4.2. The current ' 'version is {current_version}.').format( - script_name=SCRIPT_NAME, - current_version=weechat.info_get('version', '')) + script_name=SCRIPT_NAME, + current_version=weechat.info_get('version', '')) prnt('', error_message) return False - else: - return True + return True SCRIPT_VERSION = git_info() or SCRIPT_VERSION def dependency_versions(): """Return a string containing the versions of all dependencies.""" return ('weechat-otr {script_version}, ' - 'potr {potr_major}.{potr_minor}.{potr_patch}-{potr_sub}, ' - 'Python {python_version}, ' - 'WeeChat {weechat_version}' - ).format( - script_version=SCRIPT_VERSION, - potr_major=potr.VERSION[0], - potr_minor=potr.VERSION[1], - potr_patch=potr.VERSION[2], - potr_sub=potr.VERSION[3], - python_version=platform.python_version(), - weechat_version=weechat.info_get('version', '')) - -def excepthook(typ, value, traceback): + 'potr {potr_major}.{potr_minor}.{potr_patch}-{potr_sub}, ' + 'Python {python_version}, ' + 'WeeChat {weechat_version}' + ).format( + script_version=SCRIPT_VERSION, + potr_major=potr.VERSION[0], + potr_minor=potr.VERSION[1], + potr_patch=potr.VERSION[2], + potr_sub=potr.VERSION[3], + python_version=platform.python_version(), + weechat_version=weechat.info_get('version', '')) + +def excepthook(typ, value, tracebak): + """Add dependency versions to tracebacks.""" sys.stderr.write('Versions: ') sys.stderr.write(dependency_versions()) sys.stderr.write('\n') - sys.__excepthook__(typ, value, traceback) + sys.__excepthook__(typ, value, tracebak) sys.excepthook = excepthook if weechat.register( - SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENCE, SCRIPT_DESC, - 'shutdown', ''): + SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENCE, SCRIPT_DESC, + 'shutdown', ''): if weechat_version_ok(): init_config() - OTR_DIR = os.path.join(info_get('weechat_dir', ''), OTR_DIR_NAME) + options = { + 'directory': 'data', + } + OTR_DIR = weechat.string_eval_path_home('%%h/%s' % OTR_DIR_NAME, + {}, {}, options) create_dir() ACCOUNTS = AccountDict() @@ -2030,7 +2107,7 @@ def excepthook(typ, value, traceback): 'smp abort [NICK SERVER] || ' 'trust [NICK SERVER] || ' 'distrust [NICK SERVER] || ' - 'log [on|off] || ' + 'log [start|stop] || ' 'policy [POLICY on|off] || ' 'fingerprint [SEARCH|all]', '', @@ -2043,7 +2120,7 @@ def excepthook(typ, value, traceback): 'smp abort %(nick) %(irc_servers) %-||' 'trust %(nick) %(irc_servers) %-||' 'distrust %(nick) %(irc_servers) %-||' - 'log on|off %-||' + 'log start|stop %-||' 'policy %(otr_policy) on|off %-||' 'fingerprint all %-||', 'command_cb', diff --git a/python/pagetitle.py b/python/pagetitle.py index ea5d59db..7eb47106 100644 --- a/python/pagetitle.py +++ b/python/pagetitle.py @@ -1,101 +1,103 @@ # pagetitle plugin for weechat-0.3.0 # -# /pt http://tech.slashdot.org/tech/08/11/12/199215.shtml +# /pagetitle http://tech.slashdot.org/tech/08/11/12/199215.shtml # http://tech.slashdot.org/tech/08/11/12/199215.shtml -# ('Slashdot | Microsoft's "Dead Cow" Patch Was 7 Years In the Making') +# ('Slashdot | Microsoft's "Dead Cow" Patch Was 7 Years In the Making') # # xororand @ irc://irc.freenode.net/#weechat # -# 2009-05-02, FlashCode : +# 2021-06-05, Sébastien Helleu : +# version 0.6: make script compatible with Python 3, +# rename command /pt to /pagetitle, fix PEP8 errors +# 2009-05-02, Sébastien Helleu : # version 0.5: sync with last API changes -import htmllib -import re -import socket -import sys -import urllib2 - -limit_title_length = 100 -debug = True +from html import unescape +from urllib.error import URLError +from urllib.request import Request, urlopen -# user agent -opener = urllib2.build_opener() -opener.addheaders = [('User-agent', 'Mozilla/5.0 (weechat/pagetitle)')] -urllib2._urlopener = opener +import re +import weechat -# set a short timeout to avoid freezing weechat [seconds] -socket.setdefaulttimeout(5) +MAX_TITLE_LENGTH = 100 regex_url = re.compile("""https?://[^ ]+""") -def unescape(s): #{{{ - """Unescape HTML entities""" - p = htmllib.HTMLParser(None) - p.save_bgn() - p.feed(s) - return p.save_end() #}}} - -def getPageTitle(url): - """Retrieve the HTML from a webpage""" - - try: - u = urllib2.urlopen(url) - except urllib2.HTTPError, e: - raise NameError(str(e)) - except urllib2.URLError, e: - raise NameError(str(e)) - - info = u.info() - try: - content_type = info['Content-Type'] - if not re.match(".*/html.*",content_type): - return "" - except: - return "" - - head = u.read(8192) - head = re.sub("[\r\n\t ]"," ",head) - - title = re.search('(?i)\<title\>(.*?)\</title\>', head) - if title: - title = title.group(1) - return unescape(title) - else: - return "" - -# /pt http://foo -def on_pagetitle(data, buffer, args): - if len(args) == 0: - return weechat.WEECHAT_RC_ERROR - - msg = args - - def urlReplace(match): - url = match.group() - try: - if debug: - weechat.prnt(buffer, "pagetitle: retrieving '%s'" % url) - title = getPageTitle(url) - if len(title) > limit_title_length: - title = "%s [...]" % title[0:limit_title_length] - url = "%s ('%s')" % (url, title) - except NameError, e: - weechat.prnt(buffer, "pagetitle: URL: '%s', Error: '%s'" % (url, e)) - return url - - msg = regex_url.sub(urlReplace, msg) - weechat.command(buffer, "/say %s" % msg) - - return weechat.WEECHAT_RC_OK - -# Register plugin -import weechat -weechat.register ('pagetitle', 'xororand', '0.5', 'GPL3', """Adds HTML titles to http:// urls in your message.""", "", "") -desc = """Sends a message to the current buffer and adds HTML titles to http:// URLs. -Example: /pt check this out: http://xkcd.com/364/ -<you> check this out: http://xkcd.com/364/ (xkcd - A webcomic of romance, sarcasm, math and language)""" -weechat.hook_command ('pt', desc, 'message', 'message containing an URL', '', 'on_pagetitle', '') +def get_page_title(url): + """Retrieve the HTML <title> from a webpage.""" + req = Request( + url, + headers={ + "User-agent": "Mozilla/5.0 (weechat/pagetitle)", + }, + ) + try: + head = urlopen(req).read(8192).decode("utf-8", errors="ignore") + except URLError: + return "" + match = re.search("(?i)<title>(.*?)", head) + return unescape(match.group(1)) if match else "" + + +def add_page_titles(data): + """Add page titles for all URLs of a message.""" + buffer, msg = data.split(";", 1) + + def url_replace(match): + url = match.group() + title = get_page_title(url) + if len(title) > MAX_TITLE_LENGTH: + title = "%s [...]" % title[0:MAX_TITLE_LENGTH] + url = "%s ('%s')" % (url, title) + return url + + msg = regex_url.sub(url_replace, msg) + return f"{buffer};{msg}" + + +def process_cb(data, command, rc, stdout, stderr): + """Process callback.""" + buffer, msg = stdout.split(";", 1) + weechat.command(buffer, "/say %s" % msg) + return weechat.WEECHAT_RC_OK + + +# /pagetitle http://foo +def cmd_pagetitle_cb(data, buffer, args): + if len(args) == 0: + return weechat.WEECHAT_RC_ERROR + weechat.hook_process( + "func:add_page_titles", + 30 * 1000, + "process_cb", + f"{buffer};{args}", + ) + return weechat.WEECHAT_RC_OK + + +weechat.register( + "pagetitle", + "xororand", + "0.6", + "GPL3", + """Adds HTML titles to http:// urls in your message.""", + "", + "", +) +desc = """\ +Sends a message to the current buffer and adds HTML titles to http:// URLs. +Example: /pagetitle check this out: http://xkcd.com/364/ + check this out: http://xkcd.com/364/ (xkcd - A webcomic of romance, \ +sarcasm, math and language)""" +weechat.hook_command( + "pagetitle", + desc, + "message", + "message with URL(s)", + "", + "cmd_pagetitle_cb", + "", +) # vim:set ts=4 sw=4 noexpandtab nowrap foldmethod=marker: - diff --git a/python/postpone.py b/python/postpone.py index b5245374..43525c92 100644 --- a/python/postpone.py +++ b/python/postpone.py @@ -20,6 +20,10 @@ # (this script requires WeeChat 0.3.0 or newer) # # History: +# 2021-04-05, Sébastien Helleu +# version 0.2.5: remove commented infolist code +# 2019-07-24, Sébastien Helleu +# version 0.2.4: make script compatible with Python 3 # 2015-04-29, Colgate Minuette # version 0.2.3: add option to send queued messages on /nick # 2013-11-08, Stefan Huber @@ -38,7 +42,7 @@ SCRIPT_NAME = "postpone" SCRIPT_AUTHOR = "Alexander Schremmer " -SCRIPT_VERSION = "0.2.3" +SCRIPT_VERSION = "0.2.5" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Postpones written messages for later dispatching if target nick is not on channel" @@ -106,11 +110,6 @@ def command_run_input(data, buffer, command): postpone_data.setdefault(server, {}).setdefault(channel, {}).setdefault(nick.lower(), []).append(save) w.buffer_set(buffer, 'input', "") - # XXX why doesn't this work? i want to have the typed text - # in the history - #history_list = w.infolist_get("history", buffer, "") - #history_item = w.infolist_new_item(history_list) - #w.infolist_new_var_string(history_item, "text", input_s) return w.WEECHAT_RC_OK @@ -118,7 +117,7 @@ def command_run_input(data, buffer, command): SCRIPT_DESC, "", ""): version = w.info_get('version_number', '') or 0 - for option, default_desc in settings.iteritems(): + for option, default_desc in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_desc[0]) if int(version) >= 0x00030500: diff --git a/python/prism.py b/python/prism.py index cbde043d..b9286dac 100644 --- a/python/prism.py +++ b/python/prism.py @@ -8,6 +8,10 @@ # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION # # 0. You just DO WHAT THE FUCK YOU WANT TO. +# 2019-08-24, simonpatapon +# v0.2.12: make the script compatible with python 3 +# 2017-06-28, Aoede +# v0.2.11: add -k switch to add black background # 2015-11-16, wowaname # v0.2.9, 0.2.10: wrote an actual parser rather than regex # 2014-09-03, Matthew Martin @@ -30,11 +34,12 @@ import weechat as w import random import re +import sys SCRIPT_NAME = "prism" SCRIPT_AUTHOR = "Alex Barrett " -SCRIPT_VERSION = "0.2.10" +SCRIPT_VERSION = "0.2.12" SCRIPT_LICENSE = "WTFPL" SCRIPT_DESC = "Taste the rainbow." @@ -59,31 +64,50 @@ SCRIPT_LICENSE, SCRIPT_DESC, "", ""): w.hook_command("prism", SCRIPT_DESC, - "[-rwmbe] text|-c[wbe] text", + "[-rwmbek] text|-c[wbe] text", " -r: randomizes the order of the color sequence\n" " -w: color entire words instead of individual characters\n" " -m: append /me to beginning of output\n" " -b: backwards text (entire string is reversed)\n" " -e: eye-destroying colors (randomized background colors)\n" + " -k: add black background (note: -e overrides this)\n" " -c: specify a separator to turn on colorization\n" " eg. -c : /topic :howdy howdy howdy\n" " text: text to be colored", "-r|-w|-m|-b|-e|-c", "prism_cmd_cb", "") + + def find_another_color(colorCode): - otherColor = (unicode(colors[random.randint(1, color_count - 1) % color_count]).rjust(2, "0")) + otherColor = (str(colors[random.randint(1, color_count - 1) % color_count]).rjust(2, "0")) + try: + otherColor = otherColor.decode('utf-8') + except AttributeError: + pass while (otherColor == colorCode): - otherColor = (unicode(colors[random.randint(1, color_count - 1) % color_count]).rjust(2, "0")) + otherColor = (str(colors[random.randint(1, color_count - 1) % color_count]).rjust(2, "0")) + try: + otherColor = otherColor.decode('utf-8') + except AttributeError: + pass return otherColor + def prism_cmd_cb(data, buffer, args): global color_index - input = args.decode("UTF-8") + try: + input = args.decode('utf-8') + except AttributeError: + input = args + input_method = "command" if not input or (input[0] == '-' and input.find(' ') == -1): input = (input + ' ' if input else '') + w.buffer_get_string(buffer, "input") - input = input.decode("UTF-8") + try: + input = input.decode('utf-8') + except AttributeError: + pass input_method = "keybinding" if not input: @@ -93,27 +117,36 @@ def prism_cmd_cb(data, buffer, args): opts = input[1:optstop] if optstop else '' cmdstop = 'c' in opts and input.find(' ', optstop+1) cmd = '' - if 'm' in opts: cmd = '/me ' + if 'm' in opts: + cmd = '/me ' if 'c' in opts: find = input[optstop+1:cmdstop] where = input.find(find, cmdstop+1) cmd = input[cmdstop+1:where] input = input[where+len(find):] - else: input = input[optstop+bool(optstop):] + else: + input = input[optstop+bool(optstop):] regex = regex_words if 'w' in opts else regex_chars inc = 'r' not in opts bs = 'e' in opts + k = 'k' in opts input = input[::-1] if 'b' in opts else input - output = u"" + output = "" tokens = re.findall(regex, input) for token in tokens: # prefix each token with a color code - color_code = unicode(colors[color_index % color_count]).rjust(2, "0") + color_code = str(colors[color_index % color_count]).rjust(2, "0") + try: + color_code = color_code.decode('utf-8') + except AttributeError: + pass if bs == 1: - output += u'\x03' + color_code + ',' + find_another_color(color_code) + token + output += '\x03' + color_code + ',' + find_another_color(color_code) + token + elif k == 1: + output += '\x03' + color_code + ',' + '1'.rjust(2, "0") + token else: - output += u"\x03" + color_code + token + output += "\x03" + color_code + token # select the next color or another color at # random depending on the options specified @@ -121,7 +154,7 @@ def prism_cmd_cb(data, buffer, args): color_index += random.randint(1, color_count - 1) else: color_index += inc - output += u'\x0f' + output += '\x0f' # output starting with a / will be executed as a # command unless we escape it with a preceding / @@ -130,8 +163,10 @@ def prism_cmd_cb(data, buffer, args): output = "/" + output if len(cmd) > 0: output = cmd + output + if sys.version_info < (3, ): + output = output.encode('utf-8') if input_method == "keybinding": - w.buffer_set(buffer, "input", output.encode("UTF-8")) + w.buffer_set(buffer, "input", output) else: - w.command(buffer, output.encode("UTF-8")) + w.command(buffer, output) return w.WEECHAT_RC_OK diff --git a/python/purgelogs.py b/python/purgelogs.py index 9a36c043..527c3d8e 100644 --- a/python/purgelogs.py +++ b/python/purgelogs.py @@ -29,6 +29,8 @@ # /set plugins.var.python.purgelogs.blacklist "#weechat,#weechat-fr,nils_2" # # History: +# 2021-05-05: Sébastien Helleu +# 0.5 : add compatibility with XDG directories (WeeChat >= 3.2) # 2013-01-25: nils_2, (freenode.#weechat) # 0.4 : make script compatible with Python 3.x # 2011-09-18: nils_2, (freenode.#weechat) @@ -57,7 +59,7 @@ SCRIPT_NAME = "purgelogs" SCRIPT_AUTHOR = "nils_2 " -SCRIPT_VERSION = "0.4" +SCRIPT_VERSION = "0.5" SCRIPT_LICENSE = "GPL" SCRIPT_DESC = "delete weechatlog-files by age or size (YOU ARE USING THIS SCRIPT AT YOUR OWN RISK!)" @@ -124,7 +126,13 @@ def purgelogs_cb(data, buffer, args): def get_path(): """ get logger path """ - return w.config_string(w.config_get("logger.file.path")).replace("%h", w.info_get("weechat_dir", "")) + options = { + 'directory': 'data', + } + return w.string_eval_path_home( + w.config_string(w.config_get("logger.file.path")), + {}, {}, options + ) def is_number(s): """ check if value is a number """ diff --git a/python/pushjet.py b/python/pushjet.py new file mode 100644 index 00000000..1a60ae77 --- /dev/null +++ b/python/pushjet.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 p3lim +# +# https://github.com/p3lim/weechat-pushjet + +try: + import weechat +except ImportError: + from sys import exit + print('This script has to run under WeeChat (https://weechat.org/).') + exit(1) + +from urllib import urlencode + +SCRIPT_NAME = 'pushjet' +SCRIPT_AUTHOR = 'p3lim' +SCRIPT_VERSION = '0.1.1' +SCRIPT_LICENSE = 'MIT' +SCRIPT_DESC = 'Send highlights and mentions through Pushjet.io' + +SETTINGS = { + 'host': ( + 'https://api.pushjet.io', + 'host for the pushjet api'), + 'secret': ( + '', + 'secret for the pushjet api'), + 'level': ( + '4', + 'severity level for the message, from 1 to 5 (low to high)'), + 'timeout': ( + '30', + 'timeout for the message sending in seconds (>= 1)'), + 'separator': ( + ': ', + 'separator between nick and message in notifications'), + 'notify_on_highlight': ( + 'on', + 'push notifications for highlights in buffers (on/off)'), + 'notify_on_privmsg': ( + 'on', + 'push notifications for private messages (on/off)'), + 'notify_when': ( + 'always', + 'when to push notifications (away/detached/always/never)'), + 'ignore_buffers': ( + '', + 'comma-separated list of buffers to ignore'), + 'ignore_nicks': ( + '', + 'comma-separated list of users to not push notifications from'), +} + +def send_message(title, message): + secret = weechat.config_get_plugin('secret') + if secret != '': + data = { + 'secret': secret, + 'level': int(weechat.config_get_plugin('level')), + 'title': title, + 'message': message, + } + + host = weechat.config_get_plugin('host').rstrip('/') + '/message' + timeout = int(weechat.config_get_plugin('timeout')) * 1000 + + if timeout <= 0: + timeout = 1 + + data = urlencode(data) + cmd = 'python -c \'from urllib2 import Request, urlopen; r = urlopen(Request("%s", "%s")); print r.getcode()\'' % (host, data) + weechat.hook_process(cmd, timeout, 'send_message_callback', '') + +def send_message_callback(data, command, return_code, out, err): + if return_code != 0: + # something went wrong + return weechat.WEECHAT_RC_ERROR + + return weechat.WEECHAT_RC_OK + +def get_sender(tags, prefix): + # attempt to find sender from tags + # nicks are always prefixed with 'nick_' + for tag in tags: + if tag.startswith('nick_'): + return tag[5:] + + # fallback method to find sender from prefix + # nicks in prefixes are prefixed with optional modes (e.g @ for operators) + # so we have to strip away those first, if they exist + if prefix.startswith(('~', '&', '@', '%', '+', '-', ' ')): + return prefix[1:] + + return prefix + +def get_buffer_names(buffer): + buffer_names = [] + buffer_names.append(weechat.buffer_get_string(buffer, 'short_name')) + buffer_names.append(weechat.buffer_get_string(buffer, 'name')) + return buffer_names + +def should_send(buffer, tags, nick, highlighted): + if not nick: + # a nick is required to form a correct message, bail + return False + + if highlighted: + if weechat.config_get_plugin('notify_on_highlight') != 'on': + # notifying on highlights is disabled, bail + return False + elif weechat.buffer_get_string(buffer, 'localvar_type') == 'private': + if weechat.config_get_plugin('notify_on_privmsg') != 'on': + # notifying on private messages is disabled, bail + return False + else: + # not a highlight or private message, bail + return False + + notify_when = weechat.config_get_plugin('notify_when') + if notify_when == 'never': + # user has opted to not be notified, bail + return False + elif notify_when == 'away': + # user has opted to only be notified when away + infolist_args = ( + weechat.buffer_get_string(buffer, 'localvar_channel'), + weechat.buffer_get_string(buffer, 'localvar_server'), + weechat.buffer_get_string(buffer, 'localvar_nick') + ) + + if not None in infolist_args: + infolist = weechat.infolist_get('irc_nick', '', ','.join(infolist_args)) + if infolist: + away_status = weechat.infolist_integer(infolist, 'away') + weechat.infolist_free(infolist) + if not away_status: + # user is not away, bail + return False + elif notify_when == 'detached': + # user has opted to only be notified when detached (relays) + num_relays = weechat.info_get('relay_client_count', 'connected') + if num_relays == 0: + # no relays connected, bail + return False + + if nick == weechat.buffer_get_string(buffer, 'localvar_nick'): + # the sender was the current user, bail + return False + + if nick in weechat.config_get_plugin('ignore_nicks').split(','): + # the sender was on the ignore list, bail + return False + + for buffer_name in get_buffer_names(buffer): + if buffer_name in weechat.config_get_plugin('ignore_buffers').split(','): + # the buffer was on the ignore list, bail + return False + + return True + +def message_callback(data, buffer, date, tags, displayed, highlight, prefix, message): + nick = get_sender(tags, prefix) + + if should_send(buffer, tags, nick, int(highlight)): + message = '%s%s%s' % (nick, weechat.config_get_plugin('separator'), message) + + if int(highlight): + buffer_names = get_buffer_names(buffer) + send_message(buffer_names[0] or buffer_names[1], message) + else: + send_message('Private Message', message) + + return weechat.WEECHAT_RC_OK + +# register plugin +weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', '') + +# grab all messages in any buffer +weechat.hook_print('', '', '', 1, 'message_callback', '') + +# register configuration defaults +for option, value in SETTINGS.items(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value[0]) + + weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) diff --git a/python/pybuffer.py b/python/pybuffer.py index eb3fde7e..ba19d6d1 100644 --- a/python/pybuffer.py +++ b/python/pybuffer.py @@ -40,7 +40,7 @@ # '0x9ca4ce0' # >>> buffer_get(b, 'input') # Traceback (most recent call last): -# File "", line 1, in +# File "", line 1, in # NameError: name 'buffer_get' is not defined # >>> search('buffer') # ['buffer_clear', 'buffer_close', 'buffer_get_integer', 'buffer_get_pointer', 'buffer_get_string', @@ -57,6 +57,10 @@ # # # History: +# 2018-10-03 +# version 0.3: +# * Python3 compatibility (Pol Van Aubel ) +# # 2010-11-05 # version 0.2: # * More interperter console. @@ -66,10 +70,14 @@ # 2010-10-30 # version 0.1: Initial release ### +from __future__ import print_function + +import sys # Only required for python version check. +PY2 = sys.version_info < (3,) SCRIPT_NAME = "pybuffer" SCRIPT_AUTHOR = "Elián Hanisch " -SCRIPT_VERSION = "0.2" +SCRIPT_VERSION = "0.3" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Python interpreter for WeeChat and module for debug scripts." @@ -78,8 +86,8 @@ from weechat import WEECHAT_RC_OK, prnt import_ok = True except ImportError: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org/") import_ok = False import code, sys, traceback @@ -88,21 +96,21 @@ def callback(method): """This function will take a bound method or function and make it a callback.""" # try to create a descriptive and unique name. - func = method.func_name + funcname = method.__name__ try: - im_self = method.im_self + classinst = method.__self__ try: - inst = im_self.__name__ + instname = classinst.__name__ except AttributeError: try: - inst = im_self.name + instname = classinst.name except AttributeError: - raise Exception("Instance %s has no __name__ attribute" %im_self) - cls = type(im_self).__name__ - name = '_'.join((cls, inst, func)) + raise Exception("Instance %s has no __name__ attribute" % classinst) + classname = type(classinst).__name__ + name = '_'.join((classname, instname, funcname)) except AttributeError: # not a bound method - name = func + name = funcname # set our callback import __main__ @@ -127,8 +135,8 @@ class Command(object): def __init__(self): assert self.command, "No command defined" self.__name__ = self.command - self._pointer = '' - self._callback = '' + self._pointer = '' + self._callback = '' def __call__(self, *args): return self.callback(*args) @@ -138,7 +146,7 @@ def callback(self, data, buffer, args): self.data, self.buffer, self.args = data, buffer, args try: self.parser(args) # argument parsing - except ArgumentError, e: + except ArgumentError as e: error('Argument error, %s' %e) except NoArguments: pass @@ -165,7 +173,7 @@ def hook(self): self.completion, self._callback, '') if pointer == '': - raise Exception, "hook_command failed: %s %s" %(SCRIPT_NAME, self.command) + raise Exception("hook_command failed: %s %s" %(SCRIPT_NAME, self.command)) self._pointer = pointer def unhook(self): @@ -220,10 +228,18 @@ def error(self, s, *args): def prnt(self, s, *args, **kwargs): """Prints messages in buffer.""" buffer = self._getBuffer() - if not isinstance(s, basestring): - s = str(s) + + # Ensure s is a str (Py3) or a str/unicode (Py2) so that s % args will work. + if PY2: + # Don't accidentally convert unicode to utf-8 or ASCII or whatever because we want to see the object. + if not isinstance(s, basestring): + s = str(s) + else: + if not isinstance(s, str): + s = str(s) + if args: - s = s %args + s = s % args try: s = kwargs['prefix'] + s except KeyError: @@ -304,7 +320,7 @@ def _create(self): def __call__(self, s, *args, **kwargs): kwargs['prefix'] = self.color_call self.prnt(s, *args, **kwargs) - + def input_return(self, data, buffer, command): # we need to send returns even when there's no input. if data == buffer and not weechat.buffer_get_string(buffer, 'input'): diff --git a/python/pyrnotify.py b/python/pyrnotify.py index 5d24f92a..1cea0686 100644 --- a/python/pyrnotify.py +++ b/python/pyrnotify.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- # ex:sw=4 ts=4:ai: # -# Copyright (c) 2012 by Krister Svanlund +# SPDX-FileCopyrightText: 2012 Krister Svanlund +# +# SPDX-License-Identifier: GPL-3.0-or-later +# # based on tcl version: # Remote Notification Script v1.1 # by Gotisch @@ -25,7 +28,7 @@ # # On the "client" (where the notifications will end up), host is # the remote host where weechat is running: -# python2 location/of/pyrnotify.py 4321 & ssh -R 4321:localhost:4321 username@host +# python2 location/of/pyrnotify.py 4321 & ssh -R 4321:localhost:4321 username@host # You can have a second argument to specified the time to display the notification # python2 location/of/pyrnotify.py 4321 2000 & ssh -R 4321:localhost:4321 username@host # Important to remember is that you should probably setup the @@ -33,80 +36,98 @@ # autossh to do this in the background. # # In weechat: -# /python load pyrnotify.py -# and set the port -# /set plugins.var.python.pyrnotify.port 4321 +# /python load pyrnotify.py +# and set the port +# /set plugins.var.python.pyrnotify.port 4321 # # It is also possible to set which host pyrnotify shall connect to, # this is not recommended. Using a ssh port-forward is much safer # and doesn't require any ports but ssh to be open. # ChangeLog: +# 2025-12-06: Modernize code, avoid escaping using regex by using json +# instead of shell-like serialization. # +# 2018-08-20: Make it work with python3 +# use of sendall instead of send # 2014-05-10: Change hook_print callback argument type of displayed/highlight # (WeeChat >= 1.0) # 2012-06-19: Added simple escaping to the title and body strings for # the script to handle trailing backslashes. + try: import weechat as w + in_weechat = True except ImportError as e: in_weechat = False -import os, sys, re +import json +import os +import re import socket import subprocess -import shlex +import sys -SCRIPT_NAME = "pyrnotify" -SCRIPT_AUTHOR = "Krister Svanlund " -SCRIPT_VERSION = "1.0" +SCRIPT_NAME = "pyrnotify" +SCRIPT_AUTHOR = "Krister Svanlund " +SCRIPT_VERSION = "2.0.1" SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Send remote notifications over SSH" +SCRIPT_DESC = "Send remote notifications over SSH" -def escape(s): - return re.sub(r'([\\"\'])', r'\\\1', s) -def run_notify(icon, nick,chan,message): - host = w.config_get_plugin('host') +def run_notify(icon, nick, chan, message): + host = w.config_get_plugin("host") try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.connect((host, int(w.config_get_plugin('port')))) - s.send("normal %s \"%s to %s\" \"%s\"" % (icon, nick, escape(chan), escape(message))) + s.connect((host, int(w.config_get_plugin("port")))) + msg = { + "urgency": "normal", + "icon": icon, + "nick": nick, + "chan": chan, + "message": message, + } + s.sendall(json.dumps(msg, ensure_ascii=False).encode("UTF-8")) s.close() except Exception as e: - w.prnt("", "Could not send notification: %s" % str(e)) + w.prnt("", "Could not send notification: {0}".format(e)) + def on_msg(*a): if len(a) == 8: data, buffer, timestamp, tags, displayed, highlight, sender, message = a if data == "private" or int(highlight): - if data == "private" and w.config_get_plugin('pm-icon'): - icon = w.config_get_plugin('pm-icon') + if data == "private" and w.config_get_plugin("pm-icon"): + icon = w.config_get_plugin("pm-icon") else: - icon = w.config_get_plugin('icon') - buffer = "me" if data == "private" else w.buffer_get_string(buffer, "short_name") + icon = w.config_get_plugin("icon") + buffer = ( + "me" if data == "private" else w.buffer_get_string(buffer, "short_name") + ) run_notify(icon, sender, buffer, message) - #w.prnt("", str(a)) return w.WEECHAT_RC_OK + def weechat_script(): - settings = {'host' : "localhost", - 'port' : "4321", - 'icon' : "utilities-terminal", - 'pm-icon' : "emblem-favorite"} - if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - for (kw, v) in settings.items(): + settings = { + "host": "localhost", + "port": "4321", + "icon": "utilities-terminal", + "pm-icon": "emblem-favorite", + } + if w.register( + SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "" + ): + for kw, v in settings.items(): if not w.config_get_plugin(kw): w.config_set_plugin(kw, v) w.hook_print("", "notify_message", "", 1, "on_msg", "") w.hook_print("", "notify_private", "", 1, "on_msg", "private") - w.hook_print("", "notify_highlight", "", 1, "on_msg", "") # Not sure if this is needed - - - - + w.hook_print( + "", "notify_highlight", "", 1, "on_msg", "" + ) # Not sure if this is needed ###################################### @@ -114,44 +135,53 @@ def weechat_script(): ## supposed to be executed in weechat, instead it runs when the script is executed from ## commandline. + def accept_connections(s, timeout=None): conn, addr = s.accept() try: - data = "" - d = conn.recv(1024) - while d: + data = b"" + while d := conn.recv(1024): data += d - d = conn.recv(1024) finally: conn.close() if data: try: - urgency, icon, title, body = shlex.split(data) + notif = json.loads(data.decode("UTF-8")) + argv = [ + "notify-send", + "-u", + notif["urgency"], + "-c", + "IRC", + "-i", + notif["icon"], + "--", + notif["chan"], + notif["message"], + ] if timeout: - subprocess.call(["notify-send", "-t", timeout, "-u", urgency, "-c", "IRC", "-i", icon, escape(title), escape(body)]) - else: - subprocess.call(["notify-send", "-u", urgency, "-c", "IRC", "-i", icon, escape(title), escape(body)]) + argv.extend(["-t", timeout]) + subprocess.run(argv) + except (ValueError, OSError) as e: + print(e) - except ValueError as e: - print e - except OSError as e: - print e - accept_connections(s, timeout) def weechat_client(argv): + port = int(argv[1]) if len(sys.argv) > 1 else 4321 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind(("localhost", int(argv[1] if len(sys.argv) > 1 else 4321))) + s.bind(("localhost", port)) s.listen(5) try: - accept_connections(s, argv[2] if len(sys.argv) > 2 else None) - except KeyboardInterrupt as e: - print "Keyboard interrupt" - print e + while True: + accept_connections(s, argv[2] if len(sys.argv) > 2 else None) + except KeyboardInterrupt: + return finally: s.close() -if __name__ == '__main__': + +if __name__ == "__main__": if in_weechat: weechat_script() else: diff --git a/python/queryman.py b/python/queryman.py index c979f93f..bbd3173e 100644 --- a/python/queryman.py +++ b/python/queryman.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- # -# Copyright (c) 2013-2016 by nils_2 +# SPDX-FileCopyrightText: 2013-2025 Nils Görs +# SPDX-FileCopyrightText: 2017 Filip H.F. 'FiXato' Slagter # -# save and restore query buffers after /quit +# SPDX-License-Identifier: GPL-3.0-or-later # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,9 +17,32 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -# idea by lasers@freenode.#weechat +# Save and restore query buffers after /quit. # -# 2016-02-27: nils_2, (freenode.#weechat) +# Idea by lasers@freenode.#weechat +# +# 2025-10-02: nils_2 (libera.#weechat) +# 0.7 : fix a ValueError in config file, when using dcc. localvar "server", is missing for dcc buffer (reported by roughnecks) +# : add some more DEBUG() text +# +# 2023-08-01: nils_2 (libera.#weechat) +# 0.6.1 : fix a timing problem when joining autojoin-channels (reported by thecdnhermit) +# +# 2021-05-05: Sébastien Helleu +# 0.6 : add compatibility with XDG directories (WeeChat >= 3.2) +# +# 2018-08-08: nils_2, (freenode.#weechat) +# 0.5 : fix TypeError with python3.6 +# +# 2017-04-14: nils_2 & FiXato, (freenode.#weechat) +# 0.4 : big rewrite: +# : added extra hooks: +# - query buffers are now also stored when opening/closing queries +# - queries only restored on connect; no longer on every reconnect +# : current buffer position is retained +# : manual saving of query list (https://github.com/weechat/scripts/issues/196) +# +# 2015-02-27: nils_2, (freenode.#weechat) # 0.3 : make script consistent with "buffer_switch_autojoin" option (idea haasn) # # 2013-11-07: nils_2, (freenode.#weechat) @@ -45,86 +68,241 @@ SCRIPT_NAME = 'queryman' SCRIPT_AUTHOR = 'nils_2 ' -SCRIPT_VERSION = '0.3' +SCRIPT_VERSION = '0.7' SCRIPT_LICENSE = 'GPL' -SCRIPT_DESC = 'save and restore query buffers after /quit' +SCRIPT_DESC = 'save and restore query buffers after /quit and on open/close of queries' +DEBUG = False -query_buffer_list = [] queryman_filename = 'queryman.txt' +servers_opening = set([]) +servers_closing = set([]) +stored_query_buffers_per_server = {} # ================================[ callback ]=============================== +# signal_data = buffer pointer +def buffer_closing_signal_cb(data, signal, signal_data): + global servers_closing + + buf_type = weechat.buffer_get_string(signal_data,'localvar_type') + # When closing a server buffer, save all buffers + if buf_type == 'server': + # Prevent closing private buffers on this server, from triggering saving. + servers_closing.add(weechat.buffer_get_string(signal_data, 'localvar_server')) + + # # FIXME: This shouldn't be necessary, as buffers are already save when opened/closed + # reset_stored_query_buffers() + # save_stored_query_buffers_to_file() + # Only save the query buffers + elif buf_type == 'private': + server_name = weechat.buffer_get_string(signal_data, 'localvar_server') + # Don't trigger when all buffer's closing because its server buffer is closing + if server_name not in servers_closing: + channel_name = weechat.buffer_get_string(signal_data, 'localvar_channel') + remove_channel_from_stored_list(server_name, channel_name) + save_stored_query_buffers_to_file() + return weechat.WEECHAT_RC_OK + def quit_signal_cb(data, signal, signal_data): - save_query_buffer_to_file() + reset_stored_query_buffers() + save_stored_query_buffers_to_file() return weechat.WEECHAT_RC_OK -# signal_data contains servername +# signal_data = buffer pointer +def irc_pv_opened_cb(data, signal, signal_data): + server_name = weechat.buffer_get_string(signal_data, 'localvar_server') + channel_name = weechat.buffer_get_string(signal_data, 'localvar_channel') + add_channel_to_stored_list(server_name, channel_name) + debug_print("signal: irc_pv_opened server: %s query: %s" % (server_name, channel_name)) + save_stored_query_buffers_to_file() + return weechat.WEECHAT_RC_OK + +# signal_data = server name +def remove_server_from_servers_closing_cb(data, signal, signal_data): + global servers_closing + if signal_data in servers_closing: + servers_closing.remove(signal_data) + debug_print("signal: irc_server_closing") + return weechat.WEECHAT_RC_OK + +# signal_data = buffer pointer +def irc_server_opened_cb(data, signal, signal_data): + global servers_opening + + server_name = weechat.buffer_get_string(signal_data, 'localvar_server') + servers_opening.add(server_name) + debug_print("signal: irc_server_opened %s" % server_name) + return weechat.WEECHAT_RC_OK + +# signal_data = servername def irc_server_connected_signal_cb(data, signal, signal_data): - load_query_buffer_irc_server_opened(signal_data) + global servers_opening + + # Only reopen the query buffers if the server buffer was recently opened + if signal_data in servers_opening: + open_stored_query_buffers_for_server(signal_data) + servers_opening.remove(signal_data) + debug_print("signal: irc_server_connected") + return weechat.WEECHAT_RC_OK # ================================[ file ]=============================== def get_filename_with_path(): global queryman_filename - path = weechat.info_get("weechat_dir", "") + path = weechat.info_get("weechat_data_dir", "") \ + or weechat.info_get("weechat_dir", "") return os.path.join(path,queryman_filename) -def load_query_buffer_irc_server_opened(server_connected): - global query_buffer_list +# ======== [ Stored Query Buffers List ] ========== +def get_stored_list_of_query_buffers(): + global stored_query_buffers_per_server filename = get_filename_with_path() + stored_query_buffers_per_server = {} if os.path.isfile(filename): - f = open(filename, 'rb') + f = open(filename, 'r') for line in f: - servername,nick = line.split(' ') - if servername == server_connected: - noswitch = "" - switch_autojoin = weechat.config_get("irc.look.buffer_switch_autojoin") - if not weechat.config_boolean(switch_autojoin): - noswitch = "-noswitch" - weechat.command('','/query %s -server %s %s' % ( noswitch, servername, nick )) + parts = line.strip().split() + if len(parts) == 2: + server_name, nick = parts + stored_query_buffers_per_server.setdefault(server_name, set([])) + stored_query_buffers_per_server[server_name].add(nick) f.close() else: - weechat.prnt('','%s%s: Error loading query buffer from "%s"' % (weechat.prefix('error'), SCRIPT_NAME, filename)) + debug_print('Error loading query buffer from "%s"' % filename) + return stored_query_buffers_per_server -def save_query_buffer_to_file(): - global query_buffer_list +def remove_channel_from_stored_list(server_name, channel_name): + global stored_query_buffers_per_server - ptr_infolist_buffer = weechat.infolist_get('buffer', '', '') + if server_name in stored_query_buffers_per_server and channel_name in stored_query_buffers_per_server[server_name]: + stored_query_buffers_per_server[server_name].remove(channel_name) + if not len(stored_query_buffers_per_server[server_name]): + stored_query_buffers_per_server.pop(server_name, None) + +def add_channel_to_stored_list(server_name, channel_name): + global stored_query_buffers_per_server + if server_name not in stored_query_buffers_per_server: + stored_query_buffers_per_server[server_name] = set([]) + if channel_name not in stored_query_buffers_per_server[server_name]: + stored_query_buffers_per_server[server_name].add(channel_name) + +def open_query_buffer(server_name, nick): + starting_buffer = weechat.current_buffer() + noswitch = "" + switch_autojoin = weechat.config_get("irc.look.buffer_switch_autojoin") + if not weechat.config_boolean(switch_autojoin): + noswitch = "-noswitch" + + debug_print("opening query buffer: %s on server %s" % (nick, server_name)) + weechat.command('','/wait 1 /query %s -server %s %s' % ( noswitch, server_name, nick )) + weechat.buffer_set(starting_buffer, 'display', 'auto') + +def open_stored_query_buffers_for_server(server_connected): + global stored_query_buffers_per_server + + if server_connected in stored_query_buffers_per_server: + for nick in stored_query_buffers_per_server[server_connected].copy(): + debug_print("going to open query buffer: %s on connected server %s" % (nick, server_connected)) + open_query_buffer(server_connected, nick) + +def get_current_query_buffers(): + stored_query_buffers_per_server = {} + + ptr_infolist_buffer = weechat.infolist_get('buffer', '', '') while weechat.infolist_next(ptr_infolist_buffer): ptr_buffer = weechat.infolist_pointer(ptr_infolist_buffer,'pointer') - type = weechat.buffer_get_string(ptr_buffer, 'localvar_type') - if type == 'private': - server = weechat.buffer_get_string(ptr_buffer, 'localvar_server') - channel = weechat.buffer_get_string(ptr_buffer, 'localvar_channel') - query_buffer_list.insert(0,"%s %s" % (server,channel)) + buf_type = weechat.buffer_get_string(ptr_buffer, 'localvar_type') + if buf_type == 'private': + server_name = weechat.buffer_get_string(ptr_buffer, 'localvar_server') + channel_name = weechat.buffer_get_string(ptr_buffer, 'localvar_channel') + stored_query_buffers_per_server.setdefault(server_name, set([])) + stored_query_buffers_per_server[server_name].add(channel_name) weechat.infolist_free(ptr_infolist_buffer) + return stored_query_buffers_per_server + +def reset_stored_query_buffers(): + global stored_query_buffers_per_server + stored_query_buffers_per_server = get_current_query_buffers() + + +def remove_data_file(): filename = get_filename_with_path() + if os.path.isfile(filename): + os.remove(filename) + +def save_stored_query_buffers_to_file(): + global stored_query_buffers_per_server - if len(query_buffer_list): + filename = get_filename_with_path() + if len(stored_query_buffers_per_server): + debug_print("Storing %s servers:" % len(stored_query_buffers_per_server)) try: f = open(filename, 'w') - i = 0 - while i < len(query_buffer_list): - f.write('%s\n' % query_buffer_list[i]) - i = i + 1 + for (server_name, channels) in stored_query_buffers_per_server.items(): + debug_print("Storing %s channels in server %s" % (len(channels), server_name)) + for channel_name in channels: + line = "%s %s" % (server_name,channel_name) + debug_print(' - %s' % line) + f.write("%s\n" % line) f.close() except: - weechat.prnt('','%s%s: Error writing query buffer to "%s"' % (weechat.prefix('error'), SCRIPT_NAME, filename)) + print_error('Error writing query buffer to "%s"' % filename) raise else: # no query buffer(s). remove file - if os.path.isfile(filename): - os.remove(filename) + debug_print("No stored query buffers; removing data file") + remove_data_file() return +def print_error(message): + weechat.prnt('','%s%s: %s' % (weechat.prefix('error'), SCRIPT_NAME, message)) + +def debug_print(message): + if not DEBUG: + return + weechat.prnt('','DEBUG/%s: %s' % (SCRIPT_NAME, message)) + +def hook_command_cb(data, buffer, args): + if args == "": # no args given. quit + return weechat.WEECHAT_RC_OK + argv = args.strip().split(" ") + if argv[0].lower() == 'save': + save_stored_query_buffers_to_file() + return weechat.WEECHAT_RC_OK + # ================================[ main ]=============================== if __name__ == '__main__': if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): version = weechat.info_get('version_number', '') or 0 if int(version) >= 0x00030700: + weechat.hook_command(SCRIPT_NAME,SCRIPT_DESC, + 'save', + 'save : manual saving of the query list\n', + '', + 'hook_command_cb', '') + + stored_query_buffers_per_server = get_stored_list_of_query_buffers() + for (server_name, channels) in get_current_query_buffers().items(): + # Reopen the buffers for the channels in the servers we already have open: + open_stored_query_buffers_for_server(server_name) + + stored_query_buffers_per_server.setdefault(server_name, set([])) + debug_print("Already have %s queries for server %s: %s" % (len(stored_query_buffers_per_server[server_name]), server_name, ','.join(stored_query_buffers_per_server[server_name]))) + debug_print("Adding: %s" % channels) + stored_query_buffers_per_server[server_name].update(channels) + debug_print("Now have %s queries for server %s: %s" % (len(stored_query_buffers_per_server[server_name]), server_name, ','.join(stored_query_buffers_per_server[server_name]))) + save_stored_query_buffers_to_file() weechat.hook_signal('quit', 'quit_signal_cb', '') - weechat.hook_signal('irc_server_connected', 'irc_server_connected_signal_cb', '') +# weechat.hook_signal('relay_client_disconnected', 'quit_signal_cb', '') +# weechat.hook_signal('relay_client_connected', 'irc_server_connected_signal_cb', '') + weechat.hook_signal('irc_server_opened', 'irc_server_opened_cb', '') + weechat.hook_signal('irc_server_connected', 'irc_server_connected_signal_cb','') + weechat.hook_signal('irc_server_disconnected', 'remove_server_from_servers_closing_cb', '') + + # TODO: make these triggers optional? + weechat.hook_signal('irc_pv_opened', 'irc_pv_opened_cb', '') + weechat.hook_signal('buffer_closing', 'buffer_closing_signal_cb', '') diff --git a/python/queue.py b/python/queue.py deleted file mode 100644 index 9e68383d..00000000 --- a/python/queue.py +++ /dev/null @@ -1,434 +0,0 @@ -# ==================================================== # -# Script Name: queue.py -# Script Author: walk -# Script Purpose: Command queing at its finest. Hopefully. -# -# Copyright (C) 2011 walk -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# Version History: -# -# 0.4.2 - Nov 22nd, 2015 -# Add saving of static queues to disk and reloading them on startup. -# Added by Tim Kuhlman - https://github.com/tkuhlman -# 0.4.1 - Jan 20th, 2011 -# Multi-list queuing seems to work flawlessly so far. Expanded on the /help qu text. -# Properties are fully-functional. As for loading/saving of lists, I want to hold off until -# I get some feedback on whether or not that would be applicable. Please email me -# at the listed address in this script and let me know if you want this feature and/or -# any other features. -# -# 0.4.0 - Jan 16th, 2011 -# Finished adding multi-list queuing. So far, no bugs that I can tell. -# Perhaps an ability to load/save lists for multiple uses? Now working on -# setting individual properties per list then taking a few days off this thing. -# ** TESTED ON WeeChat 0.3.3 and 0.3.4 ** -# -# 0.3.5 - Jan 15th, 2011 -# Started to implement multiple queue lists. -# -# 0.3.5 - Jan 14th, 2011 -# Wrote Queue class and merged with code. I did this because -# some future release will include an ability to use multiple -# queue lists. Also fixed some bugs in parsing and executing -# arguments with /qu. /qu del by itself raised an error, /qu del j -# raised a valueerror, fixed. /qu add singlearg wouldn't add. Fixed. -# -# 0.3.4 - Jan 11th, 2011 -# Modified configuration code to use plugins.conf -# -# 0.3.3 - Jan 9th, 2011 -# Big code clean-up. Reduced a few lines by optimizing code. -# Next step, convertcommand_qu dictionary to array like -# I should have done in the first place. Could save some lines. -# -# 0.3.2 - Jan 9th, 2011 -# Added RAINBOW option (requested) -# Set as option in configuration -# -# 0.3.1 - Jan 8th, 2011 -# Added configurations (verbose, core output only) -# -# 0.3.0 - Jan 6th, 2011 -# Worked on script for quite a while. All code is functional. -# Continuing to test for bugs and improve code -# Fixed indexing issue after using /qu del with remove_index function -# -# 0.2.0 - Jan 5th, 2011 -# Cleaned up temporary testing code, built upon foundation. -# Next to include, del function and list function -# -# 0.1.0 - Jan 4th, 2011 -# Wrote basic outline with minimal functionality -# ==================================================== # - -import os -import pickle - -import_ok = True -try: - import weechat -except ImportError: - print("This script requires WeeChat") - print("To obtain a copy, for free, visit http://weechat.org") - import_ok = False - - -class Queue(): - """ A Queuing Class """ - def __init__(self): - self.data = [] - self.index = len(self.data) - self.__clearable__ = True - self.__ldl__ = '' - self.__locked__ = False - - def __iter__(self): - for char in range(self.index): - yield self.data[char] - - def __len__(self): - return len(self.data) - - def add(self, queue_text): - if self.__locked__ == False: - self.data.append(queue_text) - self.index = len(self.data) - - def remove(self, info): - if self.__locked__ == False: - tmp = '' - if info > 0: - tmp = str(self.data[info-1]) - del self.data[info-1] - self.__ldl__ = tmp - self.index = len(self.data) - elif info == 0: - tmp = str(self.data[info]) - del self.data[info] - self.__ldl__ = tmp - self.index = len(self.data) - - return self.__ldl__ - - def viewqueue(self): - list = '' - if not len(self.data) == 0: - for each in range(len(self.data)): - list+=str(each+1) + ". " + str(self.data[each]) + "\n" - else: - list='Nothing in queue' - - return list.strip("\n") - - def clearqueue(self): - if self.__clearable__ == True: - self.data = [] - self.index = 0 - - def isClear(self, cOpt): - if not cOpt in (True, False): - return - - self.__clearable__ = cOpt - - def isLocked(self, lockOpt): - if not lockOpt in (True, False): - return - - self.__locked__ = lockOpt - - def isEmpty(self): - if len(self.data) == 0: - return True - else: - return False - - -SCRIPT_NAME = "queue" -SCRIPT_AUTHOR = "walk" -SCRIPT_VERSION = "0.4.2" -SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Command queuing" - -COMM_CMD = "qu" -COMM_DESC = "Queuing commands in WeeChat" -COMM_ARGS = "[add [command] | del [index] | new [list] | dellist [list] | set [property] [on|off] |list | clear | exec | listview]" -COMM_ARGS_DESC = "Examples: \n\ - /qu add /msg chanserv op #foo bar \n\ - /qu del 1 \n\ - /qu new weechat \n\ - - Use the 'new' argument to switch to already defined lists as well. \n\ - /qu dellist weechat \n\ - /qu list - List commands in current list \n\ - /qu list weechat - With optional parameter, you can choose to list the commands of a specified list. \n\ - /qu clear - Clear current list.. add a listname to clear a specified list. \n\ - /qu exec - Execute the commands of the current list.. you can also specify a list here as well. \n\ - /qu listview - Outputs the names of all your lists. \n\ - /qu save - Save static lists to disk \n\ - /qu set static on - Sets static property to ON for current list. This means that when executed, the list WILL NOT clear. The clear command will not work either.\n \ - \n\ - PROPERTIES (for set command):\n \ - static - prevents a list from clearing manually or automatically but can still add and del commands.\n \ - lock - prevents the user from adding/deleting entries to a list. Can be combined with static." -COMM_COMPL = "add|del|list|exec|new|listview|dellist" - -COMMAND_QU = {'default': Queue()} -CURR_LIST = 'default' - -def __config__(): - """ Configuration initialization """ - - if not weechat.config_get_plugin("core_output_only") in ("yes", "no"): - weechat.config_set_plugin("core_output_only", "yes") - if not weechat.config_get_plugin("rainbow_allow") in ("yes", "no"): - weechat.config_set_plugin("rainbow_allow", "no") - if not weechat.config_get_plugin("verbose") in ("yes", "no"): - weechat.config_set_plugin("verbose", "yes") - - load() - return weechat.WEECHAT_RC_OK - -def load(): - """ Load saved queues from pickle. """ - global COMMAND_QU - pickle_path = os.path.join(weechat.info_get("weechat_dir", ""), 'queue.pickle') - if os.path.exists(pickle_path): - with open(pickle_path, 'r') as qu_pickle: - COMMAND_QU = pickle.load(qu_pickle) - - if 'default' not in COMMAND_QU: - COMMAND_QU['default'] = Queue() - -def rainbow(data): - """ Not my favorite option but a requested one """ - - colors='red yellow green blue magenta' - c=colors.split() - count=0 - colorHolder='' - - for each in data: - if count > 4: count = 0 - if not each == " ": - colorHolder+=weechat.color(c[count])+each - count += 1 - else: - colorHolder+=" " - - return str(colorHolder) - -def prntcore(data, essential=0, rb=0): - """ Built more on weechat.prnt """ - - if weechat.config_get_plugin("verbose") == 'yes' or essential==1: - if weechat.config_get_plugin("core_output_only") =='yes': - buffer = '' - else: - buffer = weechat.current_buffer() - if rb == 0: - weechat.prnt(buffer, data) - else: - weechat.prnt(buffer, rainbow(data)) - return weechat.WEECHAT_RC_OK - -def save(): - """ Save to disk all static lists as a pickle. """ - global COMMAND_QU - pickle_path = os.path.join(weechat.info_get("weechat_dir", ""), 'queue.pickle') - to_save = {} - for name, qu in COMMAND_QU.iteritems(): - if not qu.__clearable__: # Note isClear method doesn't show status it sets it - to_save[name] = qu - - with open(pickle_path, 'w') as qu_pickle: - pickle.dump(to_save, qu_pickle, pickle.HIGHEST_PROTOCOL) - -def rejoin(data, delimiter=' '): - """ Rejoins a split string """ - tmpString = '' - for each in data: - tmpString+=each+delimiter - - tmpString = tmpString.strip() - return tmpString - -def qu_cb(data, buffer, args): - """ Process hook_command info """ - - global CURR_LIST, COMMAND_QU - if weechat.config_get_plugin('rainbow_allow') == 'no': - rainbowit=0 - else: - rainbowit=1 - - if args == "": - return weechat.WEECHAT_RC_OK - - argv = args.split() - arglist = ['add', 'del', 'new', 'dellist', 'list', 'clear', 'exec', 'listview', 'save', 'set'] - - if not argv[0] in arglist: - prntcore('[ queue -> not a valid argument: {0}'.format(argv[0]), rb=rainbowit) - return weechat.WEECHAT_RC_OK - - if argv[0].lower() == "add" and len(argv) > 1: - if not COMMAND_QU[CURR_LIST].__locked__ == True: - COMMAND_QU[CURR_LIST].add(rejoin(argv[1:])) - prntcore("[ queue added -> "+str(rejoin(argv[1:])) + " ]", rb=rainbowit) - else: - prntcore("[ queue -> the lock property is enabled for this list ({0}). please disable it before adding/deleting. ]", rb=rainbowit) - - elif argv[0].lower() == "del" and len(argv) > 1: - if not COMMAND_QU[CURR_LIST].__locked__ == True: - try: - rmd = COMMAND_QU[CURR_LIST].remove(int(argv[1])) - prntcore("[ queue -> deleted: ({0}) {1} ]".format(argv[1],rmd), rb=rainbowit) - except (IndexError, ValueError): - prntcore("[ queue -> invalid reference. please check /qu list and try again. ]", rb=rainbowit) - else: - prntcore("[ queue -> the lock property is enabled for this list ({0}). please disable it before adding/deleting. ]".format(CURR_LIST), rb=rainbowit) - - elif argv[0].lower() == "clear": - this_list = None - if len(argv) > 1 and argv[1].lower() in COMMAND_QU.keys(): - this_list = CURR_LIST - CURR_LIST = argv[1].lower() - - if COMMAND_QU[CURR_LIST].__clearable__ == True: - if not COMMAND_QU[CURR_LIST].isEmpty(): - COMMAND_QU[CURR_LIST].clearqueue() - prntcore('[ queue -> command queue list cleared. ]', rb=rainbowit) - else: - prntcore('[ queue -> command queue already empty. ]', rb=rainbowit) - else: - prntcore('[ queue -> please turn off the static property to clear the {0} list. ]'.format(CURR_LIST), rb=rainbowit) - - if not this_list == None: - CURR_LIST = this_list - this_list = None - - elif argv[0].lower() == "list": - this_list = None - if len(argv) > 1 and argv[1].lower() in COMMAND_QU.keys(): - this_list = CURR_LIST - CURR_LIST = argv[1].lower() - - qHeader = '[ COMMAND QUEUE: {0} ]'.format(CURR_LIST) - prntcore(" ", 1) - prntcore("-"*len(qHeader), 1, rb=rainbowit) - prntcore(qHeader, 1, rainbowit) - prntcore("-"*len(qHeader), 1, rainbowit) - prntcore(COMMAND_QU[CURR_LIST].viewqueue(), 1, rb=rainbowit) - - if not this_list == None: - CURR_LIST = this_list - this_list = None - - elif argv[0].lower() == "exec": - - this_list = None - if len(argv) > 1 and argv[1].lower() in COMMAND_QU.keys(): - this_list = CURR_LIST - CURR_LIST = argv[1].lower() - - if len(COMMAND_QU[CURR_LIST]) > 0: - - for each in COMMAND_QU[CURR_LIST]: - weechat.command(buffer, each) - COMMAND_QU[CURR_LIST].clearqueue() - if COMMAND_QU[CURR_LIST].__clearable__ == True: - prntcore('[ queue -> finished executing list: {0}. command list cleared. ]'.format(CURR_LIST), rb=rainbowit) - else: - prntcore('[ queue -> finished executing list: {0} ]'.format(CURR_LIST), rb=rainbowit) - else: - prntcore("[ queue -> nothing to execute. please add to the queue using /qu add ", rb=rainbowit) - - if not this_list == None: - CURR_LIST = this_list - this_list = None - - elif argv[0].lower() == "new" and len(args.split()) > 1: - if argv[1].lower() in COMMAND_QU.keys(): - CURR_LIST = argv[1].lower() - prntcore("[ queue -> switched queue list to: {0}".format(CURR_LIST), rb=rainbowit) - else: - COMMAND_QU[argv[1].lower()] = Queue() - CURR_LIST = argv[1].lower() - prntcore("[ queue -> created new list. current list is: {0}".format(CURR_LIST), rb=rainbowit) - - elif argv[0].lower() == "listview": - qHeader = 'QUEUE LISTS' - listCount = 1 - - prntcore(' ', 1) - prntcore('-'*len(qHeader), 1, rb=rainbowit) - prntcore(qHeader, 1, rb=rainbowit) - prntcore('-'*len(qHeader), 1, rb=rainbowit) - - for each in COMMAND_QU.keys(): - prntcore(str(listCount) + ". " + str(each), 1, rb=rainbowit) - listCount += 1 - - elif argv[0].lower() == "dellist" and len(args.split()) > 1: - if not argv[1].lower() in COMMAND_QU.keys(): - prntcore('[ queue -> {0} is not a list. ]'.format(argv[1].lower()), rb=rainbowit) - elif argv[1].lower() == 'default': - prntcore('[ queue -> cannot delete the default list. ]', rb=rainbowit) - else: - if argv[1].lower() == CURR_LIST: - CURR_LIST = 'default' - del COMMAND_QU[argv[1].lower()] - prntcore('[ queue -> {0} successfully deleted.'.format(argv[1].lower()), rb=rainbowit) - - elif argv[0].lower() == "save": - save() - elif argv[0].lower() == "set" and len(argv) == 4: - setargs = args.split() - list_name = setargs[1].lower() - set_prop = setargs[2].lower() - toggle = setargs[3].lower() - properties = ['static', 'lock'] - - if not list_name in COMMAND_QU.keys(): - prntcore('[ queue -> list must be created before you can set properties ]', rb=rainbowit) - elif not set_prop in properties: - prntcore('[ queue -> invalid property. please try again. ]', rb=rainbowit) - elif not toggle in ('on', 'off'): - prntcore('[ queue -> only valid options for a property are ON or OFF ]', rb=rainbowit) - else: - if set_prop == 'static': - if toggle=='on': - COMMAND_QU[list_name].isClear(False) - prntcore('[ queue -> static property toggled on for: {0} ]'.format(list_name), rb=rainbowit) - save() - else: - COMMAND_QU[list_name].isClear(True) - prntcore('[ queue -> static property toggled off for: {0} ]'.format(list_name), rb=rainbowit) - save() - - elif set_prop == 'lock': - if toggle=='on': - COMMAND_QU[list_name].isLocked(True) - prntcore('[ queue -> lock property toggled on for: {0} ]'.format(list_name), rb=rainbowit) - else: - COMMAND_QU[list_name].isLocked(False) - prntcore('[ queue -> lock property toggled off for: {0} ]'.format(list_name), rb=rainbowit) - - return weechat.WEECHAT_RC_OK - -if import_ok and weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - weechat.hook_command(COMM_CMD, COMM_DESC, COMM_ARGS, COMM_ARGS_DESC, COMM_COMPL, "qu_cb", "") - __config__() diff --git a/python/quick_force_color.py b/python/quick_force_color.py index 315e2e72..c3553b8a 100644 --- a/python/quick_force_color.py +++ b/python/quick_force_color.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2012-2016 by nils_2 +# Copyright (c) 2012-2017 by nils_2 # # quickly add/del/change entry in nick_color_force # @@ -17,8 +17,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +# 2017-08-17: nils_2,(freenode.#weechat) +# 0.6.1: print nicks in sorted order +# 2017-05-18: ticalc-travis (https://github.com/weechatter/weechat-scripts/pull/18) +# 0.6 : Clean up some redundant code +# : Add nicks to irc.look.nick_color_force in sorted order for easier manual editing +# : Display proper feedback on incorrect commands +# : Fix inconsistencies in help syntax +# : Don't retain nicks that have been manually removed from nick_color_force +# : Provide feedback messages for successful operations # 2016-04-17: nils_2,(freenode.#weechat) -# 0.5 : make script compatible with option weechat.look.nick_color_force (weechat >= 1.5) +# 0.5 : make script compatible with option weechat.look.nick_color_force (weechat >=1.5) # 2013-01-25: nils_2,(freenode.#weechat) # 0.4 : make script compatible with Python 3.x # 2012-07-08: obiwahn, (freenode) @@ -49,7 +58,7 @@ SCRIPT_NAME = "quick_force_color" SCRIPT_AUTHOR = "nils_2 " -SCRIPT_VERSION = "0.5" +SCRIPT_VERSION = "0.6.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "quickly add/del/change entry in nick_color_force" @@ -64,14 +73,19 @@ nick_option_new = "weechat.look.nick_color_force" nick_option = "" # ================================[ callback ]=============================== +def print_usage(buffer): + weechat.prnt(buffer, "Usage: /%s list [nick] | add nick color | del nick" % SCRIPT_NAME) + def nick_colors_cmd_cb(data, buffer, args): global colored_nicks if args == "": # no args given. quit + print_usage(buffer) return weechat.WEECHAT_RC_OK argv = args.strip().split(" ") if (len(argv) == 0) or (len(argv) >= 4): # maximum of 3 args!! + print_usage(buffer) return weechat.WEECHAT_RC_OK bufpointer = weechat.window_get_pointer(buffer,'buffer') # current buffer @@ -81,39 +95,42 @@ def nick_colors_cmd_cb(data, buffer, args): if argv[0].lower() == 'list': # list all nicks if len(colored_nicks) == 0: weechat.prnt(buffer,'%sno nicks in \"%s\"...' % (weechat.prefix("error"),nick_option)) - return weechat.WEECHAT_RC_OK - if len(argv) == 2: + elif len(argv) == 2: if argv[1] in colored_nicks: color = colored_nicks[argv[1]] # get color from given nick weechat.prnt(buffer,"%s%s: %s" % (weechat.color(color),argv[1],color)) else: weechat.prnt(buffer,"no color set for: %s" % (argv[1])) - return weechat.WEECHAT_RC_OK - weechat.prnt(buffer,"List of nicks in : %s" % nick_option) -# for nick,color in colored_nicks.items(): - for nick,color in list(colored_nicks.items()): - weechat.prnt(buffer,"%s%s: %s" % (weechat.color(color),nick,color)) - return weechat.WEECHAT_RC_OK + else: + weechat.prnt(buffer,"List of nicks in : %s" % nick_option) - if (argv[0].lower() == 'add') and (len(argv) == 3): - if argv[1] in colored_nicks: # search if nick exists - colored_nicks[argv[1]] = argv[2] + for nick,color in sorted(list(colored_nicks.items())): + weechat.prnt(buffer,"%s%s: %s" % (weechat.color(color),nick,color)) + + elif (argv[0].lower() == 'add') and (len(argv) == 3): + if argv[1] in colored_nicks: + weechat.prnt(buffer, "Changing nick '%s' to color %s%s" % (argv[1], weechat.color(argv[2]), argv[2])) else: - colored_nicks[argv[1]] = argv[2] # add [nick] = [color] + weechat.prnt(buffer, "Adding nick '%s' with color %s%s" % (argv[1], weechat.color(argv[2]), argv[2])) + colored_nicks[argv[1]] = argv[2] save_new_force_nicks() - if (argv[0].lower() == 'del') and (len(argv) == 2): + elif (argv[0].lower() == 'del') and (len(argv) == 2): if argv[1] in colored_nicks: # search if nick exists del colored_nicks[argv[1]] save_new_force_nicks() + weechat.prnt(buffer, "Removed nick '%s'" % argv[1]) + else: + weechat.prnt(buffer, "Nick '%s' not found in nick_color_force" % argv[1]) + else: + print_usage(buffer) return weechat.WEECHAT_RC_OK def save_new_force_nicks(): global colored_nicks -# new_nick_color_force = ';'.join([ ':'.join(item) for item in colored_nicks.items()]) - new_nick_color_force = ';'.join([ ':'.join(item) for item in list(colored_nicks.items())]) + new_nick_color_force = ';'.join([ ':'.join(item) for item in sorted(colored_nicks.items())]) config_pnt = weechat.config_get(nick_option) weechat.config_option_set(config_pnt,new_nick_color_force,1) @@ -133,7 +150,8 @@ def force_nick_colors_completion_cb(data, completion_item, buffer, completion): def create_list(): global nick_color_force,colored_nicks # colored_nicks = dict([elem.split(':') for elem in nick_color_force.split(';')]) - nick_color_force = weechat.config_string(weechat.config_get(nick_option)) # get list + colored_nicks = {} + nick_color_force = weechat.config_string(weechat.config_get(nick_option)) # get list if nick_color_force != '': nick_color_force = nick_color_force.strip(';') # remove ';' at beginning and end of string for elem in nick_color_force.split(';'): # split nick1:color;nick2:color @@ -151,10 +169,10 @@ def create_list(): version = weechat.info_get('version_number', '') or 0 if int(version) >= 0x00030400: weechat.hook_command(SCRIPT_NAME,SCRIPT_DESC, - 'add || del || list', + 'add || del || list []', 'add : add a nick with its color to nick_color_force\n' 'del : delete given nick with its color from nick_color_force\n' - 'list : list all forced nicks with its assigned color or optional from one nick\n\n' + 'list [] : list all forced nicks with its assigned color or optional from one nick\n\n' 'Examples:\n' ' add nick nils_2 with color red:\n' ' /' + SCRIPT_NAME + ' add nils_2 red\n' diff --git a/python/read_marker.py b/python/read_marker.py new file mode 100644 index 00000000..41946857 --- /dev/null +++ b/python/read_marker.py @@ -0,0 +1,166 @@ +# Copyright (c) 2022 Simon Ser +# +# License: GNU Affero General Public License version 3 +# https://www.gnu.org/licenses/agpl-3.0.en.html +# +# This script adds support for the draft/read-marker IRC extension, defined in +# https://ircv3.net/specs/extensions/read-marker +# +# The extension synchronizes the read marker between multiple clients. If a +# user is connected on two devices (e.g. a laptop and a phone), reading a +# message on a device will also mark it as read on the other device. + +import weechat +import datetime + +weechat.register("read_marker", "emersion", "0.2.0", "AGPL3", "draft/read-marker extension support", "", "") + +READ_MARKER_CAP = "draft/read-marker" + +weechat_version = int(weechat.info_get("version_number", "") or 0) + +if weechat_version < 0x04000000: + caps_option = weechat.config_get("irc.server_default.capabilities") + caps = weechat.config_string(caps_option) + if READ_MARKER_CAP not in caps: + if caps != "": + caps += "," + caps += READ_MARKER_CAP + weechat.config_option_set(caps_option, caps, 1) + +read_times = {} + +def server_by_name(server_name): + hdata = weechat.hdata_get("irc_server") + server_list = weechat.hdata_get_list(hdata, "irc_servers") + if weechat_version >= 0x03040000: + return weechat.hdata_search( + hdata, + server_list, + "${irc_server.name} == ${name}", + {}, + {"name": server_name}, + {}, + 1, + ) + else: + return weechat.hdata_search( + hdata, + server_list, + "${irc_server.name} == " + server_name, + 1, + ) + +def set_buffer_read_time(buffer, t): + if buffer in read_times and t <= read_times[buffer]: + return False + read_times[buffer] = t + return True + +def get_last_message_time(buffer): + lines = weechat.hdata_pointer(weechat.hdata_get("buffer"), buffer, "own_lines") + line = weechat.hdata_pointer(weechat.hdata_get("lines"), lines, "last_line") + while line: + line_data = weechat.hdata_pointer(weechat.hdata_get("line"), line, "data") + tags_count = weechat.hdata_integer(weechat.hdata_get("line_data"), line_data, "tags_count") + tags = [ + weechat.hdata_string(weechat.hdata_get("line_data"), line_data, "{}|tags_array".format(i)) + for i in range(tags_count) + ] + irc_tags = [t for t in tags if t.startswith("irc_")] + if len(irc_tags) > 0: + break + line = weechat.hdata_pointer(weechat.hdata_get("line"), line, "prev_line") + if not line: + return None + # TODO: get timestamp with millisecond granularity + ts = weechat.hdata_time(weechat.hdata_get("line_data"), line_data, "date") + t = datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc) + return t + +def sync_buffer_hotlist(buffer): + t = get_last_message_time(buffer) + if t != None and buffer in read_times and read_times[buffer] >= t: + weechat.buffer_set(buffer, "hotlist", "-1") + +def handle_markread_msg(data, signal, signal_data): + server_name = signal.split(",")[0] + msg = weechat.info_get_hashtable("irc_message_parse", { "message": signal_data }) + + args = msg["arguments"].split(" ") + target = args[0] + criteria = args[1] + if criteria == "*": + return weechat.WEECHAT_RC_OK_EAT + if not criteria.startswith("timestamp="): + return weechat.WEECHAT_RC_OK + s = criteria.replace("timestamp=", "").replace("Z", "+00:00") + t = datetime.datetime.fromisoformat(s) + + buffer = weechat.info_get("irc_buffer", server_name + "," + target) + if buffer and set_buffer_read_time(buffer, t): + sync_buffer_hotlist(buffer) + + return weechat.WEECHAT_RC_OK_EAT + +def handle_buffer_close(data, signal, signal_data): + buffer = signal_data + + if buffer in read_times: + del read_times[buffer] + return weechat.WEECHAT_RC_OK + +def send_buffer_read(buffer, server_name, short_name): + server = server_by_name(server_name) + + hdata = weechat.hdata_get("irc_server") + cap_list = weechat.hdata_hashtable(hdata, server, "cap_list") + if not READ_MARKER_CAP in cap_list: + return + + t = get_last_message_time(buffer) + if t == None: + return + + if not set_buffer_read_time(buffer, t): + return + + # Workaround for WeeChat timestamps missing millisecond granularity + t += datetime.timedelta(milliseconds=999) + t = t.astimezone(datetime.timezone.utc) + s = t.isoformat(timespec="milliseconds").replace("+00:00", "Z") + cmd = "MARKREAD " + short_name + " timestamp=" + s + server_buffer = weechat.buffer_search("irc", "server." + server_name) + weechat.command_options(server_buffer, "/quote " + cmd, { "commands": "quote" }) + +def handle_hotlist_change(data, signal, signal_data): + buffer = signal_data + + if buffer: + sync_buffer_hotlist(buffer) + return weechat.WEECHAT_RC_OK + + hdata = weechat.hdata_get("buffer") + buffer = weechat.hdata_get_list(hdata, "gui_buffers") + while buffer: + full_name = weechat.hdata_string(hdata, buffer, "full_name") + short_name = weechat.hdata_string(hdata, buffer, "short_name") + hotlist = weechat.hdata_pointer(hdata, buffer, "hotlist") + if not hotlist and full_name.startswith("irc.") and not full_name.startswith("irc.server."): + # Trim "irc." prefix and "." suffix to obtain server name + server_name = full_name.replace("irc.", "", 1)[:-len(short_name) - 1] + send_buffer_read(buffer, server_name, short_name) + buffer = weechat.hdata_pointer(hdata, buffer, "next_buffer") + return weechat.WEECHAT_RC_OK + +def handle_cap_sync_req(data, modifier, modifier_data, requested): + supported = modifier_data.split(",")[1].split(" ") + if READ_MARKER_CAP in supported: + requested += " " + READ_MARKER_CAP + return requested + +weechat.hook_signal("*,irc_raw_in_markread", "handle_markread_msg", "") +weechat.hook_signal("buffer_closed", "handle_buffer_close", "") +weechat.hook_signal("hotlist_changed", "handle_hotlist_change", "") +if weechat_version >= 0x04000000: + weechat.hook_modifier("irc_cap_sync_req", "handle_cap_sync_req", "") diff --git a/python/reop.py b/python/reop.py new file mode 100644 index 00000000..f76b294c --- /dev/null +++ b/python/reop.py @@ -0,0 +1,284 @@ +"""Unlike most automatic op/voice plugins, this plugin uses the reop (R) and invite (I) lists of a channel for automatic op and voice control. This allows for transparent and centralised channel administration while responsibility is shared with all channel operators. This is particularly useful for networks that have no services (e.g., IRCnet). + + +Subcommands + + list: Show the reop and invite lists of all activated channels. + reload: Reload the reop and invite lists for all activated channels. + + +Configuration + +The plugin can be enabled or disabled via the following setting. + + /set plugins.var.python.reop.enabled + +The plugin can be activated for channels in a particular network by adding a key-value pair to the configuration with the name of the network as the key and a comma separated list of channel names as its value. E.g., the following activates the plugin for channels #chan1 and #chan2 on ircnet. + + /set plugins.var.python.reop.net.ircnet #chan1,#chan2 + +Servers or other clients may also set user privileges. To avoid unnecessary chatter, the current user privileges are checked some time after a user joins. Only if the privilege level is lower than configured in the reop and invite lists, the appropriate privileges are granted. The delay is configured via the following setting. + + /set plugins.var.python.reop.delay 1000 + + +Usage + +Operator control is managed by manipulating the reop (R) list. + +Grant operator privileges. + + /mode #chan1 +R nick!user@host.name + +Revoke operator privileges. + + /mode #chan1 -R nick!user@host.name + +Voice control is managed by manipulating the invite (I) list. + +Grant voice privileges. + + /mode #chan1 +I nick!user@host.name + +Revoke voice privileges. + + /mode #chan1 -I nick!user@host.name +""" +from weechat import ( + WEECHAT_RC_OK, buffer_search, command, config_get_plugin, + config_set_plugin, hook_command, hook_modifier, hook_signal, hook_timer, + info_get, info_get_hashtable, infolist_get, infolist_free, infolist_next, + infolist_string, prnt, register, string_match) + +_name = 'reop' +_author = 'Jeroen F.J. Laros ' +_version = '1.0.0' +_license = 'MIT' +_description = 'Use reop and invite lists for automatic op/voice control.' + +reop_data = {} + + +def _parse(signal, signal_data): + """Parse a signal.""" + network = signal.split(',')[0] + msg = info_get_hashtable('irc_message_parse', {'message': signal_data}) + buf = info_get('irc_buffer', '{},{}'.format(network, msg['channel'])) + + return network, msg, buf + + +def _level(network, channel, nick): + """Determine the current privilege level.""" + nicks = infolist_get( + 'irc_nick', '', '{},{},{}'.format(network, channel, nick)) + + prefix = ' ' + while infolist_next(nicks): + prefix = infolist_string(nicks, 'prefix') + + infolist_free(nicks) + + # y q a o h v * + return [' ', '+', '@'].index(prefix) + + +def timer_cb(data, remaining_calls): + """Timer callback.""" + buf, level, network, channel, nick = reop_data['cmd'].pop() + + if _level(network, channel, nick) < level: + if level == 2: + command(buf, '/mode {} +o {}'.format(channel, nick)) + elif level == 1: + command(buf, '/mode {} +v {}'.format(channel, nick)) + + return WEECHAT_RC_OK + + +def _schedule(buf, level, network, channel, nick): + """Schedule a mode change.""" + reop_data['cmd'].append((buf, level, network, channel, nick)) + hook_timer(reop_data['delay'], 0, 1, 'timer_cb', '') + + +def _join(buf, channel): + """Get reop and invite lists.""" + reop_data['hide'] += 2 + + command(buf, '/mode {} R'.format(channel)) + command(buf, '/mode {} I'.format(channel)) + + +def join_cb(data, signal, signal_data): + """Join callback.""" + network, msg, buf = _parse(signal, signal_data) + + cache = reop_data['cache'] + if network in cache: + channel = msg['channel'] + + if channel in cache[network]: + nick = msg['nick'] + host = msg['host'] + + if nick != info_get('irc_nick', network): + # Someone is joining. + for mask in cache[network][channel]['reop']: + if string_match(host, mask, 1): + _schedule(buf, 2, network, channel, nick) + return WEECHAT_RC_OK + + for mask in cache[network][channel]['invite']: + if string_match(host, mask, 1): + _schedule(buf, 1, network, channel, nick) + return WEECHAT_RC_OK + else: + # User is joining. + cache[network][channel] = {'reop': set(), 'invite': set()} + _join(buf, channel) + + return WEECHAT_RC_OK + + +def list_cb(data, signal, signal_data): + """List callback.""" + network, msg, _ = _parse(signal, signal_data) + + cache = reop_data['cache'] + if network in cache: + channel = msg['channel'] + + if channel in cache[network]: + mask = msg['text'] + command = msg['command'] + + if command == '344': + cache[network][channel]['reop'].add(mask) + elif command == '346': + cache[network][channel]['invite'].add(mask) + + return WEECHAT_RC_OK + + +def mode_cb(data, signal, signal_data): + """Mode callback.""" + network, msg, _ = _parse(signal, signal_data) + + cache = reop_data['cache'] + if network in cache: + channel = msg['channel'] + + if channel in cache[network]: + mode, mask = msg['text'].split(' ')[:2] + + if mode == '+R': + cache[network][channel]['reop'].add(mask) + elif mode == '-R': + cache[network][channel]['reop'].discard(mask) + elif mode == '+I': + cache[network][channel]['invite'].add(mask) + elif mode == '-I': + cache[network][channel]['invite'].discard(mask) + + return WEECHAT_RC_OK + + +def _format(d, indent=0): + """Pretty print a dictionary.""" + for key in d: + prnt('', '{}{}'.format(indent * ' ', key)) + + if isinstance(d, dict): + _format(d[key], indent + 2) + + +def _init_cache(): + """Initialise the cache.""" + reop_data.update({ + 'cache': {}, 'cmd': [], 'hide': 0, + 'delay': int(config_get_plugin('delay'))}) + + networks = infolist_get('irc_server', '', '') + + while infolist_next(networks): + network = infolist_string(networks, 'name') + channels = config_get_plugin('net.{}'.format(network)) + + if channels: + cache = reop_data['cache'] + cache[network] = {} + + for channel in channels.split(','): + cache[network][channel] = { + 'reop': set(), 'invite': set()} + + buf = buffer_search('irc', '{}.{}'.format(network, channel)) + if buf: + _join(buf, channel) + + infolist_free(networks) + + +def command_cb(data, buf, args): + """Command callback.""" + cmd = args.split(' ')[0] + + if cmd == 'list': + prnt('', '\nReop cache (pending {}):\n'.format(reop_data['hide'])) + _format(reop_data['cache']) + elif cmd == 'reload': + _init_cache() + + return WEECHAT_RC_OK + + +def modifier_cb(data, modifier, modifier_data, string): + """Print modifier callback.""" + tagset = set(modifier_data.split(';')[-1].split(',')) + + if reop_data['hide']: + if {'irc_344', 'irc_346'} & tagset: + return '' + if {'irc_345', 'irc_347'} & tagset: + reop_data['hide'] -= 1 + return '' + + return string + + +def _config(): + """Configuration.""" + if not config_get_plugin('enabled'): + config_set_plugin('enabled', 'on') + status = config_get_plugin('enabled') + + if not config_get_plugin('delay'): + config_set_plugin('delay', '5000') + + return status == 'on' + + +def main(): + """Initialisation.""" + register(_name, _author, _version, _license, _description, '', '') + + if not _config(): + return + + _init_cache() + + hook_signal('*,irc_in2_join', 'join_cb', '') + hook_signal('*,irc_in2_344', 'list_cb', '') + hook_signal('*,irc_in2_346', 'list_cb', '') + hook_signal('*,irc_in2_mode', 'mode_cb', '') + + hook_modifier('weechat_print', 'modifier_cb', '') + + hook_command( + 'reop', '{}\n\n{}'.format(_description, __doc__), 'list | reload', + '', '', 'command_cb', '') + + +if __name__ == '__main__': + main() diff --git a/python/responsive_layout.py b/python/responsive_layout.py index 7314ec11..1fd43eed 100644 --- a/python/responsive_layout.py +++ b/python/responsive_layout.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2014 Stefan Wold +# Copyright (C) 2014 - 2019 Stefan Wold # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,10 +25,11 @@ # Commands: # /rlayout +from __future__ import print_function SCRIPT_NAME = "responsive_layout" SCRIPT_AUTHOR = "Stefan Wold " -SCRIPT_VERSION = "0.7" +SCRIPT_VERSION = "0.8" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Responsive layout will automatically apply layouts based on the terminals current size." SCRIPT_COMMAND = "rlayout" @@ -46,14 +47,14 @@ try: import weechat except ImportError: - print "This script must be run under WeeChat." + print("This script must be run under WeeChat.") import_ok = False try: import re from operator import itemgetter except ImportError as err: - print "Missing module(s) for %s: %s" % (SCRIPT_NAME, err) + print("Missing module(s) for %s: %s" % (SCRIPT_NAME, err)) import_ok = False diff --git a/python/sarcasm.py b/python/sarcasm.py new file mode 100644 index 00000000..3910c076 --- /dev/null +++ b/python/sarcasm.py @@ -0,0 +1,47 @@ +# Script Name: sarcasm.py +# Script Author: Fsaev +# Script License: GPLv3 + +SCRIPT_NAME = 'sarcasm' +SCRIPT_AUTHOR = 'Fsaev ' +SCRIPT_VERSION = '1.0' +SCRIPT_LICENSE = 'GPLv3' +SCRIPT_DESC = 'Adds random capitalization to your sentence' + +import_ok = True + +try: + import weechat +except ImportError: + print('This script must be run under WeeChat') + print('You can obtain a copy of WeeChat, for free, at https://weechat.org') + import_ok = False + +from random import randint + +def sarcasm_cb(data, buffer, args): + newstring = "" + for arg in args: + if randint(0, 1) == 1: + newstring += arg.upper() + else: + newstring += arg.lower() + + weechat.command(buffer, newstring) + + return weechat.WEECHAT_RC_OK + +if __name__ == "__main__" and import_ok: + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + weechat.hook_command( + "sarcasm", + """Adds random capitalization to your sentence to indicate that you are being sarcastic, e.g. +/sarcasm I love to put ketchup on my pizza + +results in: +i lOVe tO Put KEtChUp oN mY pIzZa +""", + "message", "", + "", + "sarcasm_cb", "" + ) diff --git a/python/screen_away.py b/python/screen_away.py index 8f68505d..23fa206e 100644 --- a/python/screen_away.py +++ b/python/screen_away.py @@ -24,6 +24,13 @@ # (this script requires WeeChat 0.3.0 or newer) # # History: +# 2019-03-04, Germain Z. +# version 0.16: add option "socket_file", for use with e.g. dtach +# : code reformatting for consistency/PEP8 +# 2017-11-20, Nils Görs +# version 0.15: make script python3 compatible +# : fix problem with empty "command_on_*" options +# : add option "no_output" # 2014-08-02, Nils Görs # version 0.14: add time to detach message. (idea by Mikaela) # 2014-06-19, Anders Bergh @@ -59,27 +66,37 @@ # 2009-11-27, xt # version 0.1: initial release -import weechat as w -import re import os -import datetime, time - -SCRIPT_NAME = "screen_away" -SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "0.14" -SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Set away status on screen detach" - -settings = { - 'message': ('Detached from screen', 'Away message'), - 'time_format': ('since %Y-%m-%d %H:%M:%S%z', 'time format append to away message'), - 'interval': ('5', 'How often in seconds to check screen status'), - 'away_suffix': ('', 'What to append to your nick when you\'re away.'), - 'command_on_attach': ('', 'Commands to execute on attach, separated by semicolon'), - 'command_on_detach': ('', 'Commands to execute on detach, separated by semicolon'), - 'ignore': ('', 'Comma-separated list of servers to ignore.'), - 'set_away': ('on', 'Set user as away.'), - 'ignore_relays': ('off', 'Only check screen status and ignore relay interfaces'), +import re +import time +import weechat as w + + +SCRIPT_NAME = 'screen_away' +SCRIPT_AUTHOR = 'xt ' +SCRIPT_VERSION = '0.16' +SCRIPT_LICENSE = 'GPL3' +SCRIPT_DESC = 'Set away status on screen detach' + +SETTINGS = { + 'message': ('Detached from screen', 'Away message'), + 'time_format': ('since %Y-%m-%d %H:%M:%S%z', + 'time format append to away message'), + 'interval': ('5', 'How often in seconds to check screen status'), + 'away_suffix': ('', 'What to append to your nick when you\'re away.'), + 'command_on_attach': ('', + ('Commands to execute on attach, separated by ' + 'semicolon')), + 'command_on_detach': ('', + ('Commands to execute on detach, separated by ' + 'semicolon')), + 'ignore': ('', 'Comma-separated list of servers to ignore.'), + 'set_away': ('on', 'Set user as away.'), + 'ignore_relays': ('off', + 'Only check screen status and ignore relay interfaces'), + 'no_output': ('off', + 'no detach/attach information will be displayed in buffer'), + 'socket_file': ('', 'Socket file to use (leave blank to auto-detect)'), } TIMER = None @@ -87,51 +104,62 @@ AWAY = False CONNECTED_RELAY = False -def set_timer(): - '''Update timer hook with new interval''' +def set_timer(): + '''Update timer hook with new interval.''' global TIMER if TIMER: w.unhook(TIMER) - TIMER = w.hook_timer(int(w.config_get_plugin('interval')) * 1000, - 0, 0, "screen_away_timer_cb", '') + TIMER = w.hook_timer(int(w.config_get_plugin('interval')) * 1000, 0, 0, + 'screen_away_timer_cb', '') + def screen_away_config_cb(data, option, value): - if option.endswith(".interval"): + '''Update timer / sock file on config changes.''' + global SOCK + if SOCK and option.endswith('.interval'): set_timer() + elif option.endswith('.socket_file'): + SOCK = value + if not SOCK: + SOCK = get_sock() + if SOCK: + set_timer() + elif TIMER: + w.unhook(TIMER) return w.WEECHAT_RC_OK -def get_servers(): - '''Get the servers that are not away, or were set away by this script''' +def get_servers(): + '''Get the servers that are not away, or were set away by this script.''' ignores = w.config_get_plugin('ignore').split(',') - infolist = w.infolist_get('irc_server','','') + infolist = w.infolist_get('irc_server', '', '') buffers = [] while w.infolist_next(infolist): - if not w.infolist_integer(infolist, 'is_connected') == 1 or \ - w.infolist_string(infolist, 'name') in ignores: + if (not w.infolist_integer(infolist, 'is_connected') == 1 or + w.infolist_string(infolist, 'name') in ignores): continue - if not w.config_string_to_boolean(w.config_get_plugin('set_away')) or \ - not w.infolist_integer(infolist, 'is_away') or \ - w.config_get_plugin('message') in w.infolist_string(infolist, 'away_message'): -# w.infolist_string(infolist, 'away_message') == \ -# w.config_get_plugin('message'): + if (not w.config_string_to_boolean(w.config_get_plugin('set_away')) or + not w.infolist_integer(infolist, 'is_away') or + w.config_get_plugin('message') in w.infolist_string( + infolist, 'away_message')): buffers.append((w.infolist_pointer(infolist, 'buffer'), - w.infolist_string(infolist, 'nick'))) + w.infolist_string(infolist, 'nick'))) w.infolist_free(infolist) return buffers -def screen_away_timer_cb(buffer, args): - '''Check if screen is attached, update awayness''' +def screen_away_timer_cb(buffer, args): + '''Check if screen is attached and update awayness.''' global AWAY, SOCK, CONNECTED_RELAY set_away = w.config_string_to_boolean(w.config_get_plugin('set_away')) - check_relays = not w.config_string_to_boolean(w.config_get_plugin('ignore_relays')) + check_relays = not w.config_string_to_boolean( + w.config_get_plugin('ignore_relays')) suffix = w.config_get_plugin('away_suffix') - attached = os.access(SOCK, os.X_OK) # X bit indicates attached + attached = os.access(SOCK, os.X_OK) # X bit indicates attached. - # Check wether a client is connected on relay or not + # Check wether a client is connected on relay or not. CONNECTED_RELAY = False if check_relays: infolist = w.infolist_get('relay', '', '') @@ -143,55 +171,74 @@ def screen_away_timer_cb(buffer, args): break w.infolist_free(infolist) - if (attached and AWAY) or (check_relays and CONNECTED_RELAY and not attached and AWAY): - w.prnt('', '%s: Screen attached. Clearing away status' % SCRIPT_NAME) + if ((attached and AWAY) or + (check_relays and CONNECTED_RELAY and not attached and AWAY)): + if not w.config_string_to_boolean(w.config_get_plugin('no_output')): + w.prnt('', '{}: Screen attached. Clearing away status'.format( + SCRIPT_NAME)) for server, nick in get_servers(): if set_away: - w.command(server, "/away") + w.command(server, '/away') if suffix and nick.endswith(suffix): nick = nick[:-len(suffix)] - w.command(server, "/nick %s" % nick) + w.command(server, '/nick {}'.format(nick)) AWAY = False - for cmd in w.config_get_plugin("command_on_attach").split(";"): - w.command("", cmd) + if w.config_get_plugin('command_on_attach'): + for cmd in w.config_get_plugin('command_on_attach').split(';'): + w.command('', cmd) elif not attached and not AWAY: if not CONNECTED_RELAY: - w.prnt('', '%s: Screen detached. Setting away status' % SCRIPT_NAME) + if (not w.config_string_to_boolean( + w.config_get_plugin('no_output'))): + w.prnt('', '{}: Screen detached. Setting away status'.format( + SCRIPT_NAME)) for server, nick in get_servers(): if suffix and not nick.endswith(suffix): - w.command(server, "/nick %s%s" % (nick, suffix)); + w.command(server, '/nick {}{}'.format(nick, suffix)) if set_away: - w.command(server, "/away %s %s" % (w.config_get_plugin('message'), time.strftime(w.config_get_plugin('time_format')))) + w.command(server, '/away {} {}'.format( + w.config_get_plugin('message'), + time.strftime(w.config_get_plugin('time_format')))) AWAY = True - for cmd in w.config_get_plugin("command_on_detach").split(";"): - w.command("", cmd) + if w.config_get_plugin('command_on_detach'): + for cmd in w.config_get_plugin('command_on_detach').split(';'): + w.command('', cmd) return w.WEECHAT_RC_OK +def get_sock(): + '''Try to get the appropriate sock file for screen/tmux.''' + sock = None + if 'STY' in os.environ.keys(): + # We are running under screen. + cmd_output = os.popen('env LC_ALL=C screen -ls').read() + match = re.search(r'Sockets? in (/.+)\.', cmd_output) + if match: + sock = os.path.join(match.group(1), os.environ['STY']) + + if not sock and 'TMUX' in os.environ.keys(): + # We are running under tmux. + socket_data = os.environ['TMUX'] + sock = socket_data.rsplit(',', 2)[0] + return sock + + if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, "", ""): + SCRIPT_DESC, '', ''): version = w.info_get('version_number', '') or 0 - for option, default_desc in settings.iteritems(): + for option, default_desc in SETTINGS.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_desc[0]) if int(version) >= 0x00030500: w.config_set_desc_plugin(option, default_desc[1]) - if 'STY' in os.environ.keys(): - # We are running under screen - cmd_output = os.popen('env LC_ALL=C screen -ls').read() - match = re.search(r'Sockets? in (/.+)\.', cmd_output) - if match: - SOCK = os.path.join(match.group(1), os.environ['STY']) - - if not SOCK and 'TMUX' in os.environ.keys(): - # We are running under tmux - socket_data = os.environ['TMUX'] - SOCK = socket_data.rsplit(',',2)[0] + SOCK = w.config_get_plugin('socket_file') + if not SOCK: + SOCK = get_sock() if SOCK: set_timer() - w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", - "screen_away_config_cb", "") + w.hook_config('plugins.var.python.{}.*'.format(SCRIPT_NAME), + 'screen_away_config_cb', '') diff --git a/python/selfcensor.py b/python/selfcensor.py new file mode 100644 index 00000000..624ccb92 --- /dev/null +++ b/python/selfcensor.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# (this script requires WeeChat 0.3.0 or newer) +# +# Copyright 2018 tx +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# self-censor stuff you were going to say +# +# configuration examples: +# +# set comma separated words: +# /set plugins.var.python.selfcensor.censors "shit,fuck" +# /set plugins.var.python.selfcensor.censors "://reddit.com,://youtube.com" +# /set plugins.var.python.selfcensor.censors "" +# +# set warning message: +# /set plugins.var.python.selfcensor.warning "NOPE!" +# +# set tourette message (automatically sent instead of your message): +# /set plugins.var.python.selfcensor.tourette "FUCK" +# +# disable tourette: +# /set plugins.var.python.selfcensor.tourette "" +# +# History: +# +# 2018-09-20, tx +# v0.1: initial release + +import weechat as w +import re + +SCRIPT_NAME = "selfcensor" +SCRIPT_AUTHOR = "tx -# -# Licence : GPL v2 -# Description : running shell commands in WeeChat -# Syntax : try /help shell to get some help on this script -# Precond : needs weechat >= 0.3.0 to run -# -# -# ### changelog ### -# -# * version 0.8, 2013-07-27, Sebastien Helleu : -# - don't remove empty lines in output of command -# * version 0.7, 2012-11-26, Sebastien Helleu : -# - use hashtable for command arguments (for WeeChat >= 0.4.0) -# * version 0.6, 2012-11-21, Sebastien Helleu : -# - call shell in hook_process (WeeChat >= 0.3.9.2 does not call shell any more) -# * version 0.5, 2011-10-01, Sebastien Helleu : -# - add shell buffer -# * version 0.4, 2009-05-02, Sebastien Helleu : -# - sync with last API changes -# * version 0.3, 2009-03-06, Sebastien Helleu : -# - use of hook_process to run background process -# - add option -t to kill process after seconds -# - show process running, kill it with -kill -# * version 0.2, 2009-01-31, Sebastien Helleu : -# - conversion to WeeChat 0.3.0+ -# * version 0.1, 2006-03-13, Kolter : -# - first release -# -# ============================================================================= - -import weechat, os, datetime - -SCRIPT_NAME = 'shell' -SCRIPT_AUTHOR = 'Kolter' -SCRIPT_VERSION = '0.8' -SCRIPT_LICENSE = 'GPL2' -SCRIPT_DESC = 'Run shell commands in WeeChat' - -SHELL_CMD = 'shell' -SHELL_PREFIX = '[shell] ' - -cmd_hook_process = '' -cmd_command = '' -cmd_start_time = None -cmd_buffer = '' -cmd_shell_buffer = '' -cmd_stdout = '' -cmd_stderr = '' -cmd_send_to_buffer = '' -cmd_timeout = 0 - -if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, '', ''): - weechat.hook_command( - SHELL_CMD, - 'Running shell commands in WeeChat', - '[-o|-n] [-t seconds] || -show || -kill', - ' -o: send output to current buffer (simulate user entry ' - 'with command output - dangerous, be careful when using this option)\n' - ' -n: display output in a new empty buffer\n' - '-t seconds: auto-kill process after timeout (seconds) if process ' - 'is still running\n' - ' command: shell command or builtin like cd, getenv, setenv, unsetenv\n' - ' -show: show running process\n' - ' -kill: kill running process', - '-o|-n|-t|cd|getenv|setenv|unsetenv|-show||-kill -o|-n|-t|cd|getenv|setenv|unsetenv', - 'shell_cmd', '') - -def shell_init(): - """Initialize some variables.""" - global cmd_hook_process, cmd_command, cmd_start_time, cmd_buffer, cmd_stdout, cmd_stderr - cmd_hook_process = '' - cmd_command = '' - cmd_start_time = None - cmd_buffer = '' - cmd_stdout = '' - cmd_stderr = '' - -def shell_set_title(): - """Set title on shell buffer (with working directory).""" - global cmd_shell_buffer - if cmd_shell_buffer: - weechat.buffer_set(cmd_shell_buffer, 'title', - '%s.py %s | "q": close buffer | Working dir: %s' % (SCRIPT_NAME, SCRIPT_VERSION, os.getcwd())) - -def shell_process_cb(data, command, rc, stdout, stderr): - """Callback for hook_process().""" - global cmd_hook_process, cmd_buffer, cmd_stdout, cmd_stderr, cmd_send_to_buffer - cmd_stdout += stdout - cmd_stderr += stderr - if int(rc) >= 0: - if cmd_stdout: - lines = cmd_stdout.rstrip().split('\n') - if cmd_send_to_buffer == 'current': - for line in lines: - weechat.command(cmd_buffer, '%s' % line) - else: - weechat.prnt(cmd_buffer, '') - if cmd_send_to_buffer != 'new': - weechat.prnt(cmd_buffer, '%sCommand "%s" (rc %d), stdout:' - % (SHELL_PREFIX, data, int(rc))) - for line in lines: - weechat.prnt(cmd_buffer, ' \t%s' % line) - if cmd_stderr: - lines = cmd_stderr.rstrip().split('\n') - if cmd_send_to_buffer == 'current': - for line in lines: - weechat.command(cmd_buffer, '%s' % line) - else: - weechat.prnt(cmd_buffer, '') - if cmd_send_to_buffer != 'new': - weechat.prnt(cmd_buffer, '%s%sCommand "%s" (rc %d), stderr:' - % (weechat.prefix('error'), SHELL_PREFIX, data, int(rc))) - for line in lines: - weechat.prnt(cmd_buffer, '%s%s' % (weechat.prefix('error'), line)) - cmd_hook_process = '' - shell_set_title() - return weechat.WEECHAT_RC_OK - -def shell_show_process(buffer): - """Show running process.""" - global cmd_command, cmd_start_time - if cmd_hook_process: - weechat.prnt(buffer, '%sprocess running: "%s" (started on %s)' - % (SHELL_PREFIX, cmd_command, cmd_start_time.ctime())) - else: - weechat.prnt(buffer, '%sno process running' % SHELL_PREFIX) - -def shell_kill_process(buffer): - """Kill running process.""" - global cmd_hook_process, cmd_command - if cmd_hook_process: - weechat.unhook(cmd_hook_process) - weechat.prnt(buffer, '%sprocess killed (command "%s")' % (SHELL_PREFIX, cmd_command)) - shell_init() - else: - weechat.prnt(buffer, '%sno process running' % SHELL_PREFIX) - -def shell_chdir(buffer, directory): - """Change working directory.""" - if not directory: - if os.environ.has_key('HOME'): - directory = os.environ['HOME'] - try: - os.chdir(directory) - except: - weechat.prnt(buffer, '%san error occured while running command "cd %s"' % (SHELL_PREFIX, directory)) - else: - weechat.prnt(buffer, '%schdir to "%s" ok, new path: %s' % (SHELL_PREFIX, directory, os.getcwd())) - shell_set_title() - -def shell_getenv(buffer, var): - """Get environment variable.""" - global cmd_send_to_buffer - var = var.strip() - if not var: - weechat.prnt(buffer, '%swrong syntax, try "getenv VAR"' % (SHELL_PREFIX)) - return - - value = os.getenv(var) - if value == None: - weechat.prnt(buffer, '%s$%s is not set' % (SHELL_PREFIX, var)) - else: - if cmd_send_to_buffer == 'current': - weechat.command(buffer, '$%s=%s' % (var, os.getenv(var))) - else: - weechat.prnt(buffer, '%s$%s=%s' % (SHELL_PREFIX, var, os.getenv(var))) - -def shell_setenv(buffer, expr): - """Set an environment variable.""" - global cmd_send_to_buffer - expr = expr.strip() - lexpr = expr.split('=') - - if (len(lexpr) < 2): - weechat.prnt(buffer, '%swrong syntax, try "setenv VAR=VALUE"' % (SHELL_PREFIX)) - return - - os.environ[lexpr[0].strip()] = '='.join(lexpr[1:]) - if cmd_send_to_buffer != 'current': - weechat.prnt(buffer, '%s$%s is now set to "%s"' % (SHELL_PREFIX, lexpr[0], '='.join(lexpr[1:]))) - -def shell_unsetenv(buffer, var): - """Remove environment variable.""" - var = var.strip() - if not var: - weechat.prnt(buffer, '%swrong syntax, try "unsetenv VAR"' % (SHELL_PREFIX)) - return - - if os.environ.has_key(var): - del os.environ[var] - weechat.prnt(buffer, '%s$%s is now unset' % (SHELL_PREFIX, var)) - else: - weechat.prnt(buffer, '%s$%s is not set' % (SHELL_PREFIX, var)) - -def shell_exec(buffer, command): - """Execute a command.""" - global cmd_hook_process, cmd_command, cmd_start_time, cmd_buffer - global cmd_stdout, cmd_stderr, cmd_send_to_buffer, cmd_timeout - if cmd_hook_process: - weechat.prnt(buffer, - '%sanother process is running! (use "/%s -kill" to kill it)' - % (SHELL_PREFIX, SHELL_CMD)) - return - if cmd_send_to_buffer == 'new': - weechat.prnt(buffer, '-->\t%s%s$ %s%s' - % (weechat.color('chat_buffer'), os.getcwd(), weechat.color('reset'), command)) - weechat.prnt(buffer, '') - args = command.split(' ') - if args[0] == 'cd': - shell_chdir(buffer, ' '.join(args[1:])) - elif args[0] == 'getenv': - shell_getenv (buffer, ' '.join(args[1:])) - elif args[0] == 'setenv': - shell_setenv (buffer, ' '.join(args[1:])) - elif args[0] == 'unsetenv': - shell_unsetenv (buffer, ' '.join(args[1:])) - else: - shell_init() - cmd_command = command - cmd_start_time = datetime.datetime.now() - cmd_buffer = buffer - version = weechat.info_get("version_number", "") or 0 - if int(version) >= 0x00040000: - cmd_hook_process = weechat.hook_process_hashtable('sh', { 'arg1': '-c', 'arg2': command }, - cmd_timeout * 1000, 'shell_process_cb', command) - else: - cmd_hook_process = weechat.hook_process("sh -c '%s'" % command, cmd_timeout * 1000, 'shell_process_cb', command) - -def shell_input_buffer(data, buffer, input): - """Input callback on shell buffer.""" - global cmd_send_to_buffer - if input in ('q', 'Q'): - weechat.buffer_close(buffer) - return weechat.WEECHAT_RC_OK - cmd_send_to_buffer = 'new' - weechat.prnt(buffer, '') - command = weechat.string_input_for_buffer(input) - shell_exec(buffer, command) - return weechat.WEECHAT_RC_OK - -def shell_close_buffer(data, buffer): - """Close callback on shell buffer.""" - global cmd_shell_buffer - cmd_shell_buffer = '' - return weechat.WEECHAT_RC_OK - -def shell_new_buffer(): - """Create shell buffer.""" - global cmd_shell_buffer - cmd_shell_buffer = weechat.buffer_search('python', 'shell') - if not cmd_shell_buffer: - cmd_shell_buffer = weechat.buffer_new('shell', 'shell_input_buffer', '', 'shell_close_buffer', '') - if cmd_shell_buffer: - shell_set_title() - weechat.buffer_set(cmd_shell_buffer, 'localvar_set_no_log', '1') - weechat.buffer_set(cmd_shell_buffer, 'time_for_each_line', '0') - weechat.buffer_set(cmd_shell_buffer, 'input_get_unknown_commands', '1') - weechat.buffer_set(cmd_shell_buffer, 'display', '1') - return cmd_shell_buffer - -def shell_cmd(data, buffer, args): - """Callback for /shell command.""" - global cmd_send_to_buffer, cmd_timeout - largs = args.split(' ') - - # strip spaces - while '' in largs: - largs.remove('') - while ' ' in largs: - largs.remove(' ') - - cmdbuf = buffer - - if len(largs) == 0: - shell_new_buffer() - else: - if largs[0] == '-show': - shell_show_process(cmdbuf) - elif largs[0] == '-kill': - shell_kill_process(cmdbuf) - else: - cmd_send_to_buffer = '' - cmd_timeout = 0 - while largs: - if largs[0] == '-o': - cmd_send_to_buffer = 'current' - largs = largs[1:] - continue - if largs[0] == '-n': - cmd_send_to_buffer = 'new' - cmdbuf = shell_new_buffer() - largs = largs[1:] - continue - if largs[0] == '-t' and len(largs) > 2: - cmd_timeout = int(largs[1]) - largs = largs[2:] - continue - break - if len(largs) > 0: - shell_exec(cmdbuf, ' '.join(largs)) - return weechat.WEECHAT_RC_OK diff --git a/python/shortenurl.py b/python/shortenurl.py index e3a10860..dfb548d6 100644 --- a/python/shortenurl.py +++ b/python/shortenurl.py @@ -14,6 +14,22 @@ # along with this program. If not, see . # History +# 2019-10-10, CrazyCat +# version 0.6.6: fix trouble of "b'" +# : fix short_own=off bug when user is @,% or + +# 2019-10-01, Cian Butler +# version 0.6.5: make script compatible with Python 3 +# 2019-02-20, Jochen Saalfeld +# version 0.6.4: Fix is.gd URL pulling +# (fix displaying of shortened URL for is.gd) +# 2018-11-02, Jochen Saalfeld +# version 0.6.3: Fix is.gd URL pattern +# (api.php is depricated) +# 2018-07-12, Daniel Karbach +# version 0.6.2: Fix is.gd URL pattern +# (longurl param is appended by urlencode) +# 2017-05-04, Jochen Saalfeld +# version 0.6.1: Fix support for is.gd, since the API changed # 2014-08-18, Ilkka Laukkanen # version 0.6: Add support for bit.ly via Python Bitly # (https://code.google.com/p/python-bitly/) @@ -35,16 +51,20 @@ import re import weechat -from urllib import urlencode -from urllib2 import urlopen +try: + from urllib.parse import urlencode + from urllib.request import build_opener +except ImportError: + from urllib import urlencode + from urllib2 import build_opener SCRIPT_NAME = "shortenurl" SCRIPT_AUTHOR = "John Anderson " -SCRIPT_VERSION = "0.6.0" +SCRIPT_VERSION = "0.6.6" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Shorten long incoming and outgoing URLs" -ISGD = 'http://is.gd/api.php?%s' +ISGD = 'https://is.gd/create.php?format=simple&%s' TINYURL = 'http://tinyurl.com/api-create.php?%s' # script options @@ -78,7 +98,7 @@ if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - for option, default_value in settings.iteritems(): + for option, default_value in settings.items(): if weechat.config_get_plugin(option) == "": weechat.config_set_plugin(option, default_value) @@ -91,6 +111,7 @@ def notify(data, buf, date, tags, displayed, hilight, prefix, msg): reset = weechat.color('reset') my_nick = weechat.buffer_get_string(buf, 'localvar_nick') + prefix = re.sub(r'^[@%+~]', r'', prefix) if prefix != my_nick: urls = find_and_process_urls(msg) @@ -138,11 +159,13 @@ def get_shortened_url(url): history = 1 if weechat.config_get_plugin('bitly_add_to_history') == 'true' else 0 return api.shorten(url, {'history':history}) if shortener == 'isgd': - url = ISGD % urlencode({'longurl': url}) + url = ISGD % urlencode({'url': url}) if shortener == 'tinyurl': url = TINYURL % urlencode({'url': url}) try: - return urlopen(url).read() + opener = build_opener() + opener.addheaders = [('User-Agent', 'weechat')] + return opener.open(url).read().decode('utf-8') except: return url diff --git a/python/slack.py b/python/slack.py index fab8a079..3fdc37f7 100644 --- a/python/slack.py +++ b/python/slack.py @@ -1,2434 +1,7552 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2014-1016 Ryan Huber +# Copyright (c) 2014-2016 Ryan Huber +# Copyright (c) 2015-2018 Tollef Fog Heen +# Copyright (c) 2015-2023 Trygve Aaberge # Released under the MIT license. -# -from functools import wraps +from __future__ import print_function, unicode_literals + +from collections import OrderedDict, namedtuple +from datetime import date, datetime, timedelta +from functools import partial, wraps +from io import StringIO +from itertools import chain, count, islice + +import copy +import errno +import textwrap import time import json +import hashlib import os -import pickle -import sha import re -import urllib -import HTMLParser import sys import traceback -import collections import ssl +import random +import socket +import string + +# Prevent websocket from using numpy (it's an optional dependency). We do this +# because numpy causes python (and thus weechat) to crash when it's reloaded. +# See https://github.com/numpy/numpy/issues/11925 +sys.modules["numpy"] = None + +from websocket import ( # noqa: E402 + ABNF, + create_connection, + WebSocketConnectionClosedException, +) + +try: + basestring # Python 2 + unicode + str = unicode +except NameError: # Python 3 + basestring = unicode = str + +try: + from collections.abc import ( + ItemsView, + Iterable, + KeysView, + Mapping, + Reversible, + ValuesView, + ) +except ImportError: + from collections import ItemsView, Iterable, KeysView, Mapping, ValuesView + + Reversible = object -from websocket import create_connection,WebSocketConnectionClosedException +try: + from urllib.parse import quote, unquote, urlencode +except ImportError: + from urllib import quote, unquote, urlencode + +try: + JSONDecodeError = json.JSONDecodeError +except AttributeError: + JSONDecodeError = ValueError # hack to make tests possible.. better way? try: - import weechat as w -except: + import weechat +except ImportError: pass SCRIPT_NAME = "slack" -SCRIPT_AUTHOR = "Ryan Huber " -SCRIPT_VERSION = "0.99.10" +SCRIPT_AUTHOR = "Trygve Aaberge " +SCRIPT_VERSION = "2.11.0" SCRIPT_LICENSE = "MIT" -SCRIPT_DESC = "Extends weechat for typing notification/search/etc on slack.com" +SCRIPT_DESC = "Extends WeeChat for typing notification/search/etc on slack.com" +REPO_URL = "https://github.com/wee-slack/wee-slack" -BACKLOG_SIZE = 200 -SCROLLBACK_SIZE = 500 +TYPING_DURATION = 6 -CACHE_VERSION = "4" +RECORD_DIR = "/tmp/weeslack-debug" SLACK_API_TRANSLATOR = { "channel": { - "history": "channels.history", - "join": "channels.join", - "leave": "channels.leave", - "mark": "channels.mark", - "info": "channels.info", + "history": "conversations.history", + "join": "conversations.join", + "leave": "conversations.leave", + "mark": "conversations.mark", + "info": "conversations.info", }, "im": { - "history": "im.history", - "join": "im.open", - "leave": "im.close", - "mark": "im.mark", + "history": "conversations.history", + "join": "conversations.open", + "leave": "conversations.close", + "mark": "conversations.mark", + "info": "conversations.info", + }, + "mpim": { + "history": "conversations.history", + "join": "conversations.open", + "leave": "conversations.close", + "mark": "conversations.mark", + "info": "conversations.info", }, "group": { - "history": "groups.history", - "join": "channels.join", - "leave": "groups.leave", - "mark": "groups.mark", - } - + "history": "conversations.history", + "join": "conversations.join", + "leave": "conversations.leave", + "mark": "conversations.mark", + "info": "conversations.info", + }, + "private": { + "history": "conversations.history", + "join": "conversations.join", + "leave": "conversations.leave", + "mark": "conversations.mark", + "info": "conversations.info", + }, + "shared": { + "history": "conversations.history", + "join": "conversations.join", + "leave": "conversations.leave", + "mark": "conversations.mark", + "info": "conversations.info", + }, + "thread": { + "history": None, + "join": None, + "leave": None, + "mark": "subscriptions.thread.mark", + }, } -NICK_GROUP_HERE = "0|Here" -NICK_GROUP_AWAY = "1|Away" +CONFIG_PREFIX = "plugins.var.python." + SCRIPT_NAME -sslopt_ca_certs = {} -if hasattr(ssl, "get_default_verify_paths") and callable(ssl.get_default_verify_paths): - ssl_defaults = ssl.get_default_verify_paths() - if ssl_defaults.cafile is not None: - sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile} +###### Decorators have to be up here -def dbg(message, fout=False, main_buffer=False): + +def slack_buffer_or_ignore(f): """ - send debug output to the slack-debug buffer and optionally write to a file. + Only run this function if we're in a slack buffer, else ignore """ - message = "DEBUG: {}".format(message) - #message = message.encode('utf-8', 'replace') - if fout: - file('/tmp/debug.log', 'a+').writelines(message + '\n') - if main_buffer: - w.prnt("", "slack: " + message) - else: - if slack_debug is not None: - w.prnt(slack_debug, message) + @wraps(f) + def wrapper(data, current_buffer, *args, **kwargs): + if current_buffer not in EVENTROUTER.weechat_controller.buffers: + return w.WEECHAT_RC_OK + return f(data, current_buffer, *args, **kwargs) -class SearchList(list): - """ - A normal python list with some syntactic sugar for searchability - """ - def __init__(self): - self.hashtable = {} - super(SearchList, self).__init__(self) - - def find(self, name): - if name in self.hashtable: - return self.hashtable[name] - #this is a fallback to __eq__ if the item isn't in the hashtable already - if self.count(name) > 0: - self.update_hashtable() - return self[self.index(name)] - - def append(self, item, aliases=[]): - super(SearchList, self).append(item) - self.update_hashtable() - - def update_hashtable(self): - for child in self: - if hasattr(child, "get_aliases"): - for alias in child.get_aliases(): - if alias is not None: - self.hashtable[alias] = child - - def find_by_class(self, class_name): - items = [] - for child in self: - if child.__class__ == class_name: - items.append(child) - return items - - def find_by_class_deep(self, class_name, attribute): - items = [] - for child in self: - if child.__class__ == self.__class__: - items += child.find_by_class_deep(class_name, attribute) - else: - items += (eval('child.' + attribute).find_by_class(class_name)) - return items + return wrapper -class SlackServer(object): +def slack_buffer_required(f): """ - Root object used to represent connection and state of the connection to a slack group. + Only run this function if we're in a slack buffer, else print error """ - def __init__(self, token): - self.nick = None - self.name = None - self.team = None - self.domain = None - self.server_buffer_name = None - self.login_data = None - self.buffer = None - self.token = token - self.ws = None - self.ws_hook = None - self.users = SearchList() - self.bots = SearchList() - self.channels = SearchList() - self.connecting = False - self.connected = False - self.connection_attempt_time = 0 - self.communication_counter = 0 - self.message_buffer = {} - self.ping_hook = None - self.alias = None - self.identifier = None - self.connect_to_slack() + @wraps(f) + def wrapper(data, current_buffer, *args, **kwargs): + if current_buffer not in EVENTROUTER.weechat_controller.buffers: + command_name = f.__name__.replace("command_", "", 1) + w.prnt( + "", + 'slack: command "{}" must be executed on slack buffer'.format( + command_name + ), + ) + return w.WEECHAT_RC_ERROR + return f(data, current_buffer, *args, **kwargs) - def __eq__(self, compare_str): - if compare_str == self.identifier or compare_str == self.token or compare_str == self.buffer: - return True - else: - return False + return wrapper - def __str__(self): - return "{}".format(self.identifier) - def __repr__(self): - return "{}".format(self.identifier) +def utf8_decode(f): + """ + Decode all arguments from byte strings to unicode strings. Use this for + functions called from outside of this script, e.g. callbacks from WeeChat. + """ - def add_user(self, user): - self.users.append(user, user.get_aliases()) - users.append(user, user.get_aliases()) + @wraps(f) + def wrapper(*args, **kwargs): + return f(*decode_from_utf8(args), **decode_from_utf8(kwargs)) - def add_bot(self, bot): - self.bots.append(bot) + return wrapper - def add_channel(self, channel): - self.channels.append(channel, channel.get_aliases()) - channels.append(channel, channel.get_aliases()) - def get_aliases(self): - aliases = filter(None, [self.identifier, self.token, self.buffer, self.alias]) - return aliases +NICK_GROUP_HERE = "0|Here" +NICK_GROUP_AWAY = "1|Away" +NICK_GROUP_EXTERNAL = "2|External" - def find(self, name, attribute): - attribute = eval("self." + attribute) - return attribute.find(name) +sslopt_ca_certs = {} +if hasattr(ssl, "get_default_verify_paths") and callable(ssl.get_default_verify_paths): + ssl_defaults = ssl.get_default_verify_paths() + if ssl_defaults.cafile is not None: + sslopt_ca_certs = {"ca_certs": ssl_defaults.cafile} - def get_communication_id(self): - if self.communication_counter > 999: - self.communication_counter = 0 - self.communication_counter += 1 - return self.communication_counter +EMOJI = {} +EMOJI_WITH_SKIN_TONES_REVERSE = {} - def send_to_websocket(self, data, expect_reply=True): - data["id"] = self.get_communication_id() - message = json.dumps(data) - try: - if expect_reply: - self.message_buffer[data["id"]] = data - self.ws.send(message) - dbg("Sent {}...".format(message[:100])) - except: - dbg("Unexpected error: {}\nSent: {}".format(sys.exc_info()[0], data)) - self.connected = False +###### Unicode handling - def ping(self): - request = {"type": "ping"} - self.send_to_websocket(request) - def should_connect(self): - """ - If we haven't tried to connect OR we tried and never heard back and it - has been 125 seconds consider the attempt dead and try again - """ - if self.connection_attempt_time == 0 or self.connection_attempt_time + 125 < int(time.time()): - return True +def encode_to_utf8(data): + if sys.version_info.major > 2: + return data + elif isinstance(data, unicode): + return data.encode("utf-8") + if isinstance(data, bytes): + return data + elif isinstance(data, Mapping): + return type(data)(map(encode_to_utf8, data.items())) + elif isinstance(data, Iterable): + return type(data)(map(encode_to_utf8, data)) + else: + return data + + +def decode_from_utf8(data): + if sys.version_info.major > 2: + return data + elif isinstance(data, bytes): + return data.decode("utf-8") + if isinstance(data, unicode): + return data + elif isinstance(data, Mapping): + return type(data)(map(decode_from_utf8, data.items())) + elif isinstance(data, Iterable): + return type(data)(map(decode_from_utf8, data)) + else: + return data + + +class WeechatWrapper(object): + def __init__(self, wrapped_class): + self.wrapped_class = wrapped_class + + # Helper method used to encode/decode method calls. + def wrap_for_utf8(self, method): + def hooked(*args, **kwargs): + result = method(*encode_to_utf8(args), **encode_to_utf8(kwargs)) + # Prevent wrapped_class from becoming unwrapped + if result == self.wrapped_class: + return self + return decode_from_utf8(result) + + return hooked + + # Encode and decode everything sent to/received from weechat. We use the + # unicode type internally in wee-slack, but has to send utf8 to weechat. + def __getattr__(self, attr): + orig_attr = self.wrapped_class.__getattribute__(attr) + if callable(orig_attr): + return self.wrap_for_utf8(orig_attr) else: - return False - - def connect_to_slack(self): - t = time.time() - #Double check that we haven't exceeded a long wait to connect and try again. - if self.connecting and self.should_connect(): - self.connecting = False - if not self.connecting: - async_slack_api_request("slack.com", self.token, "rtm.start", {"ts": t}) - self.connection_attempt_time = int(time.time()) - self.connecting = True - - def connected_to_slack(self, login_data): - if login_data["ok"]: - self.team = login_data["team"]["domain"] - self.domain = login_data["team"]["domain"] + ".slack.com" - dbg("connected to {}".format(self.domain)) - self.identifier = self.domain - - alias = w.config_get_plugin("server_alias.{}".format(login_data["team"]["domain"])) - if alias: - self.server_buffer_name = alias - self.alias = alias + return decode_from_utf8(orig_attr) + + # Ensure all lines sent to weechat specifies a prefix. For lines after the + # first, we want to disable the prefix, which we do by specifying the same + # number of spaces, so it aligns correctly. + def prnt_date_tags(self, buffer, date, tags, message): + if weechat_version < 0x04000000: + prefix, _, _ = message.partition("\t") + prefix = weechat.string_remove_color(encode_to_utf8(prefix), "") + prefix_spaces = " " * weechat.strlen_screen(prefix) + message = message.replace("\n", "\n{}\t".format(prefix_spaces)) + return self.wrap_for_utf8(self.wrapped_class.prnt_date_tags)( + buffer, date, tags, message + ) + + +class ProxyWrapper(object): + def __init__(self): + self.proxy_name = w.config_string(w.config_get("weechat.network.proxy_curl")) + self.proxy_string = "" + self.proxy_type = "" + self.proxy_address = "" + self.proxy_port = "" + self.proxy_user = "" + self.proxy_password = "" + self.has_proxy = False + + if self.proxy_name: + self.proxy_string = "weechat.proxy.{}".format(self.proxy_name) + self.proxy_type = w.config_string( + w.config_get("{}.type".format(self.proxy_string)) + ) + if self.proxy_type == "http": + self.proxy_address = w.config_string( + w.config_get("{}.address".format(self.proxy_string)) + ) + self.proxy_port = w.config_integer( + w.config_get("{}.port".format(self.proxy_string)) + ) + self.proxy_user = w.config_string( + w.config_get("{}.username".format(self.proxy_string)) + ) + self.proxy_password = w.config_string( + w.config_get("{}.password".format(self.proxy_string)) + ) + self.has_proxy = True else: - self.server_buffer_name = self.domain - - self.nick = login_data["self"]["name"] - self.create_local_buffer() - - if self.create_slack_websocket(login_data): - if self.ping_hook: - w.unhook(self.ping_hook) - self.communication_counter = 0 - self.ping_hook = w.hook_timer(1000 * 5, 0, 0, "slack_ping_cb", self.domain) - if len(self.users) == 0 or len(self.channels) == 0: - self.create_slack_mappings(login_data) - - self.connected = True - self.connecting = False - - self.print_connection_info(login_data) - if len(self.message_buffer) > 0: - for message_id in self.message_buffer.keys(): - if self.message_buffer[message_id]["type"] != 'ping': - resend = self.message_buffer.pop(message_id) - dbg("Resent failed message.") - self.send_to_websocket(resend) - #sleep to prevent being disconnected by websocket server - time.sleep(1) - else: - self.message_buffer.pop(message_id) - return True + w.prnt( + "", + "\nWarning: weechat.network.proxy_curl is set to {} type (name : {}, conf string : {}). Only HTTP proxy is supported.\n\n".format( + self.proxy_type, self.proxy_name, self.proxy_string + ), + ) + + def curl(self): + if not self.has_proxy: + return "" + + if self.proxy_user and self.proxy_password: + user = "{}:{}@".format(self.proxy_user, self.proxy_password) else: - token_start = self.token[:10] - error = """ -!! slack.com login error: {} - The problematic token starts with {} - Please check your API token with - "/set plugins.var.python.slack_extension.slack_api_token (token)" - -""".format(login_data["error"], token_start) - w.prnt("", error) - self.connected = False - - def print_connection_info(self, login_data): - self.buffer_prnt('Connected to Slack', backlog=True) - self.buffer_prnt('{:<20} {}'.format("Websocket URL", login_data["url"]), backlog=True) - self.buffer_prnt('{:<20} {}'.format("User name", login_data["self"]["name"]), backlog=True) - self.buffer_prnt('{:<20} {}'.format("User ID", login_data["self"]["id"]), backlog=True) - self.buffer_prnt('{:<20} {}'.format("Team name", login_data["team"]["name"]), backlog=True) - self.buffer_prnt('{:<20} {}'.format("Team domain", login_data["team"]["domain"]), backlog=True) - self.buffer_prnt('{:<20} {}'.format("Team id", login_data["team"]["id"]), backlog=True) - - def create_local_buffer(self): - if not w.buffer_search("", self.server_buffer_name): - self.buffer = w.buffer_new(self.server_buffer_name, "buffer_input_cb", "", "", "") - if w.config_string(w.config_get('irc.look.server_buffer')) == 'merge_with_core': - w.buffer_merge(self.buffer, w.buffer_search_main()) - w.buffer_set(self.buffer, "nicklist", "1") - - def create_slack_websocket(self, data): - web_socket_url = data['url'] - try: - self.ws = create_connection(web_socket_url, sslopt=sslopt_ca_certs) - self.ws_hook = w.hook_fd(self.ws.sock._sock.fileno(), 1, 0, 0, "slack_websocket_cb", self.identifier) - self.ws.sock.setblocking(0) - return True - except Exception as e: - print("websocket connection error: {}".format(e)) - return False + user = "" - def create_slack_mappings(self, data): - - for item in data["users"]: - self.add_user(User(self, item["name"], item["id"], item["presence"], item["deleted"], is_bot=item.get('is_bot', False))) - - for item in data["bots"]: - self.add_bot(Bot(self, item["name"], item["id"], item["deleted"])) - - for item in data["channels"]: - if "last_read" not in item: - item["last_read"] = 0 - if "members" not in item: - item["members"] = [] - if "topic" not in item: - item["topic"] = {} - item["topic"]["value"] = "" - if not item["is_archived"]: - self.add_channel(Channel(self, item["name"], item["id"], item["is_member"], item["last_read"], "#", item["members"], item["topic"]["value"])) - for item in data["groups"]: - if "last_read" not in item: - item["last_read"] = 0 - if not item["is_archived"]: - if item["name"].startswith("mpdm-"): - self.add_channel(MpdmChannel(self, item["name"], item["id"], item["is_open"], item["last_read"], "#", item["members"], item["topic"]["value"])) - else: - self.add_channel(GroupChannel(self, item["name"], item["id"], item["is_open"], item["last_read"], "#", item["members"], item["topic"]["value"])) - for item in data["ims"]: - if "last_read" not in item: - item["last_read"] = 0 - if item["unread_count"] > 0: - item["is_open"] = True - name = self.users.find(item["user"]).name - self.add_channel(DmChannel(self, name, item["id"], item["is_open"], item["last_read"])) - for item in data['self']['prefs']['muted_channels'].split(','): - if item == '': - continue - if self.channels.find(item) is not None: - self.channels.find(item).muted = True + if self.proxy_port: + port = ":{}".format(self.proxy_port) + else: + port = "" - for item in self.channels: - item.get_history() + return "-x{}{}{}".format(user, self.proxy_address, port) - def buffer_prnt(self, message='no message', user="SYSTEM", backlog=False): - message = message.encode('ascii', 'ignore') - if backlog: - tags = "no_highlight,notify_none,logger_backlog_end" - else: - tags = "" - if user == "SYSTEM": - user = w.config_string(w.config_get('weechat.look.prefix_network')) - if self.buffer: - w.prnt_date_tags(self.buffer, 0, tags, "{}\t{}".format(user, message)) - else: - pass - #w.prnt("", "%s\t%s" % (user, message)) -def buffer_input_cb(b, buffer, data): - channel = channels.find(buffer) - if not channel: - return w.WEECHAT_RC_OK_EAT - reaction = re.match("(\d*)(\+|-):(.*):", data) - if not reaction and not data.startswith('s/'): - channel.send_message(data) - #channel.buffer_prnt(channel.server.nick, data) - elif reaction: - if reaction.group(2) == "+": - channel.send_add_reaction(int(reaction.group(1) or 1), reaction.group(3)) - elif reaction.group(2) == "-": - channel.send_remove_reaction(int(reaction.group(1) or 1), reaction.group(3)) - elif data.count('/') == 3: - old, new = data.split('/')[1:3] - channel.change_previous_message(old.decode("utf-8"), new.decode("utf-8")) - channel.mark_read(True) - return w.WEECHAT_RC_ERROR - - -class Channel(object): - """ - Represents a single channel and is the source of truth - for channel <> weechat buffer - """ - def __init__(self, server, name, identifier, active, last_read=0, prepend_name="", members=[], topic=""): - self.name = prepend_name + name - self.current_short_name = prepend_name + name - self.identifier = identifier - self.active = active - self.last_read = float(last_read) - self.members = set(members) - self.topic = topic +class MappingReversible(Mapping, Reversible): + def keys(self): + return KeysViewReversible(self) - self.members_table = {} - self.channel_buffer = None - self.type = "channel" - self.server = server - self.typing = {} - self.last_received = None - self.messages = [] - self.scrolling = False - self.last_active_user = None - self.muted = False - if active: - self.create_buffer() - self.attach_buffer() - self.create_members_table() - self.update_nicklist() - self.set_topic(self.topic) - buffer_list_update_next() + def items(self): + return ItemsViewReversible(self) - def __str__(self): - return self.name + def values(self): + return ValuesViewReversible(self) - def __repr__(self): - return self.name - def __eq__(self, compare_str): - if compare_str == self.fullname() or compare_str == self.name or compare_str == self.identifier or compare_str == self.name[1:] or (compare_str == self.channel_buffer and self.channel_buffer is not None): - return True - else: - return False +class KeysViewReversible(KeysView, Reversible): + def __reversed__(self): + return reversed(self._mapping) - def get_aliases(self): - aliases = [self.fullname(), self.name, self.identifier, self.name[1:], ] - if self.channel_buffer is not None: - aliases.append(self.channel_buffer) - return aliases - def create_members_table(self): - for user in self.members: - self.members_table[user] = self.server.users.find(user) +class ItemsViewReversible(ItemsView, Reversible): + def __reversed__(self): + for key in reversed(self._mapping): + yield (key, self._mapping[key]) - def create_buffer(self): - channel_buffer = w.buffer_search("", "{}.{}".format(self.server.server_buffer_name, self.name)) - if channel_buffer: - self.channel_buffer = channel_buffer - else: - self.channel_buffer = w.buffer_new("{}.{}".format(self.server.server_buffer_name, self.name), "buffer_input_cb", self.name, "", "") - if self.type == "im": - w.buffer_set(self.channel_buffer, "localvar_set_type", 'private') - else: - w.buffer_set(self.channel_buffer, "localvar_set_type", 'channel') - if self.server.alias: - w.buffer_set(self.channel_buffer, "localvar_set_server", self.server.alias) - else: - w.buffer_set(self.channel_buffer, "localvar_set_server", self.server.team) - w.buffer_set(self.channel_buffer, "localvar_set_channel", self.name) - w.buffer_set(self.channel_buffer, "short_name", self.name) - buffer_list_update_next() - - def attach_buffer(self): - channel_buffer = w.buffer_search("", "{}.{}".format(self.server.server_buffer_name, self.name)) - if channel_buffer != main_weechat_buffer: - self.channel_buffer = channel_buffer - w.buffer_set(self.channel_buffer, "localvar_set_nick", self.server.nick) - w.buffer_set(self.channel_buffer, "highlight_words", self.server.nick) - else: - self.channel_buffer = None - channels.update_hashtable() - self.server.channels.update_hashtable() - def detach_buffer(self): - if self.channel_buffer is not None: - w.buffer_close(self.channel_buffer) - self.channel_buffer = None - channels.update_hashtable() - self.server.channels.update_hashtable() +class ValuesViewReversible(ValuesView, Reversible): + def __reversed__(self): + for key in reversed(self._mapping): + yield self._mapping[key] - def update_nicklist(self, user=None): - if not self.channel_buffer: - return - w.buffer_set(self.channel_buffer, "nicklist", "1") +##### Helpers - #create nicklists for the current channel if they don't exist - #if they do, use the existing pointer - here = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_HERE) - if not here: - here = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_HERE, "weechat.color.nicklist_group", 1) - afk = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_AWAY) - if not afk: - afk = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_AWAY, "weechat.color.nicklist_group", 1) - if user: - user = self.members_table[user] - nick = w.nicklist_search_nick(self.channel_buffer, "", user.name) - #since this is a change just remove it regardless of where it is - w.nicklist_remove_nick(self.channel_buffer, nick) - #now add it back in to whichever.. - if user.presence == 'away': - w.nicklist_add_nick(self.channel_buffer, afk, user.name, user.color_name, "", "", 1) - else: - w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1) +def colorize_string(color, string, reset_color="reset"): + if color: + return w.color(color) + string + w.color(reset_color) + else: + return string - #if we didn't get a user, build a complete list. this is expensive. - else: - try: - for user in self.members: - user = self.members_table[user] - if user.deleted: - continue - if user.presence == 'away': - w.nicklist_add_nick(self.channel_buffer, afk, user.name, user.color_name, "", "", 1) - else: - w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1) - except Exception as e: - dbg("DEBUG: {} {} {}".format(self.identifier, self.name, e)) - def fullname(self): - return "{}.{}".format(self.server.server_buffer_name, self.name) +def print_error(message, buffer="", warning=False): + prefix = "Warning" if warning else "Error" + w.prnt(buffer, "{}{}: {}".format(w.prefix("error"), prefix, message)) - def has_user(self, name): - return name in self.members - def user_join(self, name): - self.members.add(name) - self.create_members_table() - self.update_nicklist() +def print_message_not_found_error(msg_id): + if msg_id: + print_error( + "Invalid id given, must be an existing id or a number greater " + + "than 0 and less than the number of messages in the channel" + ) + else: + print_error("No messages found in channel") - def user_leave(self, name): - if name in self.members: - self.members.remove(name) - self.create_members_table() - self.update_nicklist() - def set_active(self): - self.active = True +def token_for_print(token): + return "{}...{}".format(token[:15], token[-10:]) - def set_inactive(self): - self.active = False - def set_typing(self, user): - if self.channel_buffer: - if w.buffer_get_integer(self.channel_buffer, "hidden") == 0: - self.typing[user] = time.time() - buffer_list_update_next() +def format_exc_tb(): + return decode_from_utf8(traceback.format_exc()) - def unset_typing(self, user): - if self.channel_buffer: - if w.buffer_get_integer(self.channel_buffer, "hidden") == 0: - try: - del self.typing[user] - buffer_list_update_next() - except: - pass - - def send_message(self, message): - message = self.linkify_text(message) - dbg(message) - request = {"type": "message", "channel": self.identifier, "text": message, "_server": self.server.domain} - self.server.send_to_websocket(request) - - def linkify_text(self, message): - message = message.split(' ') - for item in enumerate(message): - targets = re.match('.*([@#])([\w.]+\w)(\W*)', item[1]) - if targets and targets.groups()[0] == '@': - named = targets.groups() - if named[1] in ["group", "channel", "here"]: - message[item[0]] = "".format(named[1]) - if self.server.users.find(named[1]): - message[item[0]] = "<@{}>{}".format(self.server.users.find(named[1]).identifier, named[2]) - if targets and targets.groups()[0] == '#': - named = targets.groups() - if self.server.channels.find(named[1]): - message[item[0]] = "<#{}|{}>{}".format(self.server.channels.find(named[1]).identifier, named[1], named[2]) - dbg(message) - return " ".join(message) - - def set_topic(self, topic): - self.topic = topic.encode('utf-8') - w.buffer_set(self.channel_buffer, "title", self.topic) - def open(self, update_remote=True): - self.create_buffer() - self.active = True - self.get_history() - if "info" in SLACK_API_TRANSLATOR[self.type]: - async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["info"], {"name": self.name.lstrip("#")}) - if update_remote: - if "join" in SLACK_API_TRANSLATOR[self.type]: - async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["join"], {"name": self.name.lstrip("#")}) - async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["join"], {"user": users.find(self.name).identifier}) - - def close(self, update_remote=True): - #remove from cache so messages don't reappear when reconnecting - if self.active: - self.active = False - self.current_short_name = "" - self.detach_buffer() - if update_remote: - t = time.time() - async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["leave"], {"channel": self.identifier}) +def format_exc_only(): + etype, value, _ = sys.exc_info() + return "".join(decode_from_utf8(traceback.format_exception_only(etype, value))) - def closed(self): - self.channel_buffer = None - self.last_received = None - self.close() - def is_someone_typing(self): - for user in self.typing.keys(): - if self.typing[user] + 4 > time.time(): - return True - if len(self.typing) > 0: - self.typing = {} - buffer_list_update_next() - return False +def url_encode_if_not_encoded(value): + decoded = unquote(value) + is_encoded = value != decoded + if is_encoded: + return value + else: + return quote(value) - def get_typing_list(self): - typing = [] - for user in self.typing.keys(): - if self.typing[user] + 4 > time.time(): - typing.append(user) - return typing - def mark_read(self, update_remote=True): - t = time.time() +def get_localvar_type(slack_type): + if slack_type in ("im", "mpim"): + return "private" + else: + return "channel" - if self.channel_buffer: - w.buffer_set(self.channel_buffer, "unread", "") - if update_remote: - self.last_read = time.time() - self.update_read_marker(self.last_read) - def update_read_marker(self, time): - async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["mark"], {"channel": self.identifier, "ts": time}) +def get_nick_color(nick): + return w.info_get("nick_color_name", nick) - def rename(self): - if self.is_someone_typing(): - new_name = ">{}".format(self.name[1:]) - else: - new_name = self.name - if self.channel_buffer: - if self.current_short_name != new_name: - self.current_short_name = new_name - w.buffer_set(self.channel_buffer, "short_name", new_name) - def buffer_prnt(self, user='unknown_user', message='no message', time=0): - """ - writes output (message) to a buffer (channel) - """ - set_read_marker = False - time_float = float(time) - tags = "nick_" + user - # XXX: we should not set log1 for robots. - if time_float != 0 and self.last_read >= time_float: - tags += ",no_highlight,notify_none,logger_backlog_end" - set_read_marker = True - elif message.find(self.server.nick.encode('utf-8')) > -1: - tags = ",notify_highlight,log1" - elif user != self.server.nick and self.name in self.server.users: - tags = ",notify_private,notify_message,log1,irc_privmsg" - elif self.muted: - tags = ",no_highlight,notify_none,logger_backlog_end" - elif user in [x.strip() for x in w.prefix("join"), w.prefix("quit")]: - tags = ",irc_smart_filter" - else: - tags = ",notify_message,log1,irc_privmsg" - #don't write these to local log files - #tags += ",no_log" - time_int = int(time_float) - if self.channel_buffer: - prefix_same_nick = w.config_string(w.config_get('weechat.look.prefix_same_nick')) - if user == self.last_active_user and prefix_same_nick != "": - if colorize_nicks and self.server.users.find(user): - name = self.server.users.find(user).color + prefix_same_nick - else: - name = prefix_same_nick - else: - nick_prefix = w.config_string(w.config_get('weechat.look.nick_prefix')) - nick_suffix = w.config_string(w.config_get('weechat.look.nick_suffix')) +def get_thread_color(thread_id): + if config.color_thread_suffix == "multiple": + return get_nick_color(thread_id) + else: + return config.color_thread_suffix - if self.server.users.find(user): - name = self.server.users.find(user).formatted_name() - self.last_active_user = user - # XXX: handle bots properly here. - else: - name = user - self.last_active_user = None - name = nick_prefix + name + nick_suffix - name = name.decode('utf-8') - #colorize nicks in each line - chat_color = w.config_string(w.config_get('weechat.color.chat')) - if type(message) is not unicode: - message = message.decode('UTF-8', 'replace') - curr_color = w.color(chat_color) - if colorize_nicks and colorize_messages and self.server.users.find(user): - curr_color = self.server.users.find(user).color - message = curr_color + message - for user in self.server.users: - if user.name in message: - message = user.name_regex.sub( - r'\1\2{}\3'.format(user.formatted_name() + curr_color), - message) - - message = HTMLParser.HTMLParser().unescape(message) - data = u"{}\t{}".format(name, message).encode('utf-8') - w.prnt_date_tags(self.channel_buffer, time_int, tags, data) - - if set_read_marker: - self.mark_read(False) - else: - self.open(False) - self.last_received = time - self.unset_typing(user) - def buffer_redraw(self): - if self.channel_buffer and not self.scrolling: - w.buffer_clear(self.channel_buffer) - self.messages.sort() - for message in self.messages: - process_message(message.message_json, False) +def sha1_hex(s): + return str(hashlib.sha1(s.encode("utf-8")).hexdigest()) - def set_scrolling(self): - self.scrolling = True - def unset_scrolling(self): - self.scrolling = False +def get_functions_with_prefix(prefix): + return { + name[len(prefix) :]: ref + for name, ref in globals().items() + if name.startswith(prefix) + } - def has_message(self, ts): - return self.messages.count(ts) > 0 - def change_message(self, ts, text=None, suffix=''): - if self.has_message(ts): - message_index = self.messages.index(ts) +def handle_socket_error(exception, team, caller_name): + if not ( + isinstance(exception, WebSocketConnectionClosedException) + or exception.errno in (errno.EPIPE, errno.ECONNRESET, errno.ETIMEDOUT) + ): + raise + + w.prnt( + team.channel_buffer, + "Lost connection to slack team {} (on {}), reconnecting.".format( + team.domain, caller_name + ), + ) + dbg( + "Socket failed on {} with exception:\n{}".format(caller_name, format_exc_tb()), + level=5, + ) + team.set_disconnected() + + +MESSAGE_ID_REGEX_STRING = r"(?P\d+|\$[0-9a-fA-F]{3,})" +REACTION_PREFIX_REGEX_STRING = r"{}?(?P\+|-)".format( + MESSAGE_ID_REGEX_STRING +) + +EMOJI_CHAR_REGEX_STRING = "(?P[\U00000080-\U0010ffff]+)" +EMOJI_NAME_REGEX_STRING = ":(?P[a-z0-9_+-]+):" +EMOJI_CHAR_OR_NAME_REGEX_STRING = "({}|{})".format( + EMOJI_CHAR_REGEX_STRING, EMOJI_NAME_REGEX_STRING +) +EMOJI_NAME_REGEX = re.compile(EMOJI_NAME_REGEX_STRING) +EMOJI_CHAR_OR_NAME_REGEX = re.compile(EMOJI_CHAR_OR_NAME_REGEX_STRING) + + +def regex_match_to_emoji(match, include_name=False): + emoji = match.group(1) + full_match = match.group() + char = EMOJI.get(emoji, full_match) + if include_name and char != full_match: + return "{} ({})".format(char, full_match) + return char + + +def replace_string_with_emoji(text): + if config.render_emoji_as_string == "both": + return EMOJI_NAME_REGEX.sub( + partial(regex_match_to_emoji, include_name=True), + text, + ) + elif config.render_emoji_as_string: + return text + return EMOJI_NAME_REGEX.sub(regex_match_to_emoji, text) - if text is not None: - self.messages[message_index].change_text(text) - text = render_message(self.messages[message_index].message_json, True) - #if there is only one message with this timestamp, modify it directly. - #we do this because time resolution in weechat is less than slack - int_time = int(float(ts)) - if self.messages.count(str(int_time)) == 1: - modify_buffer_line(self.channel_buffer, text + suffix, int_time) - #otherwise redraw the whole buffer, which is expensive - else: - self.buffer_redraw() - return True +def replace_emoji_with_string(text): + emoji = None + key = text + while emoji is None and len(key): + emoji = EMOJI_WITH_SKIN_TONES_REVERSE.get(key) + key = key[:-1] + return emoji or text - def add_reaction(self, ts, reaction, user): - if self.has_message(ts): - message_index = self.messages.index(ts) - self.messages[message_index].add_reaction(reaction, user) - self.change_message(ts) - return True - def remove_reaction(self, ts, reaction, user): - if self.has_message(ts): - message_index = self.messages.index(ts) - self.messages[message_index].remove_reaction(reaction, user) - self.change_message(ts) - return True +###### New central Event router - def send_add_reaction(self, msg_number, reaction): - self.send_change_reaction("reactions.add", msg_number, reaction) - def send_remove_reaction(self, msg_number, reaction): - self.send_change_reaction("reactions.remove", msg_number, reaction) +class EventRouter(object): + def __init__(self): + """ + complete + Eventrouter is the central hub we use to route: + 1) incoming websocket data + 2) outgoing http requests and incoming replies + 3) local requests + It has a recorder that, when enabled, logs most events + to the location specified in RECORD_DIR. + """ + self.queue = [] + self.slow_queue = [] + self.slow_queue_timer = 0 + self.teams = {} + self.subteams = {} + self.context = {} + self.weechat_controller = WeechatController(self) + self.previous_buffer = "" + self.reply_buffer = {} + self.cmds = get_functions_with_prefix("command_") + self.proc = get_functions_with_prefix("process_") + self.handlers = get_functions_with_prefix("handle_") + self.local_proc = get_functions_with_prefix("local_process_") + self.shutting_down = False + self.recording = False + self.recording_path = "/tmp" + self.handle_next_hook = None + self.handle_next_hook_interval = -1 + + def record(self): + """ + complete + Toggles the event recorder and creates a directory for data if enabled. + """ + self.recording = not self.recording + if self.recording: + if not os.path.exists(RECORD_DIR): + os.makedirs(RECORD_DIR) - def send_change_reaction(self, method, msg_number, reaction): - if 0 < msg_number < len(self.messages): - timestamp = self.messages[-msg_number].message_json["ts"] - data = {"channel": self.identifier, "timestamp": timestamp, "name": reaction} - async_slack_api_request(self.server.domain, self.server.token, method, data) + def record_event(self, message_json, team, file_name_field, subdir=None): + """ + complete + Called each time you want to record an event. + message_json is a json in dict form + file_name_field is the json key whose value you want to be part of the file name + """ + now = time.time() - def change_previous_message(self, old, new): - message = self.my_last_message() - if new == "" and old == "": - async_slack_api_request(self.server.domain, self.server.token, 'chat.delete', {"channel": self.identifier, "ts": message['ts']}) + if team: + team_subdomain = team.subdomain else: - new_message = message["text"].replace(old, new) - async_slack_api_request(self.server.domain, self.server.token, 'chat.update', {"channel": self.identifier, "ts": message['ts'], "text": new_message.encode("utf-8")}) - - def my_last_message(self): - for message in reversed(self.messages): - if "user" in message.message_json and "text" in message.message_json and message.message_json["user"] == self.server.users.find(self.server.nick).identifier: - return message.message_json - - def cache_message(self, message_json, from_me=False): - if from_me: - message_json["user"] = self.server.users.find(self.server.nick).identifier - self.messages.append(Message(message_json)) - if len(self.messages) > SCROLLBACK_SIZE: - self.messages = self.messages[-SCROLLBACK_SIZE:] - - def get_history(self): - if self.active: - for message in message_cache[self.identifier]: - process_message(json.loads(message), True) - if self.last_received != None: - async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["history"], {"channel": self.identifier, "oldest": self.last_received, "count": BACKLOG_SIZE}) + team_json = message_json.get("team") + if team_json: + team_subdomain = team_json.get("domain") else: - async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["history"], {"channel": self.identifier, "count": BACKLOG_SIZE}) - + team_subdomain = "unknown_team" + + directory = "{}/{}".format(RECORD_DIR, team_subdomain) + if subdir: + directory = "{}/{}".format(directory, subdir) + if not os.path.exists(directory): + os.makedirs(directory) + mtype = message_json.get(file_name_field, "unknown") + f = open("{}/{}-{}.json".format(directory, now, mtype), "w") + f.write("{}".format(json.dumps(message_json))) + f.close() + + def store_context(self, data): + """ + A place to store data and vars needed by callback returns. We need this because + WeeChat's "callback_data" has a limited size and WeeChat will crash if you exceed + this size. + """ + identifier = "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(40) + ) + self.context[identifier] = data + dbg("stored context {} {} ".format(identifier, data.url)) + return identifier + + def retrieve_context(self, identifier): + """ + A place to retrieve data and vars needed by callback returns. We need this because + WeeChat's "callback_data" has a limited size and WeeChat will crash if you exceed + this size. + """ + return self.context.get(identifier) -class GroupChannel(Channel): + def delete_context(self, identifier): + """ + Requests can span multiple requests, so we may need to delete this as a last step + """ + if identifier in self.context: + del self.context[identifier] - def __init__(self, server, name, identifier, active, last_read=0, prepend_name="", members=[], topic=""): - super(GroupChannel, self).__init__(server, name, identifier, active, last_read, prepend_name, members, topic) - self.type = "group" + def shutdown(self): + """ + complete + This toggles shutdown mode. Shutdown mode tells us not to + talk to Slack anymore. Without this, typing /quit will trigger + a race with the buffer close callback and may result in you + leaving every slack channel. + """ + self.shutting_down = not self.shutting_down -class MpdmChannel(Channel): + def register_team(self, team): + """ + complete + Adds a team to the list of known teams for this EventRouter. + """ + if isinstance(team, SlackTeam): + self.teams[team.get_team_hash()] = team + else: + raise InvalidType(type(team)) + + def reconnect_if_disconnected(self): + for team in self.teams.values(): + time_since_last_ping = time.time() - team.last_ping_time + time_since_last_pong = time.time() - team.last_pong_time + if ( + team.connected + and time_since_last_ping < 5 + and time_since_last_pong > 30 + ): + w.prnt( + team.channel_buffer, + "Lost connection to slack team {} (no pong), reconnecting.".format( + team.domain + ), + ) + team.set_disconnected() + if not team.connected: + team.connect() + dbg("reconnecting {}".format(team)) + + @utf8_decode + def receive_ws_callback(self, team_hash, fd): + """ + This is called by the global method of the same name. + It is triggered when we have incoming data on a websocket, + which needs to be read. Once it is read, we will ensure + the data is valid JSON, add metadata, and place it back + on the queue for processing as JSON. + """ + team = self.teams[team_hash] + while True: + try: + # Read the data from the websocket associated with this team. + opcode, data = team.ws.recv_data(control_frame=True) + except ssl.SSLWantReadError: + # No more data to read at this time. + return w.WEECHAT_RC_OK + except (WebSocketConnectionClosedException, socket.error) as e: + handle_socket_error(e, team, "receive") + return w.WEECHAT_RC_OK + + if opcode == ABNF.OPCODE_PONG: + team.last_pong_time = time.time() + return w.WEECHAT_RC_OK + elif opcode != ABNF.OPCODE_TEXT: + return w.WEECHAT_RC_OK + + message_json = json.loads(data.decode("utf-8")) + if self.recording: + self.record_event(message_json, team, "type", "websocket") + message_json["wee_slack_metadata_team"] = team + self.receive(message_json) + + def http_check_ratelimited(self, request_metadata, response): + parts = response.split("\r\n\r\nHTTP/") + last_header_part, body = parts[-1].split("\r\n\r\n", 1) + header_lines = last_header_part.split("\r\n") + http_status = header_lines[0].split(" ")[1] + + if http_status == "429": + for header in header_lines[1:]: + name, value = header.split(":", 1) + if name.lower() == "retry-after": + retry_after = int(value.strip()) + request_metadata.retry_time = time.time() + retry_after + return "", "ratelimited" + + return body, "" + + def retry_request(self, request_metadata, data, return_code, err): + self.reply_buffer.pop(request_metadata.response_id, None) + self.delete_context(data) + retry_text = ( + "retrying" + if request_metadata.should_try() + else "will not retry after too many failed attempts" + ) + team = ( + "for team {}".format(request_metadata.team) + if request_metadata.team + else "with token {}".format(token_for_print(request_metadata.token)) + ) + w.prnt( + "", + ( + "Failed requesting {} {}, {}. " + + "If this persists, try increasing slack_timeout. Error (code {}): {}" + ).format( + request_metadata.request, + team, + retry_text, + return_code, + err, + ), + ) + dbg( + "{} failed with return_code {} and error {}. stack:\n{}".format( + request_metadata.request, + return_code, + err, + "".join(traceback.format_stack()), + ), + level=5, + ) + self.receive(request_metadata) + + @utf8_decode + def receive_httprequest_callback(self, data, command, return_code, out, err): + """ + complete + Receives the result of an http request we previously handed + off to WeeChat (WeeChat bundles libcurl). WeeChat can fragment + replies, so it buffers them until the reply is complete. + It is then populated with metadata here so we can identify + where the request originated and route properly. + """ + request_metadata = self.retrieve_context(data) + dbg( + "RECEIVED CALLBACK with request of {} id of {} and code {} of length {}".format( + request_metadata.request, + request_metadata.response_id, + return_code, + len(out), + ) + ) + if return_code == 0: + if len(out) > 0: + if request_metadata.response_id not in self.reply_buffer: + self.reply_buffer[request_metadata.response_id] = StringIO() + self.reply_buffer[request_metadata.response_id].write(out) + + response = self.reply_buffer[request_metadata.response_id].getvalue() + body, error = self.http_check_ratelimited(request_metadata, response) + if error: + self.retry_request(request_metadata, data, return_code, error) + else: + j = json.loads(body) + + try: + j[ + "wee_slack_process_method" + ] = request_metadata.request_normalized + if self.recording: + self.record_event( + j, + request_metadata.team, + "wee_slack_process_method", + "http", + ) + j["wee_slack_request_metadata"] = request_metadata + self.reply_buffer.pop(request_metadata.response_id) + self.receive(j) + self.delete_context(data) + except: + dbg("HTTP REQUEST CALLBACK FAILED", True) + # We got an empty reply and this is weird so just ditch it and retry + else: + dbg("length was zero, probably a bug..") + self.delete_context(data) + self.receive(request_metadata) + elif return_code == -1: + if request_metadata.response_id not in self.reply_buffer: + self.reply_buffer[request_metadata.response_id] = StringIO() + self.reply_buffer[request_metadata.response_id].write(out) + else: + self.retry_request(request_metadata, data, return_code, err) + return w.WEECHAT_RC_OK - def __init__(self, server, name, identifier, active, last_read=0, prepend_name="", members=[], topic=""): - name = ",".join("-".join(name.split("-")[1:-1]).split("--")) - super(MpdmChannel, self).__init__(server, name, identifier, active, last_read, prepend_name, members, topic) - self.type = "group" + def receive(self, dataobj, slow=False): + """ + Receives a raw object and places it on the queue for + processing. Object must be known to handle_next or + be JSON. + """ + dbg("RECEIVED FROM QUEUE") + if slow: + self.slow_queue.append(dataobj) + else: + self.queue.append(dataobj) -class DmChannel(Channel): + def handle_next(self): + """ + complete + Main handler of the EventRouter. This is called repeatedly + via callback to drain events from the queue. It also attaches + useful metadata and context to events as they are processed. + """ + wanted_interval = 100 + if len(self.slow_queue) > 0 or len(self.queue) > 0: + wanted_interval = 10 + if ( + self.handle_next_hook is None + or wanted_interval != self.handle_next_hook_interval + ): + if self.handle_next_hook: + w.unhook(self.handle_next_hook) + self.handle_next_hook = w.hook_timer( + wanted_interval, 0, 0, "handle_next", "" + ) + self.handle_next_hook_interval = wanted_interval + + if len(self.slow_queue) > 0 and ((self.slow_queue_timer + 1) < time.time()): + dbg("from slow queue", 0) + self.queue.append(self.slow_queue.pop()) + self.slow_queue_timer = time.time() + if len(self.queue) > 0: + j = self.queue.pop(0) + # Reply is a special case of a json reply from websocket. + if isinstance(j, SlackRequest): + if j.should_try(): + if j.retry_ready(): + local_process_async_slack_api_request(j, self) + else: + self.slow_queue.append(j) + else: + dbg("Max retries for Slackrequest") - def __init__(self, server, name, identifier, active, last_read=0, prepend_name=""): - super(DmChannel, self).__init__(server, name, identifier, active, last_read, prepend_name) - self.type = "im" + else: + if "reply_to" in j: + dbg("SET FROM REPLY") + function_name = "reply" + elif "type" in j: + dbg("SET FROM type") + function_name = j["type"] + elif "wee_slack_process_method" in j: + dbg("SET FROM META") + function_name = j["wee_slack_process_method"] + else: + dbg("SET FROM NADA") + function_name = "unknown" + + request = j.get("wee_slack_request_metadata") + if request: + team = request.team + channel = request.channel + metadata = request.metadata + callback = request.callback + else: + team = j.get("wee_slack_metadata_team") + channel = None + metadata = {} + callback = None + + if team: + if "channel" in j: + channel_id = ( + j["channel"]["id"] + if isinstance(j["channel"], dict) + else j["channel"] + ) + channel = team.channels.get(channel_id, channel) + if "user" in j: + user_id = ( + j["user"]["id"] + if isinstance(j["user"], dict) + else j["user"] + ) + metadata["user"] = team.users.get(user_id) + + dbg("running {}".format(function_name)) + if callable(callback): + callback(j, self, team, channel, metadata) + elif ( + function_name.startswith("local_") + and function_name in self.local_proc + ): + self.local_proc[function_name](j, self, team, channel, metadata) + elif function_name in self.proc: + self.proc[function_name](j, self, team, channel, metadata) + elif function_name in self.handlers: + self.handlers[function_name](j, self, team, channel, metadata) + else: + dbg("Callback not implemented for event: {}".format(function_name)) - def rename(self): - global colorize_private_chats - if self.server.users.find(self.name).presence == "active": - new_name = self.server.users.find(self.name).formatted_name('+', colorize_private_chats) +def handle_next(data, remaining_calls): + try: + EVENTROUTER.handle_next() + except: + if config.debug_mode: + traceback.print_exc() else: - new_name = self.server.users.find(self.name).formatted_name(' ', colorize_private_chats) - - if self.channel_buffer: - if self.current_short_name != new_name: - self.current_short_name = new_name - w.buffer_set(self.channel_buffer, "short_name", new_name) + pass + return w.WEECHAT_RC_OK - def update_nicklist(self, user=None): - pass -class User(object): +class WeechatController(object): + """ + Encapsulates our interaction with WeeChat + """ - def __init__(self, server, name, identifier, presence="away", deleted=False, is_bot=False): - self.server = server - self.name = name - self.identifier = identifier - self.deleted = deleted - self.presence = presence + def __init__(self, eventrouter): + self.eventrouter = eventrouter + self.buffers = {} + self.previous_buffer = None - self.channel_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, self.name)) - self.update_color() - self.name_regex = re.compile(r"([\W]|\A)(@{0,1})" + self.name + "('s|[^'\w]|\Z)") - self.is_bot = is_bot + def iter_buffers(self): + for b in self.buffers: + yield (b, self.buffers[b]) - if deleted: - return - self.nicklist_pointer = w.nicklist_add_nick(server.buffer, "", self.name, self.color_name, "", "", 1) - if self.presence == 'away': - w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "0") + def register_buffer(self, buffer_ptr, channel): + """ + complete + Adds a WeeChat buffer to the list of handled buffers for this EventRouter + """ + if isinstance(buffer_ptr, basestring): + self.buffers[buffer_ptr] = channel else: - w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "1") -# w.nicklist_add_nick(server.buffer, "", self.formatted_name(), "", "", "", 1) + raise InvalidType(type(buffer_ptr)) - def __str__(self): - return self.name + def unregister_buffer(self, buffer_ptr, update_remote=False, close_buffer=False): + """ + complete + Adds a WeeChat buffer to the list of handled buffers for this EventRouter + """ + channel = self.buffers.get(buffer_ptr) + if channel: + channel.destroy_buffer(update_remote) + del self.buffers[buffer_ptr] + if close_buffer: + w.buffer_close(buffer_ptr) - def __repr__(self): - return self.name + def get_channel_from_buffer_ptr(self, buffer_ptr): + return self.buffers.get(buffer_ptr) - def __eq__(self, compare_str): - try: - if compare_str == self.name or compare_str == "@" + self.name or compare_str == self.identifier: - return True - else: - return False - except: - return False + def get_all(self, buffer_ptr): + return self.buffers - def get_aliases(self): - return [self.name, "@" + self.name, self.identifier] + def get_previous_buffer_ptr(self): + return self.previous_buffer - def set_active(self): - if self.deleted: - return + def set_previous_buffer(self, data): + self.previous_buffer = data - self.presence = "active" - for channel in self.server.channels: - if channel.has_user(self.identifier): - channel.update_nicklist(self.identifier) - w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "1") - dm_channel = self.server.channels.find(self.name) - if dm_channel and dm_channel.active: - buffer_list_update_next() - - def set_inactive(self): - if self.deleted: - return - self.presence = "away" - for channel in self.server.channels: - if channel.has_user(self.identifier): - channel.update_nicklist(self.identifier) - w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "0") - dm_channel = self.server.channels.find(self.name) - if dm_channel and dm_channel.active: - buffer_list_update_next() +###### New Local Processors - def update_color(self): - if colorize_nicks: - if self.name == self.server.nick: - self.color_name = w.config_string(w.config_get('weechat.color.chat_nick_self')) - else: - self.color_name = w.info_get('irc_nick_color_name', self.name) - self.color = w.color(self.color_name) - else: - self.color = "" - self.color_name = "" - def formatted_name(self, prepend="", enable_color=True): - if colorize_nicks and enable_color: - print_color = self.color - else: - print_color = "" - return print_color + prepend + self.name +def local_process_async_slack_api_request(request, event_router): + """ + complete + Sends an API request to Slack. You'll need to give this a well formed SlackRequest object. + DEBUGGING!!! The context here cannot be very large. WeeChat will crash. + """ + if not event_router.shutting_down: + weechat_request = "url:{}".format(request.request_string()) + weechat_request += "&nonce={}".format( + "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(4) + ) + ) + request.tried() + options = request.options() + options["header"] = "1" + context = event_router.store_context(request) + w.hook_process_hashtable( + weechat_request, + options, + config.slack_timeout, + "receive_httprequest_callback", + context, + ) + + +###### New Callbacks + + +@utf8_decode +def ws_ping_cb(data, remaining_calls): + for team in EVENTROUTER.teams.values(): + if team.ws and team.connected: + try: + team.ws.ping() + team.last_ping_time = time.time() + except (WebSocketConnectionClosedException, socket.error) as e: + handle_socket_error(e, team, "ping") + return w.WEECHAT_RC_OK - def create_dm_channel(self): - async_slack_api_request(self.server.domain, self.server.token, "im.open", {"user": self.identifier}) -class Bot(object): +@utf8_decode +def reconnect_callback(*args): + EVENTROUTER.reconnect_if_disconnected() + return w.WEECHAT_RC_OK - def __init__(self, server, name, identifier, deleted=False): - self.server = server - self.name = name - self.identifier = identifier - self.deleted = deleted - self.update_color() - def __eq__(self, compare_str): - if compare_str == self.identifier or compare_str == self.name: - return True +@utf8_decode +def buffer_renamed_cb(data, signal, current_buffer): + channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) + if ( + isinstance(channel, SlackChannelCommon) + and not channel.buffer_rename_in_progress + ): + if w.buffer_get_string(channel.channel_buffer, "old_full_name"): + channel.label_full_drop_prefix = True + channel.label_full = w.buffer_get_string(channel.channel_buffer, "name") else: - return False + channel.label_short_drop_prefix = True + channel.label_short = w.buffer_get_string( + channel.channel_buffer, "short_name" + ) - def __str__(self): - return "{}".format(self.identifier) + channel.rename() + return w.WEECHAT_RC_OK - def __repr__(self): - return "{}".format(self.identifier) - def update_color(self): - if colorize_nicks: - self.color_name = w.info_get('irc_nick_color_name', self.name.encode('utf-8')) - self.color = w.color(self.color_name) - else: - self.color_name = "" - self.color = "" +@utf8_decode +def buffer_closing_callback(data, signal, current_buffer): + """ + Receives a callback from WeeChat when a buffer is being closed. + """ + EVENTROUTER.weechat_controller.unregister_buffer(current_buffer, True, False) + return w.WEECHAT_RC_OK + + +@utf8_decode +def buffer_input_callback(signal, buffer_ptr, data): + """ + incomplete + Handles everything a user types in the input bar. In our case + this includes add/remove reactions, modifying messages, and + sending messages. + """ + if weechat_version < 0x2090000: + data = data.replace("\r", "\n") + eventrouter = eval(signal) + channel = eventrouter.weechat_controller.get_channel_from_buffer_ptr(buffer_ptr) + if not channel: + return w.WEECHAT_RC_ERROR + + reaction = re.match( + r"{}{}\s*$".format( + REACTION_PREFIX_REGEX_STRING, EMOJI_CHAR_OR_NAME_REGEX_STRING + ), + data, + ) + substitute = re.match("{}?s/".format(MESSAGE_ID_REGEX_STRING), data) + if reaction: + emoji = reaction.group("emoji_char") or reaction.group("emoji_name") + if reaction.group("reaction_change") == "+": + channel.send_add_reaction(reaction.group("msg_id"), emoji) + elif reaction.group("reaction_change") == "-": + channel.send_remove_reaction(reaction.group("msg_id"), emoji) + elif substitute: + try: + old, new, flags = re.split(r"(? ">channel" and + user presence via " name" <-> "+name". + """ + + for buf in EVENTROUTER.weechat_controller.buffers.values(): + buf.refresh() + return w.WEECHAT_RC_OK + + +def quit_notification_callback(data, signal, args): + stop_talking_to_slack() + return w.WEECHAT_RC_OK + + +@utf8_decode +def typing_notification_cb(data, signal, current_buffer): + msg = w.buffer_get_string(current_buffer, "input") + if len(msg) > 8 and msg[0] != "/": + global typing_timer + now = time.time() + if typing_timer + 4 < now: + channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) + if channel and channel.type != "thread": + identifier = channel.identifier + request = {"type": "typing", "channel": identifier} + channel.team.send_to_websocket(request, expect_reply=False) + typing_timer = now + return w.WEECHAT_RC_OK + + +@utf8_decode +def typing_update_cb(data, remaining_calls): + w.bar_item_update("slack_typing_notice") + return w.WEECHAT_RC_OK + + +@utf8_decode +def slack_never_away_cb(data, remaining_calls): + if config.never_away: + for team in EVENTROUTER.teams.values(): + set_own_presence_active(team) + return w.WEECHAT_RC_OK + + +@utf8_decode +def typing_bar_item_cb(data, item, current_window, current_buffer, extra_info): + """ + Privides a bar item indicating who is typing in the current channel AND + why is typing a DM to you globally. + """ + typers = [] + current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) + + # first look for people typing in this channel + if current_channel: + # this try is mostly becuase server buffers don't implement is_someone_typing + try: + if current_channel.type != "im" and current_channel.is_someone_typing(): + typers += current_channel.get_typing_list() + except: + pass + + # here is where we notify you that someone is typing in DM + # regardless of which buffer you are in currently + for team in EVENTROUTER.teams.values(): + for channel in team.channels.values(): + if channel.type == "im": + if channel.is_someone_typing(): + typers.append("D/" + channel.name) + + typing = ", ".join(typers) + if typing != "": + typing = colorize_string(config.color_typing_notice, "typing: " + typing) + + return typing + + +@utf8_decode +def away_bar_item_cb(data, item, current_window, current_buffer, extra_info): + channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) + if not channel: + return "" + + if channel.team.is_user_present(channel.team.myidentifier): + return "" + else: + away_color = w.config_string(w.config_get("weechat.color.item_away")) + if channel.team.my_manual_presence == "away": + return colorize_string(away_color, "manual away") + else: + return colorize_string(away_color, "auto away") + + +@utf8_decode +def channel_completion_cb(data, completion_item, current_buffer, completion): + """ + Adds all channels on all teams to completion list + """ + current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) + should_include_channel = lambda channel: channel.active and channel.type in [ + "channel", + "group", + "private", + "shared", + ] + + other_teams = [ + team + for team in EVENTROUTER.teams.values() + if not current_channel or team != current_channel.team + ] + for team in other_teams: + for channel in team.channels.values(): + if should_include_channel(channel): + completion_list_add( + completion, channel.name, 0, w.WEECHAT_LIST_POS_SORT + ) + + if current_channel: + for channel in sorted( + current_channel.team.channels.values(), + key=lambda channel: channel.name, + reverse=True, + ): + if should_include_channel(channel): + completion_list_add( + completion, channel.name, 0, w.WEECHAT_LIST_POS_BEGINNING + ) + + if should_include_channel(current_channel): + completion_list_add( + completion, current_channel.name, 0, w.WEECHAT_LIST_POS_BEGINNING + ) + return w.WEECHAT_RC_OK + + +@utf8_decode +def dm_completion_cb(data, completion_item, current_buffer, completion): + """ + Adds all dms/mpdms on all teams to completion list + """ + for team in EVENTROUTER.teams.values(): + for channel in team.channels.values(): + if channel.active and channel.type in ["im", "mpim"]: + completion_list_add( + completion, channel.name, 0, w.WEECHAT_LIST_POS_SORT + ) + return w.WEECHAT_RC_OK + + +@utf8_decode +def nick_completion_cb(data, completion_item, current_buffer, completion): + """ + Adds all @-prefixed nicks to completion list + """ + current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) + if current_channel is None or current_channel.members is None: + return w.WEECHAT_RC_OK + + base_command = completion_get_string(completion, "base_command") + if base_command in ["invite", "msg", "query", "whois"]: + members = current_channel.team.members + else: + members = current_channel.members + + for member in members: + user = current_channel.team.users.get(member) + if user and not user.deleted: + completion_list_add(completion, user.name, 1, w.WEECHAT_LIST_POS_SORT) + completion_list_add(completion, "@" + user.name, 1, w.WEECHAT_LIST_POS_SORT) + return w.WEECHAT_RC_OK + + +@utf8_decode +def emoji_completion_cb(data, completion_item, current_buffer, completion): + """ + Adds all :-prefixed emoji to completion list + """ + current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) + if current_channel is None: + return w.WEECHAT_RC_OK + + base_word = completion_get_string(completion, "base_word") + reaction = re.match(REACTION_PREFIX_REGEX_STRING + ":", base_word) + prefix = reaction.group(0) if reaction else ":" + + for emoji in current_channel.team.emoji_completions: + completion_list_add( + completion, prefix + emoji + ":", 0, w.WEECHAT_LIST_POS_SORT + ) + return w.WEECHAT_RC_OK + + +@utf8_decode +def thread_completion_cb(data, completion_item, current_buffer, completion): + """ + Adds all $-prefixed thread ids to completion list + """ + current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) + if current_channel is None or not hasattr(current_channel, "hashed_messages"): + return w.WEECHAT_RC_OK + + threads = ( + x for x in current_channel.hashed_messages.items() if isinstance(x[0], str) + ) + for thread_id, message_ts in sorted(threads, key=lambda item: item[1]): + message = current_channel.messages.get(message_ts) + if message and message.number_of_replies(): + completion_list_add( + completion, "$" + thread_id, 0, w.WEECHAT_LIST_POS_BEGINNING + ) + return w.WEECHAT_RC_OK + + +@utf8_decode +def topic_completion_cb(data, completion_item, current_buffer, completion): + """ + Adds topic for current channel to completion list + """ + current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) + if current_channel is None: + return w.WEECHAT_RC_OK + + topic = current_channel.render_topic() + channel_names = [channel.name for channel in current_channel.team.channels.values()] + if topic.split(" ", 1)[0] in channel_names: + topic = "{} {}".format(current_channel.name, topic) + + completion_list_add(completion, topic, 0, w.WEECHAT_LIST_POS_SORT) + return w.WEECHAT_RC_OK + + +@utf8_decode +def usergroups_completion_cb(data, completion_item, current_buffer, completion): + """ + Adds all @-prefixed usergroups to completion list + """ + current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) + if current_channel is None: + return w.WEECHAT_RC_OK + + subteam_handles = [ + subteam.handle for subteam in current_channel.team.subteams.values() + ] + for group in subteam_handles + ["@channel", "@everyone", "@here"]: + completion_list_add(completion, group, 1, w.WEECHAT_LIST_POS_SORT) + return w.WEECHAT_RC_OK + + +@utf8_decode +def complete_next_cb(data, current_buffer, command): + """Extract current word, if it is equal to a nick, prefix it with @ and + rely on nick_completion_cb adding the @-prefixed versions to the + completion lists, then let WeeChat's internal completion do its + thing + """ + current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) + if ( + not hasattr(current_channel, "members") + or current_channel is None + or current_channel.members is None + ): + return w.WEECHAT_RC_OK + + line_input = w.buffer_get_string(current_buffer, "input") + current_pos = w.buffer_get_integer(current_buffer, "input_pos") - 1 + input_length = w.buffer_get_integer(current_buffer, "input_length") + + word_start = 0 + word_end = input_length + # If we're on a non-word, look left for something to complete + while ( + current_pos >= 0 + and line_input[current_pos] != "@" + and not line_input[current_pos].isalnum() + ): + current_pos = current_pos - 1 + if current_pos < 0: + current_pos = 0 + for l in range(current_pos, 0, -1): + if line_input[l] != "@" and not line_input[l].isalnum(): + word_start = l + 1 + break + for l in range(current_pos, input_length): + if not line_input[l].isalnum(): + word_end = l + break + word = line_input[word_start:word_end] + + for member in current_channel.members: + user = current_channel.team.users.get(member) + if user and user.name == word: + # Here, we cheat. Insert a @ in front and rely in the @ + # nicks being in the completion list + w.buffer_set( + current_buffer, + "input", + line_input[:word_start] + "@" + line_input[word_start:], + ) + w.buffer_set( + current_buffer, + "input_pos", + str(w.buffer_get_integer(current_buffer, "input_pos") + 1), + ) + return w.WEECHAT_RC_OK_EAT + return w.WEECHAT_RC_OK + + +def script_unloaded(): + stop_talking_to_slack() + return w.WEECHAT_RC_OK + + +def stop_talking_to_slack(): + """ + complete + Prevents a race condition where quitting closes buffers + which triggers leaving the channel because of how close + buffer is handled + """ + if "EVENTROUTER" in globals(): + EVENTROUTER.shutdown() + for team in EVENTROUTER.teams.values(): + team.ws.shutdown() + return w.WEECHAT_RC_OK + + +##### New Classes + + +class SlackRequest(object): + """ + Encapsulates a Slack api request. Valuable as an object that we can add to the queue and/or retry. + makes a SHA of the requst url and current time so we can re-tag this on the way back through. + """ + + def __init__( + self, + team, + request, + post_data=None, + channel=None, + metadata=None, + retries=3, + token=None, + cookies=None, + callback=None, + ): + if team is None and token is None: + raise ValueError("Both team and token can't be None") + self.team = team + self.request = request + self.post_data = post_data if post_data else {} + self.channel = channel + self.metadata = metadata if metadata else {} + self.retries = retries + self.retry_time = 0 + self.token = token if token else team.token + self.cookies = cookies or {} + if ":" in self.token: + token, cookie = self.token.split(":", 1) + self.token = token + if cookie.startswith("d="): + for name, value in [c.split("=") for c in cookie.split(";")]: + self.cookies[name] = value + else: + self.cookies["d"] = cookie + self.callback = callback + self.domain = "api.slack.com" + self.reset() + + def reset(self): + self.tries = 0 + self.start_time = time.time() + self.request_normalized = re.sub(r"\W+", "", self.request) + self.url = "https://{}/api/{}?{}".format( + self.domain, self.request, urlencode(encode_to_utf8(self.post_data)) + ) + self.response_id = sha1_hex("{}{}".format(self.url, self.start_time)) + + def __repr__(self): + return ( + "SlackRequest(team={}, request='{}', post_data={}, retries={}, token='{}', " + "cookies={}, tries={}, start_time={})" + ).format( + self.team, + self.request, + self.post_data, + self.retries, + token_for_print(self.token), + self.cookies, + self.tries, + self.start_time, + ) + + def request_string(self): + return "{}".format(self.url) + + def options(self): + cookies = "; ".join( + [ + "{}={}".format(key, url_encode_if_not_encoded(value)) + for key, value in self.cookies.items() + ] + ) + return { + "useragent": "wee_slack {}".format(SCRIPT_VERSION), + "httpheader": "Authorization: Bearer {}".format(self.token), + "cookie": cookies, + } + + def options_as_cli_args(self): + options = self.options() + options["user-agent"] = options.pop("useragent") + httpheader = options.pop("httpheader") + headers = [": ".join(x) for x in options.items()] + httpheader.split("\n") + return ["-H{}".format(header) for header in headers] + + def tried(self): + self.tries += 1 + self.response_id = sha1_hex("{}{}".format(self.url, time.time())) + + def should_try(self): + return self.tries < self.retries + + def retry_ready(self): + if self.retry_time: + return time.time() > self.retry_time + else: + return (self.start_time + (self.tries**2)) < time.time() + + +class SlackSubteam(object): + """ + Represents a slack group or subteam + """ + + def __init__(self, originating_team_id, is_member, **kwargs): + self.handle = "@{}".format(kwargs["handle"]) + self.identifier = kwargs["id"] + self.name = kwargs["name"] + self.description = kwargs.get("description") + self.team_id = originating_team_id + self.is_member = is_member + + def __repr__(self): + return "Name:{} Identifier:{}".format(self.name, self.identifier) + + def __eq__(self, compare_str): + return compare_str == self.identifier + + +class SlackTeam(object): + """ + incomplete + Team object under which users and channels live.. Does lots. + """ + + def __init__( + self, + eventrouter, + token, + team_hash, + websocket_url, + team_info, + subteams, + nick, + myidentifier, + my_manual_presence, + users, + bots, + channels, + **kwargs + ): + self.slack_api_translator = copy.deepcopy(SLACK_API_TRANSLATOR) + self.identifier = team_info["id"] + self.type = "team" + self.active = True + self.team_hash = team_hash + self.ws_url = websocket_url + self.connected = False + self.connecting_rtm = False + self.connecting_ws = False + self.ws = None + self.ws_counter = 0 + self.ws_replies = {} + self.last_ping_time = 0 + self.last_pong_time = time.time() + self.eventrouter = eventrouter + self.token = token + self.team = self + self.subteams = subteams + self.team_info = team_info + self.subdomain = team_info["domain"] + self.domain = self.subdomain + ".slack.com" + self.set_name() + self.nick = nick + self.myidentifier = myidentifier + self.my_manual_presence = my_manual_presence + try: + if self.channels: + for c in channels.keys(): + if not self.channels.get(c): + self.channels[c] = channels[c] + except: + self.channels = channels + self.users = users + self.bots = bots + self.channel_buffer = None + self.got_history = True + self.history_needs_update = False + self.create_buffer() + self.set_muted_channels(kwargs.get("muted_channels", "")) + self.set_highlight_words(kwargs.get("highlight_words", "")) + for c in self.channels.keys(): + channels[c].set_related_server(self) + channels[c].check_should_open() + # Last step is to make sure my nickname is the set color + self.users[self.myidentifier].force_color( + w.config_string(w.config_get("weechat.color.chat_nick_self")) + ) + # This highlight step must happen after we have set related server + self.load_emoji_completions() + + def __repr__(self): + return "domain={} nick={}".format(self.subdomain, self.nick) + + def __eq__(self, compare_str): + return ( + compare_str == self.token + or compare_str == self.domain + or compare_str == self.subdomain + ) + + @property + def members(self): + return self.users.keys() + + def load_emoji_completions(self): + self.emoji_completions = list(EMOJI.keys()) + if self.emoji_completions: + s = SlackRequest(self, "emoji.list") + self.eventrouter.receive(s) + + def add_channel(self, channel): + self.channels[channel["id"]] = channel + channel.set_related_server(self) + + def generate_usergroup_map(self): + return {s.handle: s.identifier for s in self.subteams.values()} + + def set_name(self): + alias = config.server_aliases.get(self.subdomain) + if alias: + self.name = alias + elif config.short_buffer_names: + self.name = self.subdomain + else: + self.name = "slack.{}".format(self.subdomain) + + def create_buffer(self): + if not self.channel_buffer: + self.channel_buffer = w.buffer_new( + self.name, "buffer_input_callback", "EVENTROUTER", "", "" + ) + self.eventrouter.weechat_controller.register_buffer( + self.channel_buffer, self + ) + w.buffer_set(self.channel_buffer, "input_prompt", self.nick) + w.buffer_set(self.channel_buffer, "input_multiline", "1") + w.buffer_set(self.channel_buffer, "localvar_set_type", "server") + w.buffer_set(self.channel_buffer, "localvar_set_slack_type", self.type) + w.buffer_set(self.channel_buffer, "localvar_set_nick", self.nick) + w.buffer_set(self.channel_buffer, "localvar_set_server", self.name) + w.buffer_set( + self.channel_buffer, + "localvar_set_completion_default_template", + "${weechat.completion.default_template}|%(usergroups)|%(emoji)", + ) + self.buffer_merge() + + def buffer_merge(self, config_value=None): + if not config_value: + config_value = w.config_string(w.config_get("irc.look.server_buffer")) + if config_value == "merge_with_core": + w.buffer_merge(self.channel_buffer, w.buffer_search_main()) + else: + w.buffer_unmerge(self.channel_buffer, 0) + + def destroy_buffer(self, update_remote): + pass + + def set_muted_channels(self, muted_str): + self.muted_channels = {x for x in muted_str.split(",") if x} + for channel in self.channels.values(): + channel.set_highlights() + channel.rename() + + def set_highlight_words(self, highlight_str): + self.highlight_words = {x for x in highlight_str.split(",") if x} + for channel in self.channels.values(): + channel.set_highlights() + + def formatted_name(self): + return self.domain + + def buffer_prnt(self, data, message=False): + tag_name = "team_message" if message else "team_info" + ts = SlackTS() + w.prnt_date_tags(self.channel_buffer, ts.major, tag(ts, tag_name), data) + + def send_message(self, message, subtype=None, request_dict_ext={}): + w.prnt("", "ERROR: Sending a message in the team buffer is not supported") + + def find_channel_by_members(self, members, channel_type=None): + for channel in self.channels.values(): + if channel.members == members and ( + channel_type is None or channel.type == channel_type + ): + return channel + + def get_channel_map(self): + return {v.name: k for k, v in self.channels.items()} + + def get_username_map(self): + return {v.name: k for k, v in self.users.items()} + + def get_team_hash(self): + return self.team_hash + + @staticmethod + def generate_team_hash(team_id, subdomain): + return str(sha1_hex("{}{}".format(team_id, subdomain))) + + def refresh(self): + pass + + def is_user_present(self, user_id): + user = self.users.get(user_id) + if user and user.presence == "active": + return True + else: + return False + + def mark_read(self, ts=None, update_remote=True, force=False): + pass + + def connect(self): + if not self.connected and not self.connecting_ws: + if self.ws_url: + self.connecting_ws = True + try: + # only http proxy is currently supported + proxy = ProxyWrapper() + timeout = config.slack_timeout / 1000 + cookie = SlackRequest(self.team, "").options()["cookie"] + if proxy.has_proxy: + ws = create_connection( + self.ws_url, + cookie=cookie, + timeout=timeout, + sslopt=sslopt_ca_certs, + http_proxy_host=proxy.proxy_address, + http_proxy_port=proxy.proxy_port, + http_proxy_auth=(proxy.proxy_user, proxy.proxy_password), + ) + else: + ws = create_connection( + self.ws_url, + cookie=cookie, + timeout=timeout, + sslopt=sslopt_ca_certs, + ) + + self.hook = w.hook_fd( + ws.sock.fileno(), + 1, + 0, + 0, + "receive_ws_callback", + self.get_team_hash(), + ) + ws.sock.setblocking(0) + except: + w.prnt( + self.channel_buffer, + "Failed connecting to slack team {}, retrying.".format( + self.domain + ), + ) + dbg( + "connect failed with exception:\n{}".format(format_exc_tb()), + level=5, + ) + return False + finally: + self.connecting_ws = False + self.ws = ws + self.set_reconnect_url(None) + self.set_connected() + elif not self.connecting_rtm: + # The fast reconnect failed, so start over-ish + for chan in self.channels: + self.channels[chan].history_needs_update = True + s = get_rtm_connect_request(self.token, retries=999, team=self) + self.eventrouter.receive(s) + self.connecting_rtm = True + + def set_connected(self): + self.connected = True + self.last_pong_time = time.time() + self.buffer_prnt( + "Connected to Slack team {} ({}) with username {}".format( + self.team_info["name"], self.domain, self.nick + ) + ) + dbg("connected to {}".format(self.domain)) + + if config.background_load_all_history: + for channel in self.channels.values(): + if channel.channel_buffer: + channel.get_history(slow_queue=True) + else: + current_channel = self.eventrouter.weechat_controller.buffers.get( + w.current_buffer() + ) + if ( + isinstance(current_channel, SlackChannelCommon) + and current_channel.team == self + ): + current_channel.get_history(slow_queue=True) + + def set_disconnected(self): + w.unhook(self.hook) + self.connected = False + + def set_reconnect_url(self, url): + self.ws_url = url + + def next_ws_transaction_id(self): + self.ws_counter += 1 + return self.ws_counter + + def send_to_websocket(self, data, expect_reply=True): + data["id"] = self.next_ws_transaction_id() + message = json.dumps(data) + try: + if expect_reply: + self.ws_replies[data["id"]] = data + self.ws.send(encode_to_utf8(message)) + dbg("Sent {}...".format(message[:100])) + except (WebSocketConnectionClosedException, socket.error) as e: + handle_socket_error(e, self, "send") + + def update_member_presence(self, user, presence): + user.presence = presence + + for c in self.channels: + c = self.channels[c] + if user.id in c.members: + c.buffer_name_needs_update = True + c.update_nicklist(user.id) + + def subscribe_users_presence(self): + # FIXME: There is a limitation in the API to the size of the + # json we can send. + # We should try to be smarter to fetch the users whom we want to + # subscribe to. + users = list(self.users.keys())[:750] + if self.myidentifier not in users: + users.append(self.myidentifier) + self.send_to_websocket( + { + "type": "presence_sub", + "ids": users, + }, + expect_reply=False, + ) + + +class SlackChannelCommon(object): + def __init__(self): + self.label_full_drop_prefix = False + self.label_full = None + self.label_short_drop_prefix = False + self.label_short = None + self.buffer_rename_in_progress = False + + def prnt_message( + self, message, history_message=False, no_log=False, force_render=False + ): + text = self.render(message, force_render) + thread_channel = isinstance(self, SlackThreadChannel) + + if message.subtype == "join": + tagset = "join" + prefix = w.prefix("join").strip() + elif message.subtype == "leave": + tagset = "leave" + prefix = w.prefix("quit").strip() + elif message.subtype == "topic": + tagset = "topic" + prefix = w.prefix("network").strip() + else: + channel_type = self.parent_channel.type if thread_channel else self.type + if channel_type in ["im", "mpim"]: + tagset = "dm" + else: + tagset = "channel" + + if message.subtype == "me_message": + prefix = w.prefix("action").rstrip() + else: + prefix = message.sender + + extra_tags = None + if message.subtype == "thread_broadcast": + extra_tags = [message.subtype] + elif isinstance(message, SlackThreadMessage) and not thread_channel: + if config.thread_messages_in_channel: + extra_tags = [message.subtype] + else: + return + + self.buffer_prnt( + prefix, + text, + message.ts, + tagset=tagset, + tag_nick=message.sender_plain, + history_message=history_message, + no_log=no_log, + extra_tags=extra_tags, + ) + + def print_getting_history(self): + if self.channel_buffer: + ts = SlackTS() + w.buffer_set(self.channel_buffer, "print_hooks_enabled", "0") + w.prnt_date_tags( + self.channel_buffer, + ts.major, + tag(ts, backlog=True, no_log=True), + "\tgetting channel history...", + ) + w.buffer_set(self.channel_buffer, "print_hooks_enabled", "1") + + def reprint_messages(self, history_message=False, no_log=True, force_render=False): + if self.channel_buffer: + w.buffer_clear(self.channel_buffer) + self.last_line_from = None + for message in self.visible_messages.values(): + self.prnt_message(message, history_message, no_log, force_render) + if ( + self.identifier in self.pending_history_requests + or config.thread_messages_in_channel + and self.pending_history_requests + ): + self.print_getting_history() + + def send_message(self, message, subtype=None, request_dict_ext={}): + if subtype == "me_message": + message = linkify_text(message, self.team, escape_characters=False) + s = SlackRequest( + self.team, + "chat.meMessage", + {"channel": self.identifier, "text": message}, + channel=self, + ) + self.eventrouter.receive(s) + else: + message = linkify_text(message, self.team) + request = { + "type": "message", + "channel": self.identifier, + "text": message, + "user": self.team.myidentifier, + } + request.update(request_dict_ext) + self.team.send_to_websocket(request) + + def send_add_reaction(self, msg_id, reaction): + self.send_change_reaction("reactions.add", msg_id, reaction) + + def send_remove_reaction(self, msg_id, reaction): + self.send_change_reaction("reactions.remove", msg_id, reaction) + + def send_change_reaction(self, method, msg_id, reaction): + message = self.message_from_hash_or_index(msg_id) + if message is None: + print_message_not_found_error(msg_id) + return + + reaction_name = replace_emoji_with_string(reaction) + if method == "toggle": + reaction = message.get_reaction(reaction_name) + if reaction and self.team.myidentifier in reaction["users"]: + method = "reactions.remove" + else: + method = "reactions.add" + + data = { + "channel": self.identifier, + "timestamp": message.ts, + "name": reaction_name, + } + s = SlackRequest( + self.team, method, data, channel=self, metadata={"reaction": reaction} + ) + self.eventrouter.receive(s) + + def edit_nth_previous_message(self, msg_id, old, new, flags): + message_filter = ( + lambda message: message.user_identifier == self.team.myidentifier + ) + message = self.message_from_hash_or_index(msg_id, message_filter) + if message is None: + if msg_id: + print_error( + "Invalid id given, must be an existing id to one of your " + + "messages or a number greater than 0 and less than the number " + + "of your messages in the channel" + ) + else: + print_error("You don't have any messages in this channel") + return + if new == "" and old == "": + post_data = {"channel": self.identifier, "ts": message.ts} + s = SlackRequest(self.team, "chat.delete", post_data, channel=self) + self.eventrouter.receive(s) + else: + num_replace = 0 if "g" in flags else 1 + f = re.UNICODE + f |= re.IGNORECASE if "i" in flags else 0 + f |= re.MULTILINE if "m" in flags else 0 + f |= re.DOTALL if "s" in flags else 0 + old_message_text = message.message_json["text"] + new_message_text = re.sub(old, new, old_message_text, num_replace, f) + if new_message_text != old_message_text: + post_data = { + "channel": self.identifier, + "ts": message.ts, + "text": new_message_text, + } + s = SlackRequest(self.team, "chat.update", post_data, channel=self) + self.eventrouter.receive(s) + else: + print_error("The regex didn't match any part of the message") + + def message_from_hash(self, ts_hash, message_filter=None): + if not ts_hash: + return + ts_hash_without_prefix = ts_hash[1:] if ts_hash[0] == "$" else ts_hash + ts = self.hashed_messages.get(ts_hash_without_prefix) + message = self.messages.get(ts) + if message is None: + return + if message_filter and not message_filter(message): + return + return message + + def message_from_index(self, index, message_filter=None, reverse=True): + for ts in reversed(self.visible_messages) if reverse else self.visible_messages: + message = self.messages[ts] + if not message_filter or message_filter(message): + index -= 1 + if index == 0: + return message + + def message_from_hash_or_index( + self, hash_or_index=None, message_filter=None, reverse=True + ): + message = self.message_from_hash(hash_or_index, message_filter) + if not message: + if not hash_or_index: + index = 1 + elif hash_or_index.isdigit(): + index = int(hash_or_index) + else: + return + message = self.message_from_index(index, message_filter, reverse) + return message + + def change_message(self, ts, message_json=None): + ts = SlackTS(ts) + m = self.messages.get(ts) + if not m: + return + if message_json: + m.message_json.update(message_json) + + if ( + not isinstance(m, SlackThreadMessage) + or m.subtype == "thread_broadcast" + or config.thread_messages_in_channel + ): + new_text = self.render(m, force=True) + modify_buffer_line(self.channel_buffer, ts, new_text) + if isinstance(m, SlackThreadMessage) or m.thread_channel is not None: + thread_channel = ( + m.parent_message.thread_channel + if isinstance(m, SlackThreadMessage) + else m.thread_channel + ) + if thread_channel and thread_channel.active: + new_text = thread_channel.render(m, force=True) + modify_buffer_line(thread_channel.channel_buffer, ts, new_text) + + def mark_read(self, ts=None, update_remote=True, force=False, post_data={}): + if self.new_messages or force: + if self.channel_buffer: + w.buffer_set(self.channel_buffer, "unread", "") + w.buffer_set(self.channel_buffer, "hotlist", "-1") + if not ts: + ts = next(reversed(self.messages), SlackTS()) + if ts > self.last_read: + self.last_read = SlackTS(ts) + if update_remote: + args = {"channel": self.identifier, "ts": ts} + args.update(post_data) + mark_method = self.team.slack_api_translator[self.type].get("mark") + if mark_method: + s = SlackRequest(self.team, mark_method, args, channel=self) + self.eventrouter.receive(s) + self.new_messages = False + + def destroy_buffer(self, update_remote): + self.channel_buffer = None + self.got_history = False + self.active = False + + +class SlackChannel(SlackChannelCommon): + """ + Represents an individual slack channel. + """ + + def __init__(self, eventrouter, channel_type="channel", **kwargs): + super(SlackChannel, self).__init__() + self.active = False + for key, value in kwargs.items(): + setattr(self, key, value) + self.eventrouter = eventrouter + self.team = kwargs.get("team") + self.identifier = kwargs["id"] + self.type = channel_type + self.set_name(kwargs["name"]) + self.slack_purpose = kwargs.get("purpose", {"value": ""}) + self.topic = kwargs.get("topic", {"value": ""}) + self.last_read = SlackTS(kwargs.get("last_read", 0)) + self.channel_buffer = None + self.got_history = False + self.got_members = False + self.history_needs_update = False + self.pending_history_requests = set() + self.messages = OrderedDict() + self.visible_messages = SlackChannelVisibleMessages(self) + self.hashed_messages = SlackChannelHashedMessages(self) + self.thread_channels = {} + self.new_messages = False + self.typing = {} + # short name relates to the localvar we change for typing indication + self.set_members(kwargs.get("members", [])) + self.unread_count_display = 0 + self.last_line_from = None + self.buffer_name_needs_update = False + self.last_refresh_typing = False + + def __eq__(self, compare_str): + if ( + compare_str == self.slack_name + or compare_str == self.formatted_name() + or compare_str == self.formatted_name(style="long_default") + ): + return True + else: + return False + + def __repr__(self): + return "Name:{} Identifier:{}".format(self.name, self.identifier) + + @property + def muted(self): + return self.identifier in self.team.muted_channels + + def set_name(self, slack_name): + self.slack_name = slack_name + self.name = self.formatted_name() + self.buffer_name_needs_update = True + + def refresh(self): + typing = self.is_someone_typing() + if self.buffer_name_needs_update or typing != self.last_refresh_typing: + self.last_refresh_typing = typing + self.buffer_name_needs_update = False + self.rename(typing) + + def rename(self, typing=None): + if self.channel_buffer: + self.buffer_rename_in_progress = True + if typing is None: + typing = self.is_someone_typing() + present = ( + self.team.is_user_present(self.user) if self.type == "im" else None + ) + + name = self.formatted_name("long_default", typing, present) + short_name = self.formatted_name("sidebar", typing, present) + w.buffer_set(self.channel_buffer, "name", name) + w.buffer_set(self.channel_buffer, "short_name", short_name) + self.buffer_rename_in_progress = False + + def set_members(self, members): + self.members = set(members) + self.update_nicklist() + + def set_unread_count_display(self, count): + self.unread_count_display = count + self.new_messages = bool(self.unread_count_display) + if self.muted and config.muted_channels_activity != "all": + return + for c in range(self.unread_count_display): + if self.type in ["im", "mpim"]: + w.buffer_set(self.channel_buffer, "hotlist", "2") + else: + w.buffer_set(self.channel_buffer, "hotlist", "1") + + def formatted_name(self, style="default", typing=False, present=None): + show_typing = typing and not self.muted and config.channel_name_typing_indicator + if style == "sidebar" and show_typing: + prepend = ">" + elif self.type == "group" or self.type == "private": + prepend = config.group_name_prefix + elif self.type == "shared": + prepend = config.shared_name_prefix + elif self.type == "im": + if style != "sidebar": + prepend = "" + elif present and config.show_buflist_presence: + prepend = "+" + elif config.channel_name_typing_indicator or config.show_buflist_presence: + prepend = " " + else: + prepend = "" + elif self.type == "mpim": + if style == "sidebar": + prepend = "@" + else: + prepend = "" + else: + prepend = "#" + + name = self.label_full or self.slack_name + + if style == "sidebar": + name = self.label_short or name + if self.label_short_drop_prefix: + if show_typing: + name = prepend + name[1:] + elif ( + self.type == "im" + and present + and config.show_buflist_presence + and name[0] == " " + ): + name = prepend + name[1:] + else: + name = prepend + name + + if self.muted: + sidebar_color = config.color_buflist_muted_channels + elif self.type == "im" and config.colorize_private_chats: + sidebar_color = self.color_name + else: + sidebar_color = "" + + return colorize_string(sidebar_color, name) + elif style == "long_default": + if self.label_full_drop_prefix: + return name + else: + return "{}.{}{}".format(self.team.name, prepend, name) + else: + if self.label_full_drop_prefix: + return name + else: + return prepend + name + + def render_topic(self, fallback_to_purpose=False): + topic = self.topic["value"] + if not topic and fallback_to_purpose: + topic = self.slack_purpose["value"] + return unhtmlescape(unfurl_refs(topic)) + + def set_topic(self, value=None): + if value is not None: + self.topic = {"value": value} + if self.channel_buffer: + topic = self.render_topic(fallback_to_purpose=True) + w.buffer_set(self.channel_buffer, "title", topic) + + def update_from_message_json(self, message_json): + for key, value in message_json.items(): + setattr(self, key, value) + + def open(self, update_remote=True): + if update_remote: + join_method = self.team.slack_api_translator[self.type].get("join") + if join_method: + s = SlackRequest( + self.team, join_method, {"channel": self.identifier}, channel=self + ) + self.eventrouter.receive(s) + self.create_buffer() + self.active = True + self.get_history() + + def check_should_open(self, force=False): + if hasattr(self, "is_archived") and self.is_archived: + return + + if force: + self.create_buffer() + return + + if ( + getattr(self, "is_open", False) + or self.unread_count_display + or self.type not in ["im", "mpim"] + and getattr(self, "is_member", False) + ): + self.create_buffer() + elif self.type in ["im", "mpim"]: + # If it is an IM or MPIM, we still might want to open it if there are unread messages. + info_method = self.team.slack_api_translator[self.type].get("info") + if info_method: + s = SlackRequest( + self.team, info_method, {"channel": self.identifier}, channel=self + ) + self.eventrouter.receive(s) + + def set_related_server(self, team): + self.team = team + + def highlights(self): + nick_highlights = {"@" + self.team.nick, self.team.myidentifier} + subteam_highlights = { + subteam.handle + for subteam in self.team.subteams.values() + if subteam.is_member + } + highlights = nick_highlights | subteam_highlights | self.team.highlight_words + if self.muted and config.muted_channels_activity == "personal_highlights": + return highlights + else: + return highlights | {"@channel", "@everyone", "@group", "@here"} + + def set_highlights(self): + # highlight my own name and any set highlights + if self.channel_buffer: + h_str = ",".join(self.highlights()) + w.buffer_set(self.channel_buffer, "highlight_words", h_str) + + if self.muted and config.muted_channels_activity != "all": + notify_level = "0" if config.muted_channels_activity == "none" else "1" + w.buffer_set(self.channel_buffer, "notify", notify_level) + else: + buffer_full_name = w.buffer_get_string(self.channel_buffer, "full_name") + w.command( + self.channel_buffer, + "/mute /unset weechat.notify.{}".format(buffer_full_name), + ) + + if self.muted and config.muted_channels_activity == "none": + w.buffer_set( + self.channel_buffer, "highlight_tags_restrict", "highlight_force" + ) + else: + w.buffer_set(self.channel_buffer, "highlight_tags_restrict", "") + + for thread_channel in self.thread_channels.values(): + thread_channel.set_highlights(h_str) + + def create_buffer(self): + """ + Creates the WeeChat buffer where the channel magic happens. + """ + if not self.channel_buffer: + self.active = True + self.channel_buffer = w.buffer_new( + self.formatted_name(style="long_default"), + "buffer_input_callback", + "EVENTROUTER", + "", + "", + ) + self.eventrouter.weechat_controller.register_buffer( + self.channel_buffer, self + ) + w.buffer_set(self.channel_buffer, "input_prompt", self.team.nick) + w.buffer_set(self.channel_buffer, "input_multiline", "1") + w.buffer_set( + self.channel_buffer, "localvar_set_type", get_localvar_type(self.type) + ) + w.buffer_set(self.channel_buffer, "localvar_set_slack_type", self.type) + w.buffer_set( + self.channel_buffer, "localvar_set_channel", self.formatted_name() + ) + w.buffer_set(self.channel_buffer, "localvar_set_nick", self.team.nick) + w.buffer_set( + self.channel_buffer, + "localvar_set_completion_default_template", + "${weechat.completion.default_template}|%(usergroups)|%(emoji)", + ) + self.buffer_rename_in_progress = True + w.buffer_set( + self.channel_buffer, "short_name", self.formatted_name(style="sidebar") + ) + self.buffer_rename_in_progress = False + self.set_highlights() + self.set_topic() + if self.channel_buffer: + w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.name) + self.update_nicklist() + + info_method = self.team.slack_api_translator[self.type].get("info") + if info_method: + s = SlackRequest( + self.team, info_method, {"channel": self.identifier}, channel=self + ) + self.eventrouter.receive(s) + + if self.type == "im": + join_method = self.team.slack_api_translator[self.type].get("join") + if join_method: + s = SlackRequest( + self.team, + join_method, + {"users": self.user, "return_im": True}, + channel=self, + ) + self.eventrouter.receive(s) + + def destroy_buffer(self, update_remote): + super(SlackChannel, self).destroy_buffer(update_remote) + self.messages = OrderedDict() + if update_remote and not self.eventrouter.shutting_down: + s = SlackRequest( + self.team, + self.team.slack_api_translator[self.type]["leave"], + {"channel": self.identifier}, + channel=self, + ) + self.eventrouter.receive(s) + + def buffer_prnt( + self, + nick, + text, + timestamp, + tagset, + tag_nick=None, + history_message=False, + no_log=False, + extra_tags=None, + ): + data = "{}\t{}".format(format_nick(nick, self.last_line_from), text) + self.last_line_from = nick + ts = SlackTS(timestamp) + # without this, DMs won't open automatically + if not self.channel_buffer and ts > self.last_read: + self.open(update_remote=False) + if self.channel_buffer: + # backlog messages - we will update the read marker as we print these + backlog = ts <= self.last_read + if not backlog: + self.new_messages = True + + no_log = no_log or history_message and backlog + self_msg = tag_nick == self.team.nick + tags = tag( + ts, + tagset, + user=tag_nick, + self_msg=self_msg, + backlog=backlog, + no_log=no_log, + extra_tags=extra_tags, + ) + + if ( + config.unhide_buffers_with_activity + and not self.is_visible() + and not self.muted + and not no_log + ): + w.buffer_set(self.channel_buffer, "hidden", "0") + + if no_log: + w.buffer_set(self.channel_buffer, "print_hooks_enabled", "0") + w.prnt_date_tags(self.channel_buffer, ts.major, tags, data) + if no_log: + w.buffer_set(self.channel_buffer, "print_hooks_enabled", "1") + if backlog or (self_msg and tagset != "join"): + self.mark_read(ts, update_remote=False, force=True) + + def store_message(self, message_to_store): + if not self.active: + return + + old_message = self.messages.get(message_to_store.ts) + if old_message and old_message.submessages and not message_to_store.submessages: + message_to_store.submessages = old_message.submessages + + self.messages[message_to_store.ts] = message_to_store + self.messages = OrderedDict(sorted(self.messages.items())) + + max_history = w.config_integer( + w.config_get("weechat.history.max_buffer_lines_number") + ) + messages_to_check = islice( + self.messages.items(), max(0, len(self.messages) - max_history) + ) + messages_to_delete = [] + for ts, message in messages_to_check: + if ts == message_to_store.ts: + pass + elif isinstance(message, SlackThreadMessage): + thread_channel = self.thread_channels.get(message.thread_ts) + if thread_channel is None or not thread_channel.active: + messages_to_delete.append(ts) + elif message.number_of_replies(): + if ( + message.thread_channel is None or not message.thread_channel.active + ) and not any( + submessage in self.messages for submessage in message.submessages + ): + messages_to_delete.append(ts) + else: + messages_to_delete.append(ts) + + for ts in messages_to_delete: + message_hash = self.hashed_messages.get(ts) + if message_hash: + del self.hashed_messages[ts] + del self.hashed_messages[message_hash] + del self.messages[ts] + + def is_visible(self): + return w.buffer_get_integer(self.channel_buffer, "hidden") == 0 + + def get_members(self): + if not self.got_members: + # Slack has started returning only a few members for some channels + # in rtm.start. I don't know how we can check if the member list is + # complete, so we have to fetch members for all channels. + s = SlackRequest( + self.team, + "conversations.members", + {"channel": self.identifier, "limit": 1000}, + channel=self, + ) + self.eventrouter.receive(s) + + def get_history(self, slow_queue=False, full=False, no_log=False): + if self.identifier in self.pending_history_requests: + return + + self.print_getting_history() + self.pending_history_requests.add(self.identifier) + self.get_members() + + post_data = {"channel": self.identifier, "limit": config.history_fetch_count} + if self.got_history and self.messages and not full: + post_data["oldest"] = next(reversed(self.messages)) + + s = SlackRequest( + self.team, + self.team.slack_api_translator[self.type]["history"], + post_data, + channel=self, + metadata={"slow_queue": slow_queue, "no_log": no_log}, + ) + self.eventrouter.receive(s, slow_queue) + self.got_history = True + self.history_needs_update = False + + def get_thread_history(self, thread_ts, slow_queue=False, no_log=False): + if thread_ts in self.pending_history_requests: + return + + if config.thread_messages_in_channel: + self.print_getting_history() + thread_channel = self.thread_channels.get(thread_ts) + if thread_channel and thread_channel.active: + thread_channel.print_getting_history() + self.pending_history_requests.add(thread_ts) + + post_data = { + "channel": self.identifier, + "ts": thread_ts, + "limit": config.history_fetch_count, + } + s = SlackRequest( + self.team, + "conversations.replies", + post_data, + channel=self, + metadata={"thread_ts": thread_ts, "no_log": no_log}, + ) + self.eventrouter.receive(s, slow_queue) + + # Typing related + def set_typing(self, user): + if self.channel_buffer and self.is_visible(): + self.typing[user.name] = time.time() + self.buffer_name_needs_update = True + + def is_someone_typing(self): + """ + Walks through dict of typing folks in a channel and fast + returns if any of them is actively typing. If none are, + nulls the dict and returns false. + """ + typing_expire_time = time.time() - TYPING_DURATION + for timestamp in self.typing.values(): + if timestamp > typing_expire_time: + return True + if self.typing: + self.typing = {} + return False + + def get_typing_list(self): + """ + Returns the names of everyone in the channel who is currently typing. + """ + typing_expire_time = time.time() - TYPING_DURATION + typing = [] + for user, timestamp in self.typing.items(): + if timestamp > typing_expire_time: + typing.append(user) + else: + del self.typing[user] + return typing + + def user_joined(self, user_id): + # ugly hack - for some reason this gets turned into a list + self.members = set(self.members) + self.members.add(user_id) + self.update_nicklist(user_id) + + def user_left(self, user_id): + self.members.discard(user_id) + self.update_nicklist(user_id) + + def update_nicklist(self, user=None): + if not self.channel_buffer: + return + if self.type not in ["channel", "group", "mpim", "private", "shared"]: + return + w.buffer_set(self.channel_buffer, "nicklist", "1") + # create nicklists for the current channel if they don't exist + # if they do, use the existing pointer + here = w.nicklist_search_group(self.channel_buffer, "", NICK_GROUP_HERE) + if not here: + here = w.nicklist_add_group( + self.channel_buffer, + "", + NICK_GROUP_HERE, + "weechat.color.nicklist_group", + 1, + ) + afk = w.nicklist_search_group(self.channel_buffer, "", NICK_GROUP_AWAY) + if not afk: + afk = w.nicklist_add_group( + self.channel_buffer, + "", + NICK_GROUP_AWAY, + "weechat.color.nicklist_group", + 1, + ) + + # Add External nicklist group only for shared channels + if self.type == "shared": + external = w.nicklist_search_group( + self.channel_buffer, "", NICK_GROUP_EXTERNAL + ) + if not external: + external = w.nicklist_add_group( + self.channel_buffer, + "", + NICK_GROUP_EXTERNAL, + "weechat.color.nicklist_group", + 2, + ) + + if user and len(self.members) < 1000: + user = self.team.users.get(user) + # External users that have left shared channels won't exist + if not user or user.deleted: + return + nick = w.nicklist_search_nick(self.channel_buffer, "", user.name) + # since this is a change just remove it regardless of where it is + w.nicklist_remove_nick(self.channel_buffer, nick) + # now add it back in to whichever.. + nick_group = afk + if user.is_external: + nick_group = external + elif self.team.is_user_present(user.identifier): + nick_group = here + if user.identifier in self.members: + w.nicklist_add_nick( + self.channel_buffer, + nick_group, + user.name, + user.color_name, + "", + "", + 1, + ) + + # if we didn't get a user, build a complete list. this is expensive. + else: + if len(self.members) < 1000: + try: + for user in self.members: + user = self.team.users.get(user) + if user.deleted: + continue + nick_group = afk + if user.is_external: + nick_group = external + elif self.team.is_user_present(user.identifier): + nick_group = here + w.nicklist_add_nick( + self.channel_buffer, + nick_group, + user.name, + user.color_name, + "", + "", + 1, + ) + except: + dbg( + "DEBUG: {} {} {}".format( + self.identifier, self.name, format_exc_only() + ) + ) + else: + w.nicklist_remove_all(self.channel_buffer) + for fn in ["1| too", "2| many", "3| users", "4| to", "5| show"]: + w.nicklist_add_group( + self.channel_buffer, "", fn, w.color("white"), 1 + ) + + def render(self, message, force=False): + text = message.render(force) + if isinstance(message, SlackThreadMessage): + thread_hash = self.hashed_messages[message.thread_ts] + if config.thread_broadcast_prefix and message.subtype == "thread_broadcast": + prefix = config.thread_broadcast_prefix + else: + prefix = "" + + hash_str = colorize_string( + get_thread_color(str(thread_hash)), + "[{}{}]".format(prefix, thread_hash), + ) + return "{} {}".format(hash_str, text) + + return text + + +class SlackChannelVisibleMessages(MappingReversible): + """ + Class with a reversible mapping interface (like a read-only OrderedDict) + which doesn't include the messages older than first_ts_to_display. + """ + + def __init__(self, channel): + self.channel = channel + self.first_ts_to_display = SlackTS(0) + + def __getitem__(self, key): + if key < self.first_ts_to_display: + raise KeyError(key) + return self.channel.messages[key] + + def _is_visible(self, ts): + if ts < self.first_ts_to_display: + return False + + message = self.get(ts) + if ( + isinstance(message, SlackThreadMessage) + and message.subtype != "thread_broadcast" + and not config.thread_messages_in_channel + ): + return False + + return True + + def __iter__(self): + for ts in self.channel.messages: + if self._is_visible(ts): + yield ts + + def __len__(self): + i = 0 + for _ in self: + i += 1 + return i + + def __reversed__(self): + for ts in reversed(self.channel.messages): + if self._is_visible(ts): + yield ts + + +class SlackChannelHashedMessages(dict): + def __init__(self, channel): + self.channel = channel + + def __missing__(self, key): + if not isinstance(key, SlackTS): + raise KeyError(key) + + hash_len = 3 + full_hash = sha1_hex(str(key)) + short_hash = full_hash[:hash_len] + + while any(x.startswith(short_hash) for x in self if isinstance(x, str)): + hash_len += 1 + short_hash = full_hash[:hash_len] + + if short_hash[:-1] in self: + ts_with_same_hash = self.pop(short_hash[:-1]) + other_full_hash = sha1_hex(str(ts_with_same_hash)) + other_short_hash = other_full_hash[:hash_len] + while short_hash == other_short_hash: + hash_len += 1 + short_hash = full_hash[:hash_len] + other_short_hash = other_full_hash[:hash_len] + self[other_short_hash] = ts_with_same_hash + self[ts_with_same_hash] = other_short_hash + + other_message = self.channel.messages.get(ts_with_same_hash) + if other_message: + self.channel.change_message(other_message.ts) + if other_message.thread_channel: + other_message.thread_channel.rename() + for thread_message in other_message.submessages: + self.channel.change_message(thread_message) + + self[short_hash] = key + self[key] = short_hash + return self[key] + + +class SlackDMChannel(SlackChannel): + """ + Subclass of a normal channel for person-to-person communication, which + has some important differences. + """ + + def __init__(self, eventrouter, users, myidentifier, **kwargs): + dmuser = kwargs["user"] + kwargs["name"] = users[dmuser].name if dmuser in users else dmuser + super(SlackDMChannel, self).__init__(eventrouter, "im", **kwargs) + self.update_color() + self.members = {myidentifier, self.user} + if dmuser in users: + self.set_topic(create_user_status_string(users[dmuser].profile)) + + def set_related_server(self, team): + super(SlackDMChannel, self).set_related_server(team) + if self.user not in self.team.users: + s = SlackRequest(self.team, "users.info", {"user": self.user}, channel=self) + self.eventrouter.receive(s) + + def create_buffer(self): + if not self.channel_buffer: + super(SlackDMChannel, self).create_buffer() + w.buffer_set(self.channel_buffer, "localvar_set_type", "private") + + def update_color(self): + if config.colorize_private_chats: + self.color_name = get_nick_color(self.name) + else: + self.color_name = "" + + def open(self, update_remote=True): + self.create_buffer() + self.get_history() + info_method = self.team.slack_api_translator[self.type].get("info") + if info_method: + s = SlackRequest( + self.team, info_method, {"channel": self.identifier}, channel=self + ) + self.eventrouter.receive(s) + if update_remote: + join_method = self.team.slack_api_translator[self.type].get("join") + if join_method: + s = SlackRequest( + self.team, + join_method, + {"users": self.user, "return_im": True}, + channel=self, + ) + self.eventrouter.receive(s) + + +class SlackGroupChannel(SlackChannel): + """ + A group channel is a private discussion group. + """ + + def __init__(self, eventrouter, channel_type="group", **kwargs): + super(SlackGroupChannel, self).__init__(eventrouter, channel_type, **kwargs) + + +class SlackPrivateChannel(SlackGroupChannel): + """ + A private channel is a private discussion group. At the time of writing, it + differs from group channels in that group channels are channels initially + created as private, while private channels are public channels which are + later converted to private. + """ + + def __init__(self, eventrouter, **kwargs): + super(SlackPrivateChannel, self).__init__(eventrouter, "private", **kwargs) + + +class SlackMPDMChannel(SlackChannel): + """ + An MPDM channel is a special instance of a 'group' channel. + We change the name to look less terrible in WeeChat. + """ + + def __init__(self, eventrouter, team_users, myidentifier, **kwargs): + if kwargs.get("members"): + kwargs["name"] = self.name_from_members( + team_users, kwargs["members"], myidentifier + ) + super(SlackMPDMChannel, self).__init__(eventrouter, "mpim", **kwargs) + + def name_from_members(self, team_users=None, members=None, myidentifier=None): + return ",".join( + sorted( + getattr((team_users or self.team.users).get(user_id), "name", user_id) + for user_id in (members or self.members) + if user_id != (myidentifier or self.team.myidentifier) + ) + ) + + def create_buffer(self): + if not self.channel_buffer: + self.get_members() + super(SlackMPDMChannel, self).create_buffer() + + def open(self, update_remote=True): + self.create_buffer() + self.active = True + self.get_history() + info_method = self.team.slack_api_translator[self.type].get("info") + if info_method: + s = SlackRequest( + self.team, info_method, {"channel": self.identifier}, channel=self + ) + self.eventrouter.receive(s) + if update_remote: + join_method = self.team.slack_api_translator[self.type].get("join") + if join_method: + s = SlackRequest( + self.team, + join_method, + {"users": ",".join(self.members)}, + channel=self, + ) + self.eventrouter.receive(s) + + +class SlackSharedChannel(SlackChannel): + def __init__(self, eventrouter, **kwargs): + super(SlackSharedChannel, self).__init__(eventrouter, "shared", **kwargs) + + +class SlackThreadChannel(SlackChannelCommon): + """ + A thread channel is a virtual channel. We don't inherit from + SlackChannel, because most of how it operates will be different. + """ + + def __init__(self, eventrouter, parent_channel, thread_ts): + super(SlackThreadChannel, self).__init__() + self.active = False + self.eventrouter = eventrouter + self.parent_channel = parent_channel + self.thread_ts = thread_ts + self.messages = SlackThreadChannelMessages(self) + self.channel_buffer = None + self.type = "thread" + self.got_history = False + self.history_needs_update = False + self.team = self.parent_channel.team + self.last_line_from = None + self.new_messages = False + self.buffer_name_needs_update = False + + @property + def members(self): + return self.parent_channel.members + + @property + def parent_message(self): + return self.parent_channel.messages[self.thread_ts] + + @property + def hashed_messages(self): + return self.parent_channel.hashed_messages + + @property + def last_read(self): + return self.parent_message.last_read + + @last_read.setter + def last_read(self, ts): + self.parent_message.last_read = ts + + @property + def identifier(self): + return self.parent_channel.identifier + + @property + def visible_messages(self): + return self.messages + + @property + def muted(self): + return self.parent_channel.muted + + @property + def pending_history_requests(self): + if self.thread_ts in self.parent_channel.pending_history_requests: + return {self.identifier, self.thread_ts} + else: + return set() + + def formatted_name(self, style="default"): + name = self.label_full or self.parent_message.hash + if style == "sidebar": + name = self.label_short or name + if self.label_short_drop_prefix: + return name + else: + indent_expr = w.config_string(w.config_get("buflist.format.indent")) + # Only indent with space if slack_type isn't mentioned in the indent option + indent = "" if "slack_type" in indent_expr else " " + return "{}${}".format(indent, name) + elif style == "long_default": + if self.label_full_drop_prefix: + return name + else: + channel_name = self.parent_channel.formatted_name(style="long_default") + return "{}.{}".format(channel_name, name) + else: + if self.label_full_drop_prefix: + return name + else: + channel_name = self.parent_channel.formatted_name() + return "{}.{}".format(channel_name, name) + + def mark_read(self, ts=None, update_remote=True, force=False, post_data={}): + if not self.parent_message.subscribed: + return + args = {"thread_ts": self.thread_ts} + args.update(post_data) + super(SlackThreadChannel, self).mark_read( + ts=ts, update_remote=update_remote, force=force, post_data=args + ) + + def buffer_prnt( + self, + nick, + text, + timestamp, + tagset, + tag_nick=None, + history_message=False, + no_log=False, + extra_tags=None, + ): + data = "{}\t{}".format(format_nick(nick, self.last_line_from), text) + self.last_line_from = nick + ts = SlackTS(timestamp) + if self.channel_buffer: + # backlog messages - we will update the read marker as we print these + backlog = ts <= self.last_read + if not backlog: + self.new_messages = True + + no_log = no_log or history_message and backlog + self_msg = tag_nick == self.team.nick + tags = tag( + ts, + tagset, + user=tag_nick, + self_msg=self_msg, + backlog=backlog, + no_log=no_log, + extra_tags=extra_tags, + ) + + if no_log: + w.buffer_set(self.channel_buffer, "print_hooks_enabled", "0") + w.prnt_date_tags(self.channel_buffer, ts.major, tags, data) + if no_log: + w.buffer_set(self.channel_buffer, "print_hooks_enabled", "1") + if backlog or self_msg: + self.mark_read(ts, update_remote=False, force=True) + + def get_history(self, slow_queue=False, full=False, no_log=False): + self.got_history = True + self.history_needs_update = False + + any_msg_is_none = any(message is None for message in self.messages.values()) + if not any_msg_is_none: + self.reprint_messages(history_message=True, no_log=no_log) + + if ( + full + or any_msg_is_none + or len(self.parent_message.submessages) + < self.parent_message.number_of_replies() + ): + self.parent_channel.get_thread_history(self.thread_ts, slow_queue, no_log) + + def send_message(self, message, subtype=None, request_dict_ext={}): + if subtype == "me_message": + w.prnt("", "ERROR: /me is not supported in threads") + return w.WEECHAT_RC_ERROR + + request = {"thread_ts": str(self.thread_ts)} + request.update(request_dict_ext) + super(SlackThreadChannel, self).send_message(message, subtype, request) + + def open(self, update_remote=True): + self.create_buffer() + self.active = True + self.get_history() + + def refresh(self): + if self.buffer_name_needs_update: + self.buffer_name_needs_update = False + self.rename() + + def rename(self): + if self.channel_buffer: + self.buffer_rename_in_progress = True + w.buffer_set( + self.channel_buffer, "name", self.formatted_name(style="long_default") + ) + w.buffer_set( + self.channel_buffer, "short_name", self.formatted_name(style="sidebar") + ) + self.buffer_rename_in_progress = False + + def set_highlights(self, highlight_string=None): + if self.channel_buffer: + if highlight_string is None: + highlight_string = ",".join(self.parent_channel.highlights()) + w.buffer_set(self.channel_buffer, "highlight_words", highlight_string) + + def create_buffer(self): + """ + Creates the WeeChat buffer where the thread magic happens. + """ + if not self.channel_buffer: + self.channel_buffer = w.buffer_new( + self.formatted_name(style="long_default"), + "buffer_input_callback", + "EVENTROUTER", + "", + "", + ) + self.eventrouter.weechat_controller.register_buffer( + self.channel_buffer, self + ) + w.buffer_set(self.channel_buffer, "input_prompt", self.team.nick) + w.buffer_set(self.channel_buffer, "input_multiline", "1") + w.buffer_set( + self.channel_buffer, + "localvar_set_type", + get_localvar_type(self.parent_channel.type), + ) + w.buffer_set(self.channel_buffer, "localvar_set_slack_type", self.type) + w.buffer_set(self.channel_buffer, "localvar_set_nick", self.team.nick) + w.buffer_set( + self.channel_buffer, "localvar_set_channel", self.formatted_name() + ) + w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.name) + w.buffer_set( + self.channel_buffer, + "localvar_set_completion_default_template", + "${weechat.completion.default_template}|%(usergroups)|%(emoji)", + ) + self.buffer_rename_in_progress = True + w.buffer_set( + self.channel_buffer, "short_name", self.formatted_name(style="sidebar") + ) + self.buffer_rename_in_progress = False + self.set_highlights() + time_format = w.string_eval_expression( + w.config_string(w.config_get("weechat.look.buffer_time_format")), + {}, + {}, + {}, + ) + parent_time = time.localtime(SlackTS(self.thread_ts).major) + topic = "{} {} | {}".format( + time.strftime(time_format, parent_time), + self.parent_message.sender, + self.render(self.parent_message), + ) + w.buffer_set(self.channel_buffer, "title", topic) + + def destroy_buffer(self, update_remote): + super(SlackThreadChannel, self).destroy_buffer(update_remote) + if update_remote and not self.eventrouter.shutting_down: + self.mark_read() + + def render(self, message, force=False): + return message.render(force) + + +class SlackThreadChannelMessages(MappingReversible): + """ + Class with a reversible mapping interface (like a read-only OrderedDict) + which looks up messages using the parent channel and parent message. + """ + + def __init__(self, thread_channel): + self.thread_channel = thread_channel + + @property + def _parent_message(self): + return self.thread_channel.parent_message + + def __getitem__(self, key): + if ( + key != self._parent_message.ts + and key not in self._parent_message.submessages + ): + raise KeyError(key) + return self.thread_channel.parent_channel.messages[key] + + def __iter__(self): + yield self._parent_message.ts + for ts in self._parent_message.submessages: + yield ts + + def __len__(self): + return 1 + len(self._parent_message.submessages) + + def __reversed__(self): + for ts in reversed(self._parent_message.submessages): + yield ts + yield self._parent_message.ts + + +class SlackUser(object): + """ + Represends an individual slack user. Also where you set their name formatting. + """ + + def __init__(self, originating_team_id, **kwargs): + self.identifier = kwargs["id"] + # These attributes may be missing in the response, so we have to make + # sure they're set + self.profile = {} + self.presence = kwargs.get("presence", "unknown") + self.deleted = kwargs.get("deleted", False) + self.is_external = ( + not kwargs.get("is_bot") and kwargs.get("team_id") != originating_team_id + ) + for key, value in kwargs.items(): + setattr(self, key, value) + + self.name = nick_from_profile(self.profile, kwargs["name"]) + self.username = kwargs["name"] + self.update_color() + + def __repr__(self): + return "Name:{} Identifier:{}".format(self.name, self.identifier) + + def force_color(self, color_name): + self.color_name = color_name + + def update_color(self): + # This will automatically be none/"" if the user has disabled nick + # colourization. + self.color_name = get_nick_color(self.name) + + def update_status(self, status_emoji, status_text): + self.profile["status_emoji"] = status_emoji + self.profile["status_text"] = status_text + + def formatted_name(self, prepend="", enable_color=True): + name = prepend + self.name + if enable_color: + return colorize_string(self.color_name, name) + else: + return name + + +class SlackBot(SlackUser): + """ + Basically the same as a user, but split out to identify and for future + needs + """ + + def __init__(self, originating_team_id, **kwargs): + super(SlackBot, self).__init__(originating_team_id, **kwargs) + + +class SlackMessage(object): + """ + Represents a single slack message and associated context/metadata. + These are modifiable and can be rerendered to change a message, + delete a message, add a reaction, add a thread. + Note: these can't be tied to a SlackUser object because users + can be deleted, so we have to store sender in each one. + """ + + def __init__(self, subtype, message_json, channel): + self.team = channel.team + self.channel = channel + self.subtype = subtype + self.user_identifier = message_json.get("user") + self.message_json = message_json + self.submessages = [] + self.ts = SlackTS(message_json["ts"]) + self.subscribed = message_json.get("subscribed", False) + self.last_read = SlackTS(message_json.get("last_read", 0)) + self.last_notify = SlackTS(0) + + def __hash__(self): + return hash(self.ts) + + @property + def hash(self): + return self.channel.hashed_messages[self.ts] + + @property + def thread_channel(self): + return self.channel.thread_channels.get(self.ts) + + def open_thread(self, switch=False): + if not self.thread_channel or not self.thread_channel.active: + self.channel.thread_channels[self.ts] = SlackThreadChannel( + EVENTROUTER, self.channel, self.ts + ) + self.thread_channel.open() + if switch: + w.buffer_set(self.thread_channel.channel_buffer, "display", "1") + + def render(self, force=False): + # If we already have a rendered version in the object, just return that. + if not force and self.message_json.get("_rendered_text"): + return self.message_json["_rendered_text"] + + if self.message_json.get("deleted"): + text = colorize_string(config.color_deleted, "(deleted)") + self.message_json["_rendered_text"] = text + return text + + blocks = self.message_json.get("blocks", []) + blocks_rendered = "\n".join(unfurl_blocks(blocks)) + if blocks_rendered: + text = blocks_rendered + else: + text = unhtmlescape(unfurl_refs(self.message_json.get("text", ""))) + + if self.message_json.get("mrkdwn", True): + text = render_formatting(text) + + if self.message_json.get("subtype") in ( + "channel_join", + "group_join", + ) and self.message_json.get("inviter"): + inviter_id = self.message_json.get("inviter") + text += unfurl_refs(" by invitation from <@{}>".format(inviter_id)) + + if self.subtype == "me_message" and not self.message_json["text"].startswith( + self.sender + ): + text = "{} {}".format(self.sender, text) + + if "edited" in self.message_json: + text += " " + colorize_string(config.color_edited_suffix, "(edited)") + + text += unwrap_attachments(self, text) + text += unhtmlescape(unfurl_refs(unwrap_files(self, self.message_json, text))) + text += unwrap_huddle(self, self.message_json, text) + text = text.lstrip().replace("\t", " ") + + text += create_reactions_string( + self.message_json.get("reactions", ""), self.team.myidentifier + ) + + if self.number_of_replies(): + text += " " + colorize_string( + get_thread_color(self.hash), + "[ Thread: {} Replies: {}{} ]".format( + self.hash, + self.number_of_replies(), + " Subscribed" if self.subscribed else "", + ), + ) + + # replace_string_with_emoji() was called on blocks earlier via + # unfurl_blocks(), so exclude them here + text_to_replace = text[len(blocks_rendered) :] + text = text[: len(blocks_rendered)] + replace_string_with_emoji(text_to_replace) + + self.message_json["_rendered_text"] = text + return text + + def get_sender(self, plain): + user = self.team.users.get(self.user_identifier) + if user: + name = "{}".format(user.formatted_name(enable_color=not plain)) + if user.is_external: + name += config.external_user_suffix + return name + elif "user_profile" in self.message_json: + nick = nick_from_profile( + self.message_json["user_profile"], self.user_identifier + ) + color_name = get_nick_color(nick) + name = nick if plain else colorize_string(color_name, nick) + if self.message_json.get("user_team") != self.message_json.get("team"): + name += config.external_user_suffix + return name + elif "username" in self.message_json: + username = self.message_json["username"] + if plain: + return username + elif self.message_json.get("subtype") == "bot_message": + return "{} :]".format(username) + else: + return "-{}-".format(username) + elif "service_name" in self.message_json: + service_name = self.message_json["service_name"] + if plain: + return service_name + else: + return "-{}-".format(service_name) + elif self.message_json.get("bot_id") in self.team.bots: + bot = self.team.bots[self.message_json["bot_id"]] + name = bot.formatted_name(enable_color=not plain) + if plain: + return name + else: + return "{} :]".format(name) + return self.user_identifier or self.message_json.get("bot_id") or "" + + @property + def sender(self): + return self.get_sender(False) + + @property + def sender_plain(self): + return self.get_sender(True) + + def get_reaction(self, reaction_name): + for reaction in self.message_json.get("reactions", []): + if reaction["name"] == reaction_name: + return reaction + return None + + def add_reaction(self, reaction_name, user): + reaction = self.get_reaction(reaction_name) + if reaction: + reaction["count"] += 1 + if user not in reaction["users"]: + reaction["users"].append(user) + else: + if "reactions" not in self.message_json: + self.message_json["reactions"] = [] + self.message_json["reactions"].append( + {"name": reaction_name, "count": 1, "users": [user]} + ) + + def remove_reaction(self, reaction_name, user): + reaction = self.get_reaction(reaction_name) + reaction["count"] -= 1 + if user in reaction["users"]: + reaction["users"].remove(user) + + def has_mention(self): + return w.string_has_highlight( + unfurl_refs(self.message_json.get("text")), + ",".join(self.channel.highlights()), + ) + + def number_of_replies(self): + return max(len(self.submessages), self.message_json.get("reply_count", 0)) + + def notify_thread(self, message=None): + if message is None: + if not self.submessages: + return + message = self.channel.messages.get(self.submessages[-1]) + + if ( + self.thread_channel + and self.thread_channel.active + or message.ts <= self.last_read + or message.ts <= self.last_notify + ): + return + + if message.has_mention(): + template = "You were mentioned in thread {hash}, channel {channel}" + elif self.subscribed: + template = "New message in thread {hash}, channel {channel} to which you are subscribed" + else: + return + + self.last_notify = max(message.ts, SlackTS()) + + if config.auto_open_threads and self.subscribed: + self.open_thread() + + if message.user_identifier != self.team.myidentifier and ( + config.notify_subscribed_threads is True + or config.notify_subscribed_threads == "auto" + and not config.auto_open_threads + and not config.thread_messages_in_channel + ): + message = template.format( + hash=self.hash, channel=self.channel.formatted_name() + ) + self.team.buffer_prnt(message, message=True) + + +class SlackThreadMessage(SlackMessage): + def __init__(self, parent_channel, thread_ts, message_json, *args): + subtype = message_json.get( + "subtype", + "thread_broadcast" + if message_json.get("reply_broadcast") + else "thread_message", + ) + super(SlackThreadMessage, self).__init__(subtype, message_json, *args) + self.parent_channel = parent_channel + self.thread_ts = thread_ts + + @property + def parent_message(self): + return self.parent_channel.messages.get(self.thread_ts) + + def open_thread(self, switch=False): + self.parent_message.open_thread(switch) + + +class Hdata(object): + def __init__(self, w): + self.buffer = w.hdata_get("buffer") + self.line = w.hdata_get("line") + self.line_data = w.hdata_get("line_data") + self.lines = w.hdata_get("lines") + + +class SlackTS(object): + def __init__(self, ts=None): + if isinstance(ts, int): + self.major = ts + self.minor = 0 + elif ts is not None: + self.major, self.minor = [int(x) for x in ts.split(".", 1)] + else: + self.major = int(time.time()) + self.minor = 0 + + def __cmp__(self, other): + if isinstance(other, SlackTS): + if self.major < other.major: + return -1 + elif self.major > other.major: + return 1 + elif self.major == other.major: + if self.minor < other.minor: + return -1 + elif self.minor > other.minor: + return 1 + else: + return 0 + elif isinstance(other, str): + s = self.__str__() + if s < other: + return -1 + elif s > other: + return 1 + elif s == other: + return 0 + + def __lt__(self, other): + return self.__cmp__(other) < 0 + + def __le__(self, other): + return self.__cmp__(other) <= 0 + + def __eq__(self, other): + return self.__cmp__(other) == 0 + + def __ne__(self, other): + return self.__cmp__(other) != 0 + + def __ge__(self, other): + return self.__cmp__(other) >= 0 + + def __gt__(self, other): + return self.__cmp__(other) > 0 + + def __hash__(self): + return hash("{}.{}".format(self.major, self.minor)) + + def __repr__(self): + return str("{0}.{1:06d}".format(self.major, self.minor)) + + def split(self, *args, **kwargs): + return [self.major, self.minor] + + def majorstr(self): + return str(self.major) + + def minorstr(self): + return str(self.minor) + + +###### New handlers + + +def handle_rtmstart(login_data, eventrouter, team, channel, metadata): + """ + This handles the main entry call to slack, rtm.start + """ + metadata = login_data["wee_slack_request_metadata"] + + if not login_data["ok"]: + w.prnt( + "", + "ERROR: Failed connecting to Slack with token {}: {}".format( + token_for_print(metadata.token), login_data["error"] + ), + ) + if not re.match(r"^xo\w\w(-\d+){3}-[0-9a-f]+$", metadata.token): + w.prnt( + "", + "ERROR: Token does not look like a valid Slack token. " + "Ensure it is a valid token and not just a OAuth code.", + ) + + return + + self_profile = next( + user["profile"] + for user in login_data["users"] + if user["id"] == login_data["self"]["id"] + ) + self_nick = nick_from_profile(self_profile, login_data["self"]["name"]) + + # Let's reuse a team if we have it already. + th = SlackTeam.generate_team_hash( + login_data["team"]["id"], login_data["team"]["domain"] + ) + if not eventrouter.teams.get(th): + users = {} + for item in login_data["users"]: + users[item["id"]] = SlackUser(login_data["team"]["id"], **item) + + bots = {} + for item in login_data["bots"]: + bots[item["id"]] = SlackBot(login_data["team"]["id"], **item) + + subteams = {} + for item in login_data["subteams"]["all"]: + is_member = item["id"] in login_data["subteams"]["self"] + subteams[item["id"]] = SlackSubteam( + login_data["team"]["id"], is_member=is_member, **item + ) + + channels = {} + for item in login_data["channels"]: + if item["is_shared"]: + channels[item["id"]] = SlackSharedChannel(eventrouter, **item) + elif item["is_mpim"]: + channels[item["id"]] = SlackMPDMChannel( + eventrouter, users, login_data["self"]["id"], **item + ) + elif item["is_private"]: + channels[item["id"]] = SlackPrivateChannel(eventrouter, **item) + else: + channels[item["id"]] = SlackChannel(eventrouter, **item) + + for item in login_data["ims"]: + channels[item["id"]] = SlackDMChannel( + eventrouter, users, login_data["self"]["id"], **item + ) + + for item in login_data["mpims"]: + channels[item["id"]] = SlackMPDMChannel( + eventrouter, users, login_data["self"]["id"], **item + ) + + for item in login_data["groups"]: + if not item["is_mpim"]: + channels[item["id"]] = SlackGroupChannel(eventrouter, **item) + + t = SlackTeam( + eventrouter, + metadata.token, + th, + login_data["url"], + login_data["team"], + subteams, + self_nick, + login_data["self"]["id"], + login_data["self"]["manual_presence"], + users, + bots, + channels, + muted_channels=login_data["self"]["prefs"]["muted_channels"], + highlight_words=login_data["self"]["prefs"]["highlight_words"], + ) + eventrouter.register_team(t) + + else: + t = eventrouter.teams.get(th) + if t.myidentifier != login_data["self"]["id"]: + print_error( + "The Slack team {} has tokens for two different users, this is not supported. The " + "token {} is for user {}, and the token {} is for user {}. Please remove one of " + "them.".format( + t.team_info["name"], + token_for_print(t.token), + t.nick, + token_for_print(metadata.token), + self_nick, + ) + ) + return + elif not metadata.metadata.get("reconnect"): + print_error( + "Ignoring duplicate Slack tokens for the same team ({}) and user ({}). The two " + "tokens are {} and {}.".format( + t.team_info["name"], + t.nick, + token_for_print(t.token), + token_for_print(metadata.token), + ), + warning=True, + ) + return + else: + t.set_reconnect_url(login_data["url"]) + t.connecting_rtm = False + + t.connect() + + +def handle_rtmconnect(login_data, eventrouter, team, channel, metadata): + metadata = login_data["wee_slack_request_metadata"] + team = metadata.team + team.connecting_rtm = False + + if not login_data["ok"]: + w.prnt( + "", + "ERROR: Failed reconnecting to Slack with token {}: {}".format( + token_for_print(metadata.token), login_data["error"] + ), + ) + return + + team.set_reconnect_url(login_data["url"]) + team.connect() + + +def handle_emojilist(emoji_json, eventrouter, team, channel, metadata): + if emoji_json["ok"]: + team.emoji_completions.extend(emoji_json["emoji"].keys()) + + +def handle_conversationsinfo(channel_json, eventrouter, team, channel, metadata): + channel_info = channel_json["channel"] + if "unread_count_display" in channel_info: + unread_count = channel_info["unread_count_display"] + if unread_count and channel.channel_buffer is None: + channel.create_buffer() + channel.set_unread_count_display(unread_count) + if channel_info.get("is_open") and channel.channel_buffer is None: + channel.create_buffer() + if "last_read" in channel_info: + channel.last_read = SlackTS(channel_info["last_read"]) + if "members" in channel_info: + channel.set_members(channel_info["members"]) + + # MPIMs don't have unread_count_display so we have to request the history to check if there are unread messages + if channel.type == "mpim" and not channel.got_history: + s = SlackRequest( + team, + "conversations.history", + {"channel": channel.identifier, "limit": 1}, + channel=channel, + metadata={"only_set_unread": True}, + ) + eventrouter.receive(s) + + +def handle_conversationsopen( + conversation_json, eventrouter, team, channel, metadata, object_name="channel" +): + channel_info = conversation_json[object_name] + if not channel: + channel = create_channel_from_info( + eventrouter, channel_info, team, team.myidentifier, team.users + ) + team.channels[channel_info["id"]] = channel + + if channel.channel_buffer is None: + channel.create_buffer() + + unread_count_display = channel_info.get("unread_count_display") + if unread_count_display is not None: + channel.set_unread_count_display(unread_count_display) + + if metadata.get("switch") and config.switch_buffer_on_join: + w.buffer_set(channel.channel_buffer, "display", "1") + + +def handle_mpimopen( + mpim_json, eventrouter, team, channel, metadata, object_name="group" +): + handle_conversationsopen( + mpim_json, eventrouter, team, channel, metadata, object_name + ) + + +def handle_history( + message_json, eventrouter, team, channel, metadata, includes_threads=True +): + if metadata.get("only_set_unread"): + if message_json["messages"]: + latest = message_json["messages"][0] + latest_ts = SlackTS(latest["ts"]) + if latest_ts > channel.last_read: + if not channel.channel_buffer: + channel.create_buffer() + channel.set_unread_count_display(1) + return + + channel.got_history = True + channel.history_needs_update = False + for message in reversed(message_json["messages"]): + message = process_message( + message, eventrouter, team, channel, metadata, history_message=True + ) + if ( + not includes_threads + and message + and message.number_of_replies() + and ( + config.thread_messages_in_channel + or message.subscribed + and SlackTS(message.message_json.get("latest_reply", 0)) + > message.last_read + ) + ): + channel.get_thread_history( + message.ts, metadata["slow_queue"], metadata["no_log"] + ) + + channel.pending_history_requests.discard(channel.identifier) + if ( + channel.visible_messages.first_ts_to_display.major == 0 + and message_json["messages"] + ): + channel.visible_messages.first_ts_to_display = SlackTS( + message_json["messages"][-1]["ts"] + ) + channel.reprint_messages(history_message=True, no_log=metadata["no_log"]) + for thread_channel in channel.thread_channels.values(): + thread_channel.reprint_messages(history_message=True, no_log=metadata["no_log"]) + + +handle_channelshistory = handle_history +handle_groupshistory = handle_history +handle_imhistory = handle_history +handle_mpimhistory = handle_history + + +def handle_conversationshistory( + message_json, eventrouter, team, channel, metadata, includes_threads=True +): + handle_history(message_json, eventrouter, team, channel, metadata, False) + + +def handle_conversationsreplies(message_json, eventrouter, team, channel, metadata): + for message in message_json["messages"]: + process_message( + message, eventrouter, team, channel, metadata, history_message=True + ) + channel.pending_history_requests.discard(metadata.get("thread_ts")) + thread_channel = channel.thread_channels.get(metadata.get("thread_ts")) + if thread_channel and thread_channel.active: + thread_channel.got_history = True + thread_channel.history_needs_update = False + thread_channel.reprint_messages(history_message=True, no_log=metadata["no_log"]) + if config.thread_messages_in_channel: + channel.reprint_messages(history_message=True, no_log=metadata["no_log"]) + + +def handle_conversationsmembers(members_json, eventrouter, team, channel, metadata): + if members_json["ok"]: + channel.got_members = True + channel.set_members(members_json["members"]) + unknown_users = set(members_json["members"]) - set(team.users.keys()) + for user in unknown_users: + s = SlackRequest(team, "users.info", {"user": user}, channel=channel) + eventrouter.receive(s) + if channel.type == "mpim": + name = channel.name_from_members() + channel.set_name(name) + else: + w.prnt( + team.channel_buffer, + "{}Couldn't load members for channel {}. Error: {}".format( + w.prefix("error"), channel.name, members_json["error"] + ), + ) + + +def handle_usersinfo(user_json, eventrouter, team, channel, metadata): + user_info = user_json["user"] + if not metadata.get("user"): + user = SlackUser(team.identifier, **user_info) + team.users[user_info["id"]] = user + + if channel.type == "shared": + channel.update_nicklist(user_info["id"]) + elif channel.type == "im": + channel.set_name(user.name) + channel.set_topic(create_user_status_string(user.profile)) + + +def handle_usergroupsuserslist(users_json, eventrouter, team, channel, metadata): + header = "Users in {}".format(metadata["usergroup_handle"]) + users = [team.users[key] for key in users_json["users"]] + return print_users_info(team, header, users) + + +def handle_usersprofileset(json, eventrouter, team, channel, metadata): + if not json["ok"]: + w.prnt("", "ERROR: Failed to set profile: {}".format(json["error"])) + + +def handle_conversationscreate(json, eventrouter, team, channel, metadata): + metadata = json["wee_slack_request_metadata"] + if not json["ok"]: + name = metadata.post_data["name"] + print_error("Couldn't create channel {}: {}".format(name, json["error"])) + + +def handle_conversationsinvite(json, eventrouter, team, channel, metadata): + nicks = ", ".join(metadata["nicks"]) + if json["ok"]: + w.prnt(team.channel_buffer, "Invited {} to {}".format(nicks, channel.name)) + else: + w.prnt( + team.channel_buffer, + "ERROR: Couldn't invite {} to {}. Error: {}".format( + nicks, channel.name, json["error"] + ), + ) + + +def handle_chatcommand(json, eventrouter, team, channel, metadata): + command = "{} {}".format(metadata["command"], metadata["command_args"]).rstrip() + response = unfurl_refs(json["response"]) if "response" in json else "" + if json["ok"]: + response_text = "Response: {}".format(response) if response else "No response" + w.prnt( + team.channel_buffer, 'Ran command "{}". {}'.format(command, response_text) + ) + else: + response_text = ". Response: {}".format(response) if response else "" + w.prnt( + team.channel_buffer, + 'ERROR: Couldn\'t run command "{}". Error: {}{}'.format( + command, json["error"], response_text + ), + ) + + +def handle_chatdelete(json, eventrouter, team, channel, metadata): + if not json["ok"]: + print_error("Couldn't delete message: {}".format(json["error"])) + + +def handle_chatupdate(json, eventrouter, team, channel, metadata): + if not json["ok"]: + print_error("Couldn't change message: {}".format(json["error"])) + + +def handle_reactionsadd(json, eventrouter, team, channel, metadata): + if not json["ok"]: + print_error( + "Couldn't add reaction {}: {}".format(metadata["reaction"], json["error"]) + ) + + +def handle_reactionsremove(json, eventrouter, team, channel, metadata): + if not json["ok"]: + print_error( + "Couldn't remove reaction {}: {}".format( + metadata["reaction"], json["error"] + ) + ) + + +def handle_subscriptionsthreadmark(json, eventrouter, team, channel, metadata): + if not json["ok"]: + if json["error"] == "not_allowed_token_type": + team.slack_api_translator["thread"]["mark"] = None + else: + print_error("Couldn't set thread read status: {}".format(json["error"])) + + +def handle_subscriptionsthreadadd(json, eventrouter, team, channel, metadata): + if not json["ok"]: + if json["error"] == "not_allowed_token_type": + print_error( + "Can only subscribe to a thread when using a session token, see the readme: https://github.com/wee-slack/wee-slack#4-add-your-slack-api-tokens" + ) + else: + print_error("Couldn't add thread subscription: {}".format(json["error"])) + + +def handle_subscriptionsthreadremove(json, eventrouter, team, channel, metadata): + if not json["ok"]: + if json["error"] == "not_allowed_token_type": + print_error( + "Can only unsubscribe from a thread when using a session token, see the readme: https://github.com/wee-slack/wee-slack#4-add-your-slack-api-tokens" + ) + else: + print_error("Couldn't remove thread subscription: {}".format(json["error"])) + + +###### New/converted process_ and subprocess_ methods +def process_hello(message_json, eventrouter, team, channel, metadata): + team.subscribe_users_presence() + + +def process_reconnect_url(message_json, eventrouter, team, channel, metadata): + team.set_reconnect_url(message_json["url"]) + + +def process_presence_change(message_json, eventrouter, team, channel, metadata): + users = [team.users[user_id] for user_id in message_json.get("users", [])] + if "user" in metadata: + users.append(metadata["user"]) + for user in users: + team.update_member_presence(user, message_json["presence"]) + if team.myidentifier in users: + w.bar_item_update("away") + w.bar_item_update("slack_away") + + +def process_manual_presence_change(message_json, eventrouter, team, channel, metadata): + team.my_manual_presence = message_json["presence"] + w.bar_item_update("away") + w.bar_item_update("slack_away") + + +def process_pref_change(message_json, eventrouter, team, channel, metadata): + if message_json["name"] == "muted_channels": + team.set_muted_channels(message_json["value"]) + elif message_json["name"] == "highlight_words": + team.set_highlight_words(message_json["value"]) + elif message_json["name"] == "all_notifications_prefs": + new_prefs = json.loads(message_json["value"]) + new_muted_channels = set( + channel_id + for channel_id, prefs in new_prefs["channels"].items() + if prefs["muted"] + ) + team.set_muted_channels(",".join(new_muted_channels)) + global_keywords = new_prefs["global"]["global_keywords"] + team.set_highlight_words(global_keywords) + else: + dbg("Preference change not implemented: {}\n".format(message_json["name"])) + + +def process_user_change(message_json, eventrouter, team, channel, metadata): + """ + Currently only used to update status, but lots here we could do. + """ + user = metadata["user"] + profile = message_json["user"]["profile"] + if user: + user.update_status(profile.get("status_emoji"), profile.get("status_text")) + dmchannel = team.find_channel_by_members( + {team.myidentifier, user.identifier}, channel_type="im" + ) + if dmchannel: + dmchannel.set_topic(create_user_status_string(profile)) + + +def process_user_typing(message_json, eventrouter, team, channel, metadata): + if channel and metadata["user"]: + channel.set_typing(metadata["user"]) + w.bar_item_update("slack_typing_notice") + + +def process_team_join(message_json, eventrouter, team, channel, metadata): + user = message_json["user"] + team.users[user["id"]] = SlackUser(team.identifier, **user) + + +def process_pong(message_json, eventrouter, team, channel, metadata): + team.last_pong_time = time.time() + + +def process_message( + message_json, eventrouter, team, channel, metadata, history_message=False +): + if channel is None: + return + + subtype = message_json.get("subtype") + if ( + not history_message + and not subtype + and "ts" in message_json + and SlackTS(message_json["ts"]) in channel.messages + ): + return + + subtype_functions = get_functions_with_prefix("subprocess_") + + if "thread_ts" in message_json and "reply_count" not in message_json: + message = subprocess_thread_message( + message_json, eventrouter, team, channel, history_message + ) + elif subtype in subtype_functions: + message = subtype_functions[subtype]( + message_json, eventrouter, team, channel, history_message + ) + else: + message = SlackMessage(subtype or "normal", message_json, channel) + channel.store_message(message) + channel.unread_count_display += 1 + + if message and not history_message: + channel.prnt_message(message, history_message) + + if not history_message: + download_files(message_json, channel) + + return message + + +def download_files(message_json, channel): + download_location = config.files_download_location + if not download_location: + return + options = { + "directory": "data", + } + download_location = w.string_eval_path_home(download_location, {}, {}, options) + + if not os.path.exists(download_location): + try: + os.makedirs(download_location) + except: + w.prnt( + "", + "ERROR: Failed to create directory at files_download_location: {}".format( + format_exc_only() + ), + ) + + def fileout_iter(path): + yield path + main, ext = os.path.splitext(path) + for i in count(start=1): + yield main + "-{}".format(i) + ext + + for f in message_json.get("files", []): + if f.get("mode") == "tombstone": + continue + + filetype = "" if f["title"].endswith(f["filetype"]) else "." + f["filetype"] + filename = "{}.{}_{}{}".format( + channel.team.name, channel.name, f["title"], filetype + ) + for fileout in fileout_iter(os.path.join(download_location, filename)): + if os.path.isfile(fileout): + continue + curl_options = SlackRequest(channel.team, "").options() + curl_options["file_out"] = fileout + w.hook_process_hashtable( + "url:" + f["url_private"], + curl_options, + config.slack_timeout, + "", + "", + ) + break + + +def subprocess_thread_message( + message_json, eventrouter, team, channel, history_message +): + parent_ts = SlackTS(message_json["thread_ts"]) + message = SlackThreadMessage(channel, parent_ts, message_json, channel) + + parent_message = message.parent_message + if parent_message and message.ts not in parent_message.submessages: + parent_message.submessages.append(message.ts) + parent_message.submessages.sort() + + channel.store_message(message) + + if parent_message: + channel.change_message(parent_ts) + if parent_message.thread_channel and parent_message.thread_channel.active: + if not history_message: + parent_message.thread_channel.prnt_message(message, history_message) + else: + parent_message.notify_thread(message) + else: + channel.get_thread_history(parent_ts) + + return message + + +subprocess_thread_broadcast = subprocess_thread_message + + +def subprocess_channel_join(message_json, eventrouter, team, channel, history_message): + message = SlackMessage("join", message_json, channel) + channel.store_message(message) + channel.user_joined(message_json["user"]) + return message + + +def subprocess_channel_leave(message_json, eventrouter, team, channel, history_message): + message = SlackMessage("leave", message_json, channel) + channel.store_message(message) + channel.user_left(message_json["user"]) + return message + + +def subprocess_channel_topic(message_json, eventrouter, team, channel, history_message): + message = SlackMessage("topic", message_json, channel) + channel.store_message(message) + channel.set_topic(message_json["topic"]) + return message + + +subprocess_group_join = subprocess_channel_join +subprocess_group_leave = subprocess_channel_leave +subprocess_group_topic = subprocess_channel_topic + + +def subprocess_message_replied( + message_json, eventrouter, team, channel, history_message +): + pass + + +def subprocess_message_changed( + message_json, eventrouter, team, channel, history_message +): + new_message = message_json.get("message") + channel.change_message(new_message["ts"], message_json=new_message) + + +def subprocess_message_deleted( + message_json, eventrouter, team, channel, history_message +): + channel.change_message(message_json["deleted_ts"], {"deleted": True}) + + +def process_reply(message_json, eventrouter, team, channel, metadata): + reply_to = int(message_json["reply_to"]) + original_message_json = team.ws_replies.pop(reply_to, None) + if original_message_json: + dbg("REPLY {}".format(message_json)) + channel = team.channels[original_message_json.get("channel")] + if message_json["ok"]: + original_message_json.update(message_json) + process_message( + original_message_json, + eventrouter, + team=team, + channel=channel, + metadata={}, + ) + else: + print_error( + "Couldn't send message to channel {}: {}".format( + channel.name, message_json["error"] + ) + ) + else: + dbg("Unexpected reply {}".format(message_json)) + + +def process_channel_marked(message_json, eventrouter, team, channel, metadata): + ts = message_json.get("ts") + if ts and channel is not None: + channel.mark_read(ts=ts, force=True, update_remote=False) + else: + dbg("tried to mark something weird {}".format(message_json)) + + +process_group_marked = process_channel_marked +process_im_marked = process_channel_marked +process_mpim_marked = process_channel_marked + + +def process_thread_marked(message_json, eventrouter, team, channel, metadata): + subscription = message_json.get("subscription", {}) + ts = subscription.get("last_read") + thread_ts = subscription.get("thread_ts") + channel = team.channels.get(subscription.get("channel")) + if ts and thread_ts and channel: + thread_channel = channel.thread_channels.get(SlackTS(thread_ts)) + if thread_channel: + thread_channel.mark_read(ts=ts, force=True, update_remote=False) + else: + dbg("tried to mark something weird {}".format(message_json)) + + +def process_channel_joined(message_json, eventrouter, team, channel, metadata): + if channel is None: + channel = create_channel_from_info( + eventrouter, message_json["channel"], team, team.myidentifier, team.users + ) + team.channels[message_json["channel"]["id"]] = channel + else: + channel.update_from_message_json(message_json["channel"]) + + channel.open() + + +def process_channel_created(message_json, eventrouter, team, channel, metadata): + item = message_json["channel"] + item["is_member"] = False + channel = SlackChannel(eventrouter, team=team, **item) + team.channels[item["id"]] = channel + if config.log_channel_created: + team.buffer_prnt("Channel created: {}".format(channel.name)) + + +def process_channel_rename(message_json, eventrouter, team, channel, metadata): + if channel is None: + return + channel.set_name(message_json["channel"]["name"]) + + +def process_im_created(message_json, eventrouter, team, channel, metadata): + item = message_json["channel"] + channel = SlackDMChannel( + eventrouter, team.users, team.myidentifier, team=team, **item + ) + team.channels[item["id"]] = channel + team.buffer_prnt("IM channel created: {}".format(channel.name)) + + +def process_im_open(message_json, eventrouter, team, channel, metadata): + channel.check_should_open(True) + w.buffer_set(channel.channel_buffer, "hotlist", "2") + + +def process_im_close(message_json, eventrouter, team, channel, metadata): + if channel.channel_buffer: + w.prnt( + team.channel_buffer, + "IM {} closed by another client or the server".format(channel.name), + ) + eventrouter.weechat_controller.unregister_buffer( + channel.channel_buffer, False, True + ) + + +def process_mpim_joined(message_json, eventrouter, team, channel, metadata): + item = message_json["channel"] + channel = SlackMPDMChannel( + eventrouter, team.users, team.myidentifier, team=team, **item + ) + team.channels[item["id"]] = channel + channel.open() + + +def process_group_joined(message_json, eventrouter, team, channel, metadata): + item = message_json["channel"] + if item["is_mpim"]: + return + channel = SlackGroupChannel(eventrouter, team=team, **item) + team.channels[item["id"]] = channel + channel.open() + + +def process_reaction_added(message_json, eventrouter, team, channel, metadata): + channel = team.channels.get(message_json["item"].get("channel")) + if channel is None: + return + + if message_json["item"].get("type") == "message": + ts = SlackTS(message_json["item"]["ts"]) + + message = channel.messages.get(ts) + if message: + message.add_reaction(message_json["reaction"], message_json["user"]) + channel.change_message(ts) + else: + dbg("reaction to item type not supported: " + str(message_json)) + + +def process_reaction_removed(message_json, eventrouter, team, channel, metadata): + channel = team.channels.get(message_json["item"].get("channel")) + if channel is None: + return + + if message_json["item"].get("type") == "message": + ts = SlackTS(message_json["item"]["ts"]) + + message = channel.messages.get(ts) + if message: + message.remove_reaction(message_json["reaction"], message_json["user"]) + channel.change_message(ts) + else: + dbg("Reaction to item type not supported: " + str(message_json)) + + +def process_subteam_created(subteam_json, eventrouter, team, channel, metadata): + subteam_json_info = subteam_json["subteam"] + is_member = team.myidentifier in subteam_json_info.get("users", []) + subteam = SlackSubteam(team.identifier, is_member=is_member, **subteam_json_info) + team.subteams[subteam_json_info["id"]] = subteam + + +def process_subteam_updated(subteam_json, eventrouter, team, channel, metadata): + current_subteam_info = team.subteams.get(subteam_json["subteam"]["id"]) + if current_subteam_info is None: + return + + is_member = team.myidentifier in subteam_json["subteam"].get("users", []) + new_subteam_info = SlackSubteam( + team.identifier, is_member=is_member, **subteam_json["subteam"] + ) + team.subteams[subteam_json["subteam"]["id"]] = new_subteam_info + + if current_subteam_info.is_member != new_subteam_info.is_member: + for channel in team.channels.values(): + channel.set_highlights() + + if ( + config.notify_usergroup_handle_updated + and current_subteam_info.handle != new_subteam_info.handle + ): + message = "User group {old_handle} has updated its handle to {new_handle} in team {team}.".format( + old_handle=current_subteam_info.handle, + new_handle=new_subteam_info.handle, + team=team.name, + ) + team.buffer_prnt(message, message=True) + + +def process_emoji_changed(message_json, eventrouter, team, channel, metadata): + team.load_emoji_completions() + + +def process_thread_subscribed(message_json, eventrouter, team, channel, metadata): + dbg("THREAD SUBSCRIBED {}".format(message_json)) + channel = team.channels[message_json["subscription"]["channel"]] + parent_ts = SlackTS(message_json["subscription"]["thread_ts"]) + parent_message = channel.messages.get(parent_ts) + if parent_message: + parent_message.last_read = SlackTS(message_json["subscription"]["last_read"]) + parent_message.subscribed = True + channel.change_message(parent_ts) + parent_message.notify_thread() + else: + channel.get_thread_history(parent_ts) + + +def process_thread_unsubscribed(message_json, eventrouter, team, channel, metadata): + dbg("THREAD UNSUBSCRIBED {}".format(message_json)) + channel = team.channels[message_json["subscription"]["channel"]] + parent_ts = SlackTS(message_json["subscription"]["thread_ts"]) + parent_message = channel.messages.get(parent_ts) + if parent_message: + parent_message.subscribed = False + channel.change_message(parent_ts) + + +###### New module/global methods +def render_formatting(text): + text = re.sub( + r"(^| )\*([^*\n`]+)\*(?=[^\w]|$)", + r"\1{}*\2*{}".format( + w.color(config.render_bold_as), w.color("-" + config.render_bold_as) + ), + text, + flags=re.UNICODE, + ) + text = re.sub( + r"(^| )_([^_\n`]+)_(?=[^\w]|$)", + r"\1{}_\2_{}".format( + w.color(config.render_italic_as), w.color("-" + config.render_italic_as) + ), + text, + flags=re.UNICODE, + ) + return text + + +def linkify_text(message, team, only_users=False, escape_characters=True): + # The get_username_map function is a bit heavy, but this whole + # function is only called on message send.. + usernames = team.get_username_map() + channels = team.get_channel_map() + usergroups = team.generate_usergroup_map() + if escape_characters: + message = ( + message + # Replace IRC formatting chars with Slack formatting chars. + .replace("\x02", "*") + .replace("\x1D", "_") + .replace("\x1F", config.map_underline_to) + # Escape chars that have special meaning to Slack. Note that we do not + # (and should not) perform full HTML entity-encoding here. + # See https://api.slack.com/docs/message-formatting for details. + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + ) + + def linkify_word(match): + word = match.group(0) + prefix, name = match.groups() + if prefix == "@": + if name in ["channel", "everyone", "group", "here"]: + return "".format(name) + elif name in usernames: + return "<@{}>".format(usernames[name]) + elif word in usergroups.keys(): + return "".format(usergroups[word], word) + elif prefix == "#" and not only_users: + if word in channels: + return "<#{}|{}>".format(channels[word], name) + return word + + linkify_regex = r"(?:^|(?<=\s))([@#])([\w\(\)\'.-]+)" + return re.sub(linkify_regex, linkify_word, message, flags=re.UNICODE) + + +def unfurl_blocks(blocks): + block_text = [] + for block in blocks: + try: + if block["type"] == "section": + fields = block.get("fields", []) + if "text" in block: + fields.insert(0, block["text"]) + block_text.extend(unfurl_block_element(field) for field in fields) + elif block["type"] == "actions": + elements = [] + for element in block["elements"]: + if element["type"] == "button": + elements.append(unfurl_block_element(element["text"])) + if "url" in element: + elements.append(element["url"]) + else: + elements.append( + colorize_string( + config.color_deleted, + '<>'.format( + element["type"] + ), + ) + ) + block_text.append(" | ".join(elements)) + elif block["type"] == "call": + block_text.append("Join via " + block["call"]["v1"]["join_url"]) + elif block["type"] == "divider": + block_text.append("---") + elif block["type"] == "context": + block_text.append( + " | ".join(unfurl_block_element(el) for el in block["elements"]) + ) + elif block["type"] == "image": + if "title" in block: + block_text.append(unfurl_block_element(block["title"])) + block_text.append(unfurl_block_element(block)) + elif block["type"] == "rich_text": + for element in block.get("elements", []): + if element["type"] == "rich_text_section": + rendered = unfurl_rich_text_section(element) + if rendered: + block_text.append(rendered) + elif element["type"] == "rich_text_list": + rendered = [ + "{}{} {}".format( + " " * element.get("indent", 0), + block_list_prefix( + element, element.get("offset", 0) + i + ), + unfurl_rich_text_section(e), + ) + for i, e in enumerate(element["elements"]) + ] + block_text.extend(rendered) + elif element["type"] == "rich_text_quote": + lines = [ + "> {}".format(line) + for e in element["elements"] + for line in unfurl_block_rich_text_element(e).split("\n") + ] + block_text.extend(lines) + elif element["type"] == "rich_text_preformatted": + texts = [ + e.get("text", e.get("url", "")) for e in element["elements"] + ] + if texts: + block_text.append("```\n{}\n```".format("".join(texts))) + else: + text = '<>'.format( + element["type"] + ) + block_text.append(colorize_string(config.color_deleted, text)) + dbg( + "Unsupported rich_text element: '{}'".format( + json.dumps(element) + ), + level=4, + ) + else: + block_text.append( + colorize_string( + config.color_deleted, + '<>'.format(block["type"]), + ) + ) + dbg("Unsupported block: '{}'".format(json.dumps(block)), level=4) + except Exception as e: + dbg( + "Failed to unfurl block ({}): {}".format(repr(e), json.dumps(block)), + level=4, + ) + return block_text + + +def convert_int_to_letter(num): + letter = "" + while num > 0: + num -= 1 + letter = chr((num % 26) + 97) + letter + num //= 26 + return letter + + +def convert_int_to_roman(num): + roman_numerals = { + 1000: "m", + 900: "cm", + 500: "d", + 400: "cd", + 100: "c", + 90: "xc", + 50: "l", + 40: "xl", + 10: "x", + 9: "ix", + 5: "v", + 4: "iv", + 1: "i", + } + roman_numeral = "" + for value, symbol in roman_numerals.items(): + while num >= value: + roman_numeral += symbol + num -= value + return roman_numeral + + +def block_list_prefix(element, index): + if element["style"] == "ordered": + if element["indent"] == 0 or element["indent"] == 3: + return "{}.".format(index + 1) + elif element["indent"] == 1 or element["indent"] == 4: + return "{}.".format(convert_int_to_letter(index + 1)) + else: + return "{}.".format(convert_int_to_roman(index + 1)) + else: + if element["indent"] == 0 or element["indent"] == 3: + return "•" + elif element["indent"] == 1 or element["indent"] == 4: + return "◦" + else: + return "▪︎" + + +def unfurl_rich_text_section(block): + texts = [] + prev_element = {"type": "text", "text": ""} + for element in block["elements"] + [prev_element.copy()]: + colors_apply = [] + colors_remove = [] + characters_apply = [] + characters_remove = [] + prev_style = prev_element.get("style", {}) + cur_style = element.get("style", {}) + if cur_style.get("bold", False) != prev_style.get("bold", False): + if cur_style.get("bold"): + colors_apply.append(w.color(config.render_bold_as)) + characters_apply.append("*") + else: + colors_remove.append(w.color("-" + config.render_bold_as)) + characters_remove.append("*") + if cur_style.get("italic", False) != prev_style.get("italic", False): + if cur_style.get("italic"): + colors_apply.append(w.color(config.render_italic_as)) + characters_apply.append("_") + else: + colors_remove.append(w.color("-" + config.render_italic_as)) + characters_remove.append("_") + if cur_style.get("strike", False) != prev_style.get("strike", False): + if cur_style.get("strike"): + characters_apply.append("~") + else: + characters_remove.append("~") + if cur_style.get("code", False) != prev_style.get("code", False): + if cur_style.get("code"): + characters_apply.append("`") + else: + characters_remove.append("`") + + texts.extend(reversed(characters_remove)) + texts.extend(reversed(colors_remove)) + texts.extend(colors_apply) + texts.extend(characters_apply) + texts.append(unfurl_block_rich_text_element(element)) + prev_element = element + + text = "".join(texts) + + if text.endswith("\n"): + return text[:-1] + else: + return text + + +def unfurl_block_rich_text_element(element): + if element["type"] == "text": + return element["text"] + elif element["type"] == "link": + text = element.get("text") + if text and text != element["url"]: + if element.get("style", {}).get("code"): + return text + else: + return unfurl_link(element["url"], text) + else: + return element["url"] + elif element["type"] == "emoji": + return replace_string_with_emoji(":{}:".format(element["name"])) + elif element["type"] == "color": + rgb_int = int(element["value"].lstrip("#"), 16) + weechat_color = w.info_get("color_rgb2term", str(rgb_int)) + return "{} {}".format(element["value"], colorize_string(weechat_color, "■")) + elif element["type"] == "user": + return resolve_ref("@{}".format(element["user_id"])) + elif element["type"] == "usergroup": + return resolve_ref("!subteam^{}".format(element["usergroup_id"])) + elif element["type"] == "broadcast": + return resolve_ref("@{}".format(element["range"])) + elif element["type"] == "channel": + return resolve_ref("#{}".format(element["channel_id"])) + else: + dbg("Unsupported rich text element: '{}'".format(json.dumps(element)), level=4) + return colorize_string( + config.color_deleted, + '<>'.format(element["type"]), + ) + + +def unfurl_block_element(element): + if element["type"] == "mrkdwn": + return render_formatting(unhtmlescape(unfurl_refs(element["text"]))) + elif element["type"] == "plain_text": + return unhtmlescape(unfurl_refs(element["text"])) + elif element["type"] == "image": + if element.get("alt_text"): + return "{} ({})".format(element["image_url"], element["alt_text"]) + else: + return element["image_url"] + else: + dbg("Unsupported block element: '{}'".format(json.dumps(element)), level=4) + return colorize_string( + config.color_deleted, + '<>'.format(element["type"]), + ) + + +def unfurl_link(url, text): + match_url = r"^\w+:(//)?{}$".format(re.escape(text)) + url_matches_desc = re.match(match_url, url) + if url_matches_desc and config.unfurl_auto_link_display == "text": + return text + elif url_matches_desc and config.unfurl_auto_link_display == "url": + return url + else: + return "{} ({})".format(url, text) + + +def unfurl_refs(text): + """ + input : <@U096Q7CQM|someuser> has joined the channel + ouput : someuser has joined the channel + """ + # Find all strings enclosed by <> + # - + # - <#C2147483705|#otherchannel> + # - <@U2147483697|@othernick> + # - + # Test patterns lives in ./_pytest/test_unfurl.py + + def unfurl_ref(match): + ref, fallback = match.groups() + + resolved_ref = resolve_ref(ref) + if resolved_ref != ref: + return resolved_ref + + if fallback and fallback != ref and not config.unfurl_ignore_alt_text: + if ref.startswith("#"): + return "#{}".format(fallback) + elif ref.startswith("@"): + return fallback + elif ref.startswith("!subteam"): + prefix = "@" if not fallback.startswith("@") else "" + return prefix + fallback + elif ref.startswith("!date"): + return fallback + else: + return unfurl_link(ref, fallback) + return ref + + return re.sub(r"<([^|>]*)(?:\|([^>]*))?>", unfurl_ref, text) + + +def htmlescape(text): + return text.replace("&", "&").replace("<", "<").replace(">", ">") + + +def unhtmlescape(text): + return text.replace("<", "<").replace(">", ">").replace("&", "&") + + +def unwrap_attachments(message, text_before): + attachment_texts = [] + a = message.message_json.get("attachments") + if a: + if text_before: + attachment_texts.append("") + for attachment in a: + # Attachments should be rendered roughly like: + # + # $pretext + # $author: (if rest of line is non-empty) $title ($title_link) OR $from_url + # $author: (if no $author on previous line) $text + # $fields + if not config.link_previews and ( + "original_url" in attachment or attachment.get("is_app_unfurl") + ): + continue + t = [] + prepend_title_text = "" + if "author_name" in attachment: + prepend_title_text = attachment["author_name"] + ": " + if "pretext" in attachment: + t.append(attachment["pretext"]) + link_shown = False + title = attachment.get("title") + title_link = attachment.get("title_link", "") + if title_link and title_link in text_before: + title_link = "" + link_shown = True + if title and title_link: + t.append( + "%s%s (%s)" + % ( + prepend_title_text, + title, + htmlescape(title_link), + ) + ) + prepend_title_text = "" + elif title and not title_link: + t.append( + "%s%s" + % ( + prepend_title_text, + title, + ) + ) + prepend_title_text = "" + from_url = attachment.get("from_url", "") + if from_url not in text_before and from_url != title_link: + t.append(htmlescape(from_url)) + elif from_url: + link_shown = True + + atext = attachment.get("text") + if atext: + tx = re.sub(r" *\n[\n ]+", "\n", atext) + t.append(prepend_title_text + tx) + prepend_title_text = "" + + image_url = attachment.get("image_url", "") + if ( + image_url not in text_before + and image_url != from_url + and image_url != title_link + ): + t.append(htmlescape(image_url)) + elif image_url: + link_shown = True + + for field in attachment.get("fields", []): + if field.get("title"): + t.append("{}: {}".format(field["title"], field["value"])) + else: + t.append(field["value"]) + + files = unwrap_files(message, attachment, None) + if files: + t.append(files) + + t = [unhtmlescape(unfurl_refs(x)) for x in t] + + blocks = attachment.get("blocks", []) + t.extend(unfurl_blocks(blocks)) + + if attachment.get("is_msg_unfurl"): + channel_name = resolve_ref("#{}".format(attachment["channel_id"])) + if attachment.get("is_reply_unfurl"): + footer = "From a thread in {}".format(channel_name) + else: + footer = "Posted in {}".format(channel_name) + else: + footer = attachment.get("footer") + + if footer: + ts = attachment.get("ts") + if ts: + ts_int = ts if isinstance(ts, int) else SlackTS(ts).major + if ts_int > 100000000000: + # The Slack web interface interprets very large timestamps + # as milliseconds after the epoch instead of regular Unix + # timestamps. We use the same heuristic here. + ts_int = ts_int // 1000 + time_string = "" + if date.today() - date.fromtimestamp(ts_int) <= timedelta(days=1): + time_string = " at {time}" + timestamp_formatted = resolve_ref( + "!date^{}^{{date_short_pretty}}{}".format(ts_int, time_string) + ).capitalize() + footer += " | {}".format(timestamp_formatted) + t.append(unhtmlescape(unfurl_refs(footer))) + + fallback = attachment.get("fallback") + if t == [] and fallback and not link_shown: + t.append(fallback) + if t: + lines = [ + line for part in t for line in part.strip().split("\n") if part + ] + prefix = "|" + line_color = None + color = attachment.get("color") + if color and config.colorize_attachments != "none": + weechat_color = w.info_get( + "color_rgb2term", str(int(color.lstrip("#"), 16)) + ) + if config.colorize_attachments == "prefix": + prefix = colorize_string(weechat_color, prefix) + elif config.colorize_attachments == "all": + line_color = weechat_color + attachment_texts.extend( + colorize_string(line_color, "{} {}".format(prefix, line)) + for line in lines + ) + return "\n".join(attachment_texts) + + +def unwrap_huddle(message, message_json, text_before): + """ + If huddle is linked to message, append huddle information and link + to connect. + """ + huddle_texts = [] + + if "room" in message_json: + if "name" in message_json.get("room"): + room_name = message_json.get("room").get("name") + + if room_name != "": + huddle_texts.append("Huddle name: {}".format(room_name)) + + for channel in message_json.get("room").get("channels"): + huddle_texts.append( + "https://app.slack.com/client/{team}/{channel}?open=start_huddle".format( + team=message_json.get("team"), channel=channel + ) + ) + + if text_before: + huddle_texts.insert(0, "") + return "\n".join(huddle_texts) + + +def unwrap_files(message, message_json, text_before): + files_texts = [] + for f in message_json.get("files", []): + if f.get("mode", "") == "tombstone": + text = colorize_string(config.color_deleted, "(This file was deleted.)") + elif f.get("mode", "") == "hidden_by_limit": + text = colorize_string( + config.color_deleted, + "(This file is hidden because the workspace has passed its storage limit.)", + ) + elif f.get("mimetype") == "application/vnd.slack-docs": + url = "{}?origin_team={}&origin_channel={}".format( + f["permalink"], message.team.identifier, message.channel.identifier + ) + text = "{} ({})".format(url, f["title"]) + elif f.get("url_private"): + if f.get("title"): + text = "{} ({})".format(f["url_private"], f["title"]) + else: + text = f["url_private"] + else: + dbg("File {} has unrecognized mode {}".format(f["id"], f.get("mode")), 5) + text = colorize_string( + config.color_deleted, "(This file cannot be handled.)" + ) + files_texts.append(text) + + if text_before: + files_texts.insert(0, "") + return "\n".join(files_texts) + + +def resolve_ref(ref): + if ref in ["!channel", "!everyone", "!group", "!here"]: + return ref.replace("!", "@") + for team in EVENTROUTER.teams.values(): + if ref.startswith("@"): + user = team.users.get(ref[1:]) + if user: + suffix = config.external_user_suffix if user.is_external else "" + return "@{}{}".format(user.name, suffix) + elif ref.startswith("#"): + channel = team.channels.get(ref[1:]) + if channel: + return channel.name + elif ref.startswith("!subteam"): + _, subteam_id = ref.split("^") + subteam = team.subteams.get(subteam_id) + if subteam: + return subteam.handle + elif ref.startswith("!date"): + parts = ref.split("^") + ref_datetime = datetime.fromtimestamp(int(parts[1])) + link_suffix = " ({})".format(parts[3]) if len(parts) > 3 else "" + token_to_format = { + "date_num": "%Y-%m-%d", + "date": "%B %d, %Y", + "date_short": "%b %d, %Y", + "date_long": "%A, %B %d, %Y", + "time": "%H:%M", + "time_secs": "%H:%M:%S", + } + + def replace_token(match): + token = match.group(1) + if token.startswith("date_") and token.endswith("_pretty"): + if ref_datetime.date() == date.today(): + return "today" + elif ref_datetime.date() == date.today() - timedelta(days=1): + return "yesterday" + elif ref_datetime.date() == date.today() + timedelta(days=1): + return "tomorrow" + else: + token = token.replace("_pretty", "") + if token in token_to_format: + return decode_from_utf8( + ref_datetime.strftime(token_to_format[token]) + ) + else: + return match.group(0) + + return re.sub(r"{([^}]+)}", replace_token, parts[2]) + link_suffix + + # Something else, just return as-is + return ref - def formatted_name(self, prepend="", enable_color=True): - if colorize_nicks and enable_color: - print_color = self.color - else: - print_color = "" - return print_color + prepend + self.name -class Message(object): +def create_user_status_string(profile): + real_name = profile.get("real_name") + status_emoji = replace_string_with_emoji(profile.get("status_emoji", "")) + status_text = profile.get("status_text") + if status_emoji or status_text: + return "{} | {} {}".format(real_name, status_emoji, status_text) + else: + return real_name - def __init__(self, message_json): - self.message_json = message_json - self.ts = message_json['ts'] - #split timestamp into time and counter - self.ts_time, self.ts_counter = message_json['ts'].split('.') - - def change_text(self, new_text): - if not isinstance(new_text, unicode): - new_text = unicode(new_text, 'utf-8') - self.message_json["text"] = new_text - - def add_reaction(self, reaction, user): - if "reactions" in self.message_json: - found = False - for r in self.message_json["reactions"]: - if r["name"] == reaction and user not in r["users"]: - r["users"].append(user) - found = True - - if not found: - self.message_json["reactions"].append({u"name": reaction, u"users": [user]}) - else: - self.message_json["reactions"] = [{u"name": reaction, u"users": [user]}] - def remove_reaction(self, reaction, user): - if "reactions" in self.message_json: - for r in self.message_json["reactions"]: - if r["name"] == reaction and user in r["users"]: - r["users"].remove(user) - else: - pass +def create_reaction_string(reaction, myidentifier): + if config.show_reaction_nicks: + nicks = [resolve_ref("@{}".format(user)) for user in reaction["users"]] + nicks_extra = ( + ["and others"] if len(reaction["users"]) < reaction["count"] else [] + ) + users = "({})".format(", ".join(nicks + nicks_extra)) + else: + users = reaction["count"] + reaction_string = ":{}:{}".format(reaction["name"], users) + if myidentifier in reaction["users"]: + return colorize_string( + config.color_reaction_suffix_added_by_you, + reaction_string, + reset_color=config.color_reaction_suffix, + ) + else: + return reaction_string + + +def create_reactions_string(reactions, myidentifier): + reactions_with_users = [r for r in reactions if r["count"] > 0] + reactions_string = " ".join( + create_reaction_string(r, myidentifier) for r in reactions_with_users + ) + if reactions_string: + return " " + colorize_string( + config.color_reaction_suffix, "[{}]".format(reactions_string) + ) + else: + return "" - def __eq__(self, other): - return self.ts_time == other or self.ts == other - def __repr__(self): - return "{} {} {} {}\n".format(self.ts_time, self.ts_counter, self.ts, self.message_json) +def hdata_line_ts(line_pointer): + data = w.hdata_pointer(hdata.line, line_pointer, "data") + for i in range(w.hdata_integer(hdata.line_data, data, "tags_count")): + tag = w.hdata_string(hdata.line_data, data, "{}|tags_array".format(i)) + if tag.startswith("slack_ts_"): + return SlackTS(tag[9:]) + return None - def __lt__(self, other): - return self.ts < other.ts -# Only run this function if we're in a slack buffer, else ignore -def slack_buffer_or_ignore(f): - @wraps(f) - def wrapper(current_buffer, *args, **kwargs): - server = servers.find(current_domain_name()) - if not server: - return w.WEECHAT_RC_OK - return f(current_buffer, *args, **kwargs) - return wrapper +def modify_buffer_line(buffer_pointer, ts, new_text): + own_lines = w.hdata_pointer(hdata.buffer, buffer_pointer, "own_lines") + line_pointer = w.hdata_pointer(hdata.lines, own_lines, "last_line") + # Find the last line with this ts + is_last_line = True + while line_pointer and hdata_line_ts(line_pointer) != ts: + is_last_line = False + line_pointer = w.hdata_move(hdata.line, line_pointer, -1) -def slack_command_cb(data, current_buffer, args): - a = args.split(' ', 1) - if len(a) > 1: - function_name, args = a[0], " ".join(a[1:]) + if not line_pointer: + return w.WEECHAT_RC_OK + + if weechat_version >= 0x04000000: + data = w.hdata_pointer(hdata.line, line_pointer, "data") + w.hdata_update(hdata.line_data, data, {"message": new_text}) + return w.WEECHAT_RC_OK + + # Find all lines for the message + pointers = [] + while line_pointer and hdata_line_ts(line_pointer) == ts: + pointers.append(line_pointer) + line_pointer = w.hdata_move(hdata.line, line_pointer, -1) + pointers.reverse() + + if not pointers: + return w.WEECHAT_RC_OK + + if is_last_line: + lines = new_text.split("\n") + extra_lines_count = len(lines) - len(pointers) + if extra_lines_count > 0: + line_data = w.hdata_pointer(hdata.line, pointers[0], "data") + tags_count = w.hdata_integer(hdata.line_data, line_data, "tags_count") + tags = [ + w.hdata_string(hdata.line_data, line_data, "{}|tags_array".format(i)) + for i in range(tags_count) + ] + tags = tags_set_notify_none(tags) + tags_str = ",".join(tags) + last_read_line = w.hdata_pointer(hdata.lines, own_lines, "last_read_line") + should_set_unread = last_read_line == pointers[-1] + + # Insert new lines to match the number of lines in the message + w.buffer_set(buffer_pointer, "print_hooks_enabled", "0") + for _ in range(extra_lines_count): + w.prnt_date_tags(buffer_pointer, ts.major, tags_str, " \t ") + pointers.append(w.hdata_pointer(hdata.lines, own_lines, "last_line")) + if should_set_unread: + w.buffer_set(buffer_pointer, "unread", "") + w.buffer_set(buffer_pointer, "print_hooks_enabled", "1") else: - function_name, args = a[0], None + # Split the message into at most the number of existing lines as we can't insert new lines + lines = new_text.split("\n", len(pointers) - 1) + # Replace newlines to prevent garbled lines in bare display mode + lines = [line.replace("\n", " | ") for line in lines] - try: - command = cmds[function_name](current_buffer, args) - except KeyError: - w.prnt("", "Command not found: " + function_name) - return w.WEECHAT_RC_OK + # Extend lines in case the new message is shorter than the old as we can't delete lines + lines += [""] * (len(pointers) - len(lines)) + for pointer, line in zip(pointers, lines): + data = w.hdata_pointer(hdata.line, pointer, "data") + w.hdata_update(hdata.line_data, data, {"message": line}) -@slack_buffer_or_ignore -def me_command_cb(data, current_buffer, args): - if channels.find(current_buffer): - channel = channels.find(current_buffer) - nick = channel.server.nick - message = "_{}_".format(args) - buffer_input_cb("", current_buffer, message) return w.WEECHAT_RC_OK +def nick_from_profile(profile, username): + if config.use_usernames: + nick = username + else: + full_name = profile.get("real_name") or username + if config.use_full_names: + nick = full_name + else: + nick = profile.get("display_name") or full_name + return nick.replace(" ", "") + + +def format_nick(nick, previous_nick=None): + if nick == previous_nick: + nick = w.config_string(w.config_get("weechat.look.prefix_same_nick")) or nick + nick_prefix = w.config_string(w.config_get("weechat.look.nick_prefix")) + nick_prefix_color_name = w.config_string( + w.config_get("weechat.color.chat_nick_prefix") + ) + + nick_suffix = w.config_string(w.config_get("weechat.look.nick_suffix")) + nick_suffix_color_name = w.config_string( + w.config_get("weechat.color.chat_nick_prefix") + ) + return ( + colorize_string(nick_prefix_color_name, nick_prefix) + + nick + + colorize_string(nick_suffix_color_name, nick_suffix) + ) + + +def tags_set_notify_none(tags): + notify_tags = {"notify_highlight", "notify_message", "notify_private"} + tags = [tag for tag in tags if tag not in notify_tags] + tags += ["no_highlight", "notify_none"] + return tags + + +def tag( + ts, + tagset=None, + user=None, + self_msg=False, + backlog=False, + no_log=False, + extra_tags=None, +): + tagsets = { + "team_info": ["no_highlight", "log3"], + "team_message": ["irc_privmsg", "notify_message", "log1"], + "dm": ["irc_privmsg", "notify_private", "log1"], + "join": ["irc_join", "no_highlight", "log4"], + "leave": ["irc_part", "no_highlight", "log4"], + "topic": ["irc_topic", "no_highlight", "log3"], + "channel": ["irc_privmsg", "notify_message", "log1"], + } + ts_tag = "slack_ts_{}".format(ts) + slack_tag = "slack_{}".format(tagset or "default") + nick_tag = ["nick_{}".format(user).replace(" ", "_")] if user else [] + tags = [ts_tag, slack_tag] + nick_tag + tagsets.get(tagset, []) + if (self_msg and tagset != "join") or backlog: + tags = tags_set_notify_none(tags) + if self_msg: + tags += ["self_msg"] + if backlog: + tags += ["logger_backlog"] + if no_log: + tags += ["no_log"] + tags = [ + tag for tag in tags if not tag.startswith("log") or tag == "logger_backlog" + ] + if extra_tags: + tags += extra_tags + return ",".join(OrderedDict.fromkeys(tags)) + + +def set_own_presence_active(team): + if config.use_usernames: + nick_slackbot = "slackbot" + else: + nick_slackbot = "Slackbot" + + slackbot = team.get_channel_map()[nick_slackbot] + channel = team.channels[slackbot] + request = {"type": "typing", "channel": channel.identifier} + channel.team.send_to_websocket(request, expect_reply=False) + + +###### New/converted command_ commands + + @slack_buffer_or_ignore -def join_command_cb(data, current_buffer, args): - args = args.split() - if len(args) < 2: - w.prnt(current_buffer, "Missing channel argument") - return w.WEECHAT_RC_OK_EAT - elif command_talk(current_buffer, args[1]): +@utf8_decode +def invite_command_cb(data, current_buffer, args): + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + split_args = args.split()[1:] + if not split_args: + w.prnt( + "", + 'Too few arguments for command "/invite" (help on command: /help invite)', + ) return w.WEECHAT_RC_OK_EAT + + if split_args[-1].startswith("#") or split_args[-1].startswith( + config.group_name_prefix + ): + nicks = split_args[:-1] + channel = team.channels.get(team.get_channel_map().get(split_args[-1])) + if not nicks or not channel: + w.prnt("", "{}: No such nick/channel".format(split_args[-1])) + return w.WEECHAT_RC_OK_EAT else: - return w.WEECHAT_RC_OK + nicks = split_args + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + + all_users = team.get_username_map() + users = set() + for nick in nicks: + user = all_users.get(nick.lstrip("@")) + if not user: + w.prnt("", "ERROR: Unknown user: {}".format(nick)) + return w.WEECHAT_RC_OK_EAT + users.add(user) + + s = SlackRequest( + team, + "conversations.invite", + {"channel": channel.identifier, "users": ",".join(users)}, + channel=channel, + metadata={"nicks": nicks}, + ) + EVENTROUTER.receive(s) + return w.WEECHAT_RC_OK_EAT + @slack_buffer_or_ignore +@utf8_decode def part_command_cb(data, current_buffer, args): - if channels.find(current_buffer) or servers.find(current_buffer): - args = args.split() - if len(args) > 1: - channel = args[1:] - servers.find(current_domain_name()).channels.find(channel).close(True) + e = EVENTROUTER + args = args.split() + if len(args) > 1: + team = e.weechat_controller.buffers[current_buffer].team + cmap = team.get_channel_map() + channel = "".join(args[1:]) + if channel in cmap: + buffer_ptr = team.channels[cmap[channel]].channel_buffer + e.weechat_controller.unregister_buffer( + buffer_ptr, update_remote=True, close_buffer=True + ) else: - channels.find(current_buffer).close(True) + w.prnt(team.channel_buffer, "{}: No such channel".format(channel)) + else: + e.weechat_controller.unregister_buffer( + current_buffer, update_remote=True, close_buffer=True + ) + return w.WEECHAT_RC_OK_EAT + + +def parse_topic_command(command): + _, _, args = command.partition(" ") + if args.startswith("#"): + channel_name, _, topic_arg = args.partition(" ") + else: + channel_name = None + topic_arg = args + + if topic_arg == "-delete": + topic = "" + elif topic_arg: + topic = topic_arg + else: + topic = None + + return channel_name, topic + + +@slack_buffer_or_ignore +@utf8_decode +def topic_command_cb(data, current_buffer, command): + """ + Change the topic of a channel + /topic [] [|-delete] + """ + channel_name, topic = parse_topic_command(command) + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + + if channel_name: + channel = team.channels.get(team.get_channel_map().get(channel_name)) + else: + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + + if not channel: + w.prnt(team.channel_buffer, "{}: No such channel".format(channel_name)) return w.WEECHAT_RC_OK_EAT + + if topic is None: + w.prnt( + channel.channel_buffer, + 'Topic for {} is "{}"'.format(channel.name, channel.render_topic()), + ) else: - return w.WEECHAT_RC_OK + s = SlackRequest( + team, + "conversations.setTopic", + {"channel": channel.identifier, "topic": linkify_text(topic, team)}, + channel=channel, + ) + EVENTROUTER.receive(s) + return w.WEECHAT_RC_OK_EAT -# Wrap command_ functions that require they be performed in a slack buffer -def slack_buffer_required(f): - @wraps(f) - def wrapper(current_buffer, *args, **kwargs): - server = servers.find(current_domain_name()) - if not server: - w.prnt(current_buffer, "This command must be used in a slack buffer") - return w.WEECHAT_RC_ERROR - return f(current_buffer, *args, **kwargs) - return wrapper +@slack_buffer_or_ignore +@utf8_decode +def whois_command_cb(data, current_buffer, command): + """ + Get real name of user + /whois + """ + args = command.split() + if len(args) < 2: + w.prnt(current_buffer, "Not enough arguments") + return w.WEECHAT_RC_OK_EAT + user = args[1] + if user.startswith("@"): + user = user[1:] + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + u = team.users.get(team.get_username_map().get(user)) + if u: + + def print_profile(field): + value = u.profile.get(field) + if value: + team.buffer_prnt("[{}]: {}: {}".format(user, field, value)) + + team.buffer_prnt("[{}]: {}".format(user, u.real_name)) + status_emoji = replace_string_with_emoji(u.profile.get("status_emoji", "")) + status_text = u.profile.get("status_text", "") + if status_emoji or status_text: + team.buffer_prnt("[{}]: {} {}".format(user, status_emoji, status_text)) + + team.buffer_prnt("[{}]: username: {}".format(user, u.username)) + team.buffer_prnt("[{}]: id: {}".format(user, u.identifier)) + + print_profile("title") + print_profile("email") + print_profile("phone") + print_profile("skype") + else: + team.buffer_prnt("[{}]: No such user".format(user)) + return w.WEECHAT_RC_OK_EAT -def command_register(current_buffer, args): - CLIENT_ID="2468770254.51917335286" - CLIENT_SECRET="dcb7fe380a000cba0cca3169a5fe8d70" #this is not really a secret - if not args: - message = """ -#### Retrieving a Slack token via OAUTH #### -1) Paste this into a browser: https://slack.com/oauth/authorize?client_id=2468770254.51917335286&scope=client -2) Select the team you wish to access from wee-slack in your browser. -3) Click "Authorize" in the browser **IMPORTANT: the redirect will fail, this is expected** -4) Copy the "code" portion of the URL to your clipboard -5) Return to weechat and run `/slack register [code]` -6) Add the returned token per the normal wee-slack setup instructions +@slack_buffer_or_ignore +@utf8_decode +def me_command_cb(data, current_buffer, args): + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + message = args.split(" ", 1)[1] + channel.send_message(message, subtype="me_message") + return w.WEECHAT_RC_OK_EAT -""" - w.prnt(current_buffer, message) +@utf8_decode +def command_register(data, current_buffer, args): + """ + /slack register [-nothirdparty] [code/token] + Register a Slack team in wee-slack. Call this without any arguments and + follow the instructions to register a new team. If you already have a token + for a team, you can call this with that token to add it. + + By default GitHub Pages will see a temporary code used to create your token + (but not the token itself). If you're worried about this, you can use the + -nothirdparty option, though the process will be a bit less user friendly. + """ + CLIENT_ID = "2468770254.51917335286" + CLIENT_SECRET = "dcb7fe380a000cba0cca3169a5fe8d70" # Not really a secret. + REDIRECT_URI_GITHUB = "https://wee-slack.github.io/wee-slack/oauth" + REDIRECT_URI_NOTHIRDPARTY = "http://not.a.realhost/" + + args = args.strip() + if " " in args: + nothirdparty_arg, _, code = args.partition(" ") + nothirdparty = nothirdparty_arg == "-nothirdparty" else: - aargs = args.split(None, 2) - if len(aargs) <> 1: - w.prnt(current_buffer, "ERROR: invalid args to register") + nothirdparty = args == "-nothirdparty" + code = "" if nothirdparty else args + redirect_uri = quote( + REDIRECT_URI_NOTHIRDPARTY if nothirdparty else REDIRECT_URI_GITHUB, safe="" + ) + + if not code: + if nothirdparty: + nothirdparty_note = "" + last_step = "You will see a message that the site can't be reached, this is expected. The URL for the page will have a code in it of the form `?code=`. Copy the code after the equals sign, return to WeeChat and run `/slack register -nothirdparty `." else: - #w.prnt(current_buffer, "https://slack.com/api/oauth.access?client_id={}&client_secret={}&code={}".format(CLIENT_ID, CLIENT_SECRET, aargs[0])) - ret = urllib.urlopen("https://slack.com/api/oauth.access?client_id={}&client_secret={}&code={}".format(CLIENT_ID, CLIENT_SECRET, aargs[0])).read() - d = json.loads(ret) - if d["ok"] == True: - w.prnt(current_buffer, "Success! Access token is: " + d['access_token']) - else: - w.prnt(current_buffer, "Failed! Error is: " + d['error']) + nothirdparty_note = "\nNote that by default GitHub Pages will see a temporary code used to create your token (but not the token itself). If you're worried about this, you can use the -nothirdparty option, though the process will be a bit less user friendly." + last_step = "The web page will show a command in the form `/slack register `. Run this command in WeeChat." + message = ( + textwrap.dedent( + """ + ### Connecting to a Slack team with OAuth ###{} + 1) Paste this link into a browser: https://slack.com/oauth/authorize?client_id={}&scope=client&redirect_uri={} + 2) Select the team you wish to access from wee-slack in your browser. If you want to add multiple teams, you will have to repeat this whole process for each team. + 3) Click "Authorize" in the browser. + If you get a message saying you are not authorized to install wee-slack, the team has restricted Slack app installation and you will have to request it from an admin. To do that, go to https://my.slack.com/apps/A1HSZ9V8E-wee-slack and click "Request to Install". + 4) {} + """ + ) + .strip() + .format(nothirdparty_note, CLIENT_ID, redirect_uri, last_step) + ) + w.prnt("", "\n" + message) + return w.WEECHAT_RC_OK_EAT + elif code.startswith("xox"): + add_token(code) + return w.WEECHAT_RC_OK_EAT + + uri = ( + "https://slack.com/api/oauth.access?" + "client_id={}&client_secret={}&redirect_uri={}&code={}" + ).format(CLIENT_ID, CLIENT_SECRET, redirect_uri, code) + params = {"useragent": "wee_slack {}".format(SCRIPT_VERSION)} + w.hook_process_hashtable( + "url:{}".format(uri), params, config.slack_timeout, "register_callback", "" + ) + return w.WEECHAT_RC_OK_EAT + + +command_register.completion = "-nothirdparty %-" + + +@utf8_decode +def register_callback(data, command, return_code, out, err): + if return_code != 0: + w.prnt( + "", + "ERROR: problem when trying to get Slack OAuth token. Got return code {}. Err: {}".format( + return_code, err + ), + ) + w.prnt("", "Check the network or proxy settings") + return w.WEECHAT_RC_OK_EAT + + if len(out) <= 0: + w.prnt( + "", + "ERROR: problem when trying to get Slack OAuth token. Got 0 length answer. Err: {}".format( + err + ), + ) + w.prnt("", "Check the network or proxy settings") + return w.WEECHAT_RC_OK_EAT + + d = json.loads(out) + if not d["ok"]: + w.prnt("", "ERROR: Couldn't get Slack OAuth token: {}".format(d["error"])) + return w.WEECHAT_RC_OK_EAT + + add_token(d["access_token"], d["team_name"]) + return w.WEECHAT_RC_OK_EAT + + +def add_token(token, team_name=None): + if config.is_default("slack_api_token"): + w.config_set_plugin("slack_api_token", token) + else: + # Add new token to existing set, joined by comma. + existing_tokens = config.get_string("slack_api_token") + if token in existing_tokens: + print_error("This token is already registered") + return + w.config_set_plugin("slack_api_token", ",".join([existing_tokens, token])) + + if team_name: + w.prnt("", 'Success! Added team "{}"'.format(team_name)) + else: + w.prnt("", "Success! Added token") + w.prnt("", "Please reload wee-slack with: /python reload slack") + w.prnt( + "", + "If you want to add another team you can repeat this process from step 1 before reloading wee-slack.", + ) @slack_buffer_or_ignore +@utf8_decode def msg_command_cb(data, current_buffer, args): - dbg("msg_command_cb") aargs = args.split(None, 2) - who = aargs[1] - - command_talk(current_buffer, who) + who = aargs[1].lstrip("@") + if who != "*": + join_query_command_cb(data, current_buffer, "/query " + who) if len(aargs) > 2: message = aargs[2] - server = servers.find(current_domain_name()) - if server: - channel = server.channels.find(who) + buffer_pointer = EVENTROUTER.weechat_controller.buffers[current_buffer] + team = buffer_pointer.team + if who == "*": + channel = buffer_pointer + else: + cmap = team.get_channel_map() + channel = team.channels.get(cmap.get(who)) + if channel: channel.send_message(message) return w.WEECHAT_RC_OK_EAT -@slack_buffer_required -def command_upload(current_buffer, args): - """ - Uploads a file to the current buffer - /slack upload [file_path] - """ - post_data = {} - channel = current_buffer_name(short=True) - domain = current_domain_name() - token = servers.find(domain).token +def print_team_items_info(team, header, items, extra_info_function): + team.buffer_prnt("{}:".format(header)) + if items: + max_name_length = max(len(item.name) for item in items) + for item in sorted(items, key=lambda item: item.name.lower()): + extra_info = extra_info_function(item) + team.buffer_prnt( + " {:<{}}({})".format(item.name, max_name_length + 2, extra_info) + ) + return w.WEECHAT_RC_OK_EAT - if servers.find(domain).channels.find(channel): - channel_identifier = servers.find(domain).channels.find(channel).identifier - if channel_identifier: - post_data["token"] = token - post_data["channels"] = channel_identifier - post_data["file"] = args - async_slack_api_upload_request(token, "files.upload", post_data) +def print_users_info(team, header, users): + def extra_info_function(user): + external_text = ", external" if user.is_external else "" + return user.presence + external_text -def command_talk(current_buffer, args): - """ - Open a chat with the specified user - /slack talk [user] - """ + return print_team_items_info(team, header, users, extra_info_function) - server = servers.find(current_domain_name()) - if server: - channel = server.channels.find(args) - if channel is None: - user = server.users.find(args) - if user: - user.create_dm_channel() - else: - server.buffer_prnt("User or channel {} not found.".format(args)) - else: - channel.open() - if w.config_get_plugin('switch_buffer_on_join') != '0': - w.buffer_set(channel.channel_buffer, "display", "1") - return True - else: - return False -def command_join(current_buffer, args): +@slack_buffer_required +@utf8_decode +def command_teams(data, current_buffer, args): """ - Join the specified channel - /slack join [channel] + /slack teams + List the connected Slack teams. """ - domain = current_domain_name() - if domain == "": - if len(servers) == 1: - domain = servers[0] - else: - w.prnt(current_buffer, "You are connected to multiple Slack instances, please execute /join from a server buffer. i.e. (domain).slack.com") - return - channel = servers.find(domain).channels.find(args) - if channel != None: - servers.find(domain).channels.find(args).open() - else: - w.prnt(current_buffer, "Channel not found.") + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + teams = EVENTROUTER.teams.values() + extra_info_function = lambda team: "token: {}".format(token_for_print(team.token)) + return print_team_items_info(team, "Slack teams", teams, extra_info_function) @slack_buffer_required -def command_channels(current_buffer, args): +@utf8_decode +def command_channels(data, current_buffer, args): """ - List all the channels for the slack instance (name, id, active) - /slack channels + /slack channels [regex] + List the channels in the current team. + If regex is given show channels whose names match the regular expression. """ - server = servers.find(current_domain_name()) - for channel in server.channels: - line = "{:<25} {} {}".format(channel.name, channel.identifier, channel.active) - server.buffer_prnt(line) - + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + pat = re.compile(args) + channels = [ + channel + for channel in team.channels.values() + if channel.type not in ["im", "mpim"] and pat.search(channel.name) + ] + + def extra_info_function(channel): + if channel.active: + return "member" + elif getattr(channel, "is_archived", None): + return "archived" + else: + return "not a member" -def command_nodistractions(current_buffer, args): - global hide_distractions - hide_distractions = not hide_distractions - if distracting_channels != ['']: - for channel in distracting_channels: - try: - channel_buffer = channels.find(channel).channel_buffer - if channel_buffer: - w.buffer_set(channels.find(channel).channel_buffer, "hidden", str(int(hide_distractions))) - except: - dbg("Can't hide channel {}".format(channel), main_buffer=True) - - -def command_distracting(current_buffer, args): - global distracting_channels - distracting_channels = [x.strip() for x in w.config_get_plugin("distracting_channels").split(',')] - if channels.find(current_buffer) is None: - w.prnt(current_buffer, "This command must be used in a channel buffer") - return - fullname = channels.find(current_buffer).fullname() - if distracting_channels.count(fullname) == 0: - distracting_channels.append(fullname) + if args: + return print_team_items_info( + team, 'Channels that match "' + args + '"', channels, extra_info_function + ) else: - distracting_channels.pop(distracting_channels.index(fullname)) - new = ','.join(distracting_channels) - w.config_set_plugin('distracting_channels', new) + return print_team_items_info(team, "Channels", channels, extra_info_function) @slack_buffer_required -def command_users(current_buffer, args): +@utf8_decode +def command_users(data, current_buffer, args): """ - List all the users for the slack instance (name, id, away) - /slack users + /slack users [regex] + List the users in the current team. + If regex is given show only users that match the case-insensitive regex. """ - server = servers.find(current_domain_name()) - for user in server.users: - line = "{:<40} {} {}".format(user.formatted_name(), user.identifier, user.presence) - server.buffer_prnt(line) + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + + if args: + pat = re.compile(args, flags=re.IGNORECASE) + users = [v for v in team.users.values() if pat.search(v.name)] + header = 'Users that match "{}"'.format(args) + else: + users = team.users.values() + header = "Users" + + return print_users_info(team, header, users) -def command_setallreadmarkers(current_buffer, args): +@slack_buffer_required +@utf8_decode +def command_usergroups(data, current_buffer, args): """ - Sets the read marker for all channels - /slack setallreadmarkers + /slack usergroups [handle] + List the usergroups in the current team + If handle is given show the members in the usergroup """ - for channel in channels: - channel.mark_read() - -def command_changetoken(current_buffer, args): - w.config_set_plugin('slack_api_token', args) + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + usergroups = team.generate_usergroup_map() + usergroup_key = usergroups.get(args) + + if usergroup_key: + s = SlackRequest( + team, + "usergroups.users.list", + {"usergroup": usergroup_key}, + metadata={"usergroup_handle": args}, + ) + EVENTROUTER.receive(s) + elif args: + w.prnt("", "ERROR: Unknown usergroup handle: {}".format(args)) + return w.WEECHAT_RC_ERROR + else: + def extra_info_function(subteam): + is_member = "member" if subteam.is_member else "not a member" + return "{}, {}".format(subteam.handle, is_member) -def command_test(current_buffer, args): - w.prnt(current_buffer, "worked!") + return print_team_items_info( + team, "Usergroups", team.subteams.values(), extra_info_function + ) + return w.WEECHAT_RC_OK_EAT -@slack_buffer_required -def command_away(current_buffer, args): - """ - Sets your status as 'away' - /slack away - """ - server = servers.find(current_domain_name()) - async_slack_api_request(server.domain, server.token, 'presence.set', {"presence": "away"}) +command_usergroups.completion = "%(usergroups) %-" @slack_buffer_required -def command_back(current_buffer, args): +@utf8_decode +def command_talk(data, current_buffer, args): """ - Sets your status as 'back' - /slack back + /slack talk [,[,...]] + Open a chat with the specified user(s). """ - server = servers.find(current_domain_name()) - async_slack_api_request(server.domain, server.token, 'presence.set', {"presence": "active"}) + if not args: + w.prnt("", "Usage: /slack talk [,[,...]]") + return w.WEECHAT_RC_ERROR + return join_query_command_cb(data, current_buffer, "/query " + args) -@slack_buffer_required -def command_markread(current_buffer, args): - """ - Marks current channel as read - /slack markread - """ - # refactor this - one liner i think - channel = current_buffer_name(short=True) - domain = current_domain_name() - if servers.find(domain).channels.find(channel): - servers.find(domain).channels.find(channel).mark_read() - -def command_flushcache(current_buffer, args): - global message_cache - message_cache = collections.defaultdict(list) - cache_write_cb("","") - -def command_cachenow(current_buffer, args): - cache_write_cb("","") - -def command_neveraway(current_buffer, args): - global never_away - if never_away: - never_away = False - dbg("unset never_away", main_buffer=True) - else: - never_away = True - dbg("set never_away", main_buffer=True) +command_talk.completion = "%(nicks)" -def command_printvar(current_buffer, args): - w.prnt("", "{}".format(eval(args))) +@slack_buffer_or_ignore +@utf8_decode +def join_query_command_cb(data, current_buffer, args): + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + split_args = args.split(" ", 1) + if len(split_args) < 2 or not split_args[1]: + w.prnt( + "", + 'Too few arguments for command "{}" (help on command: /help {})'.format( + split_args[0], split_args[0].lstrip("/") + ), + ) + return w.WEECHAT_RC_OK_EAT + query = split_args[1] + # Try finding the channel by name + channel = team.channels.get(team.get_channel_map().get(query)) -def command_p(current_buffer, args): - w.prnt("", "{}".format(eval(args))) + # If the channel doesn't exist, try finding a DM or MPDM instead + if not channel: + if query.startswith("#"): + w.prnt("", "ERROR: Unknown channel: {}".format(query)) + return w.WEECHAT_RC_OK_EAT + # Get the IDs of the users + all_users = team.get_username_map() + users = set() + for username in query.split(","): + user = all_users.get(username.lstrip("@")) + if not user: + w.prnt("", "ERROR: Unknown user: {}".format(username)) + return w.WEECHAT_RC_OK_EAT + users.add(user) + + if users: + if len(users) > 1: + channel_type = "mpim" + # Add the current user since MPDMs include them as a member + users.add(team.myidentifier) + else: + channel_type = "im" -def command_debug(current_buffer, args): - create_slack_debug_buffer() + channel = team.find_channel_by_members(users, channel_type=channel_type) + # If the DM or MPDM doesn't exist, create it + if not channel: + s = SlackRequest( + team, + team.slack_api_translator[channel_type]["join"], + {"users": ",".join(users)}, + metadata={"switch": True}, + ) + EVENTROUTER.receive(s) -def command_debugstring(current_buffer, args): - global debug_string - if args == '': - debug_string = None - else: - debug_string = args + if channel: + channel.open() + if config.switch_buffer_on_join: + w.buffer_set(channel.channel_buffer, "display", "1") + return w.WEECHAT_RC_OK_EAT -def command_search(current_buffer, args): - pass -# if not slack_buffer: -# create_slack_buffer() -# w.buffer_set(slack_buffer, "display", "1") -# query = args -# w.prnt(slack_buffer,"\nSearched for: %s\n\n" % (query)) -# reply = slack_api_request('search.messages', {"query":query}).read() -# data = json.loads(reply) -# for message in data['messages']['matches']: -# message["text"] = message["text"].encode('ascii', 'ignore') -# formatted_message = "%s / %s:\t%s" % (message["channel"]["name"], message['username'], message['text']) -# w.prnt(slack_buffer,str(formatted_message)) - - -def command_nick(current_buffer, args): - pass -# urllib.urlopen("https://%s/account/settings" % (domain)) -# browser.select_form(nr=0) -# browser.form['username'] = args -# reply = browser.submit() +@slack_buffer_required +@utf8_decode +def command_create(data, current_buffer, args): + """ + /slack create [-private] + Create a public or private channel. + """ + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + parts = args.split(None, 1) + if parts[0] == "-private": + args = parts[1] + private = True + else: + private = False -def command_help(current_buffer, args): - help_cmds = { k[8:]: v.__doc__ for k, v in globals().items() if k.startswith("command_") } + post_data = {"name": args, "is_private": private} + s = SlackRequest(team, "conversations.create", post_data) + EVENTROUTER.receive(s) + return w.WEECHAT_RC_OK_EAT - if args: - try: - help_cmds = {args: help_cmds[args]} - except KeyError: - w.prnt("", "Command not found: " + args) - return - for cmd, helptext in help_cmds.items(): - w.prnt('', w.color("bold") + cmd) - w.prnt('', (helptext or 'No help text').strip()) - w.prnt('', '') - -# Websocket handling methods - -def command_openweb(current_buffer, args): - trigger = w.config_get_plugin('trigger_value') - if trigger != "0": - if args is None: - channel = channels.find(current_buffer) - url = "{}/messages/{}".format(channel.server.server_buffer_name, channel.name) - topic = w.buffer_get_string(channel.channel_buffer, "title") - w.buffer_set(channel.channel_buffer, "title", "{}:{}".format(trigger, url)) - w.hook_timer(1000, 0, 1, "command_openweb", json.dumps({"topic": topic, "buffer": current_buffer})) - else: - #TODO: fix this dirty hack because i don't know the right way to send multiple args. - args = current_buffer - data = json.loads(args) - channel_buffer = channels.find(data["buffer"]).channel_buffer - w.buffer_set(channel_buffer, "title", data["topic"]) - return w.WEECHAT_RC_OK +command_create.completion = "-private" -@slack_buffer_or_ignore -def topic_command_cb(data, current_buffer, args): - n = len(args.split()) - if n < 2: - channel = channels.find(current_buffer) - if channel: - w.prnt(current_buffer, 'Topic for {} is "{}"'.format(channel.name, channel.topic)) - return w.WEECHAT_RC_OK_EAT - elif command_topic(current_buffer, args.split(None, 1)[1]): - return w.WEECHAT_RC_OK_EAT - else: - return w.WEECHAT_RC_ERROR -def command_topic(current_buffer, args): +@slack_buffer_required +@utf8_decode +def command_showmuted(data, current_buffer, args): """ - Change the topic of a channel - /slack topic [] [|-delete] - """ - server = servers.find(current_domain_name()) - if server: - arrrrgs = args.split(None, 1) - if arrrrgs[0].startswith('#'): - channel = server.channels.find(arrrrgs[0]) - topic = arrrrgs[1] - else: - channel = server.channels.find(current_buffer) - topic = args + /slack showmuted + List the muted channels in the current team. + """ + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + muted_channels = [ + team.channels[key].name for key in team.muted_channels if key in team.channels + ] + team.buffer_prnt("Muted channels: {}".format(", ".join(muted_channels))) + return w.WEECHAT_RC_OK_EAT - if channel: - if topic == "-delete": - async_slack_api_request(server.domain, server.token, 'channels.setTopic', {"channel": channel.identifier, "topic": ""}) - else: - async_slack_api_request(server.domain, server.token, 'channels.setTopic', {"channel": channel.identifier, "topic": topic}) - return True - else: - return False - else: - return False +@slack_buffer_required +@utf8_decode +def command_thread(data, current_buffer, args): + """ + /thread [count/message_id] + Open the thread for the message. + If no message id is specified the last thread in channel will be opened. + """ + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + if not isinstance(channel, SlackChannelCommon): + print_error("/thread can not be used in the team buffer, only in a channel") + return w.WEECHAT_RC_ERROR -def slack_websocket_cb(server, fd): - try: - data = servers.find(server).ws.recv() - message_json = json.loads(data) - # this magic attaches json that helps find the right dest - message_json['_server'] = server - except WebSocketConnectionClosedException: - servers.find(server).ws.close() - return w.WEECHAT_RC_OK - except Exception: - dbg("socket issue: {}\n".format(traceback.format_exc())) - return w.WEECHAT_RC_OK - # dispatch here - if "reply_to" in message_json: - function_name = "reply" - elif "type" in message_json: - function_name = message_json["type"] + message = channel.message_from_hash(args) + if not message: + message_filter = lambda message: message.number_of_replies() + message = channel.message_from_hash_or_index(args, message_filter) + + if message: + message.open_thread(switch=config.switch_buffer_on_join) + elif args: + print_error( + "Invalid id given, must be an existing id or a number greater " + + "than 0 and less than the number of thread messages in the channel" + ) else: - function_name = "unknown" - try: - proc[function_name](message_json) - except KeyError: - if function_name: - dbg("Function not implemented: {}\n{}".format(function_name, message_json)) - else: - dbg("Function not implemented\n{}".format(message_json)) - w.bar_item_update("slack_typing_notice") - return w.WEECHAT_RC_OK + print_error("No threads found in channel") -def process_reply(message_json): - global unfurl_ignore_alt_text + return w.WEECHAT_RC_OK_EAT - server = servers.find(message_json["_server"]) - identifier = message_json["reply_to"] - item = server.message_buffer.pop(identifier) - if 'text' in item and type(item['text']) is not unicode: - item['text'] = item['text'].decode('UTF-8', 'replace') - if "type" in item: - if item["type"] == "message" and "channel" in item.keys(): - item["ts"] = message_json["ts"] - channels.find(item["channel"]).cache_message(item, from_me=True) - text = unfurl_refs(item["text"], ignore_alt_text=unfurl_ignore_alt_text) - channels.find(item["channel"]).buffer_prnt(item["user"], text, item["ts"]) - dbg("REPLY {}".format(item)) +command_thread.completion = "%(threads) %-" -def process_pong(message_json): - pass +def subscribe_helper(current_buffer, args, usage, api): + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + team = channel.team -def process_pref_change(message_json): - server = servers.find(message_json["_server"]) - if message_json['name'] == u'muted_channels': - muted = message_json['value'].split(',') - for c in server.channels: - if c.identifier in muted: - c.muted = True - else: - c.muted = False + if isinstance(channel, SlackThreadChannel) and not args: + message = channel.parent_message else: - dbg("Preference change not implemented: {}\n".format(message_json['name'])) + message_filter = lambda message: message.number_of_replies() + message = channel.message_from_hash_or_index(args, message_filter) + if not message: + print_message_not_found_error(args) + return w.WEECHAT_RC_OK_EAT -def process_team_join(message_json): - server = servers.find(message_json["_server"]) - item = message_json["user"] - server.add_user(User(server, item["name"], item["id"], item["presence"])) - server.buffer_prnt("New user joined: {}".format(item["name"])) + last_read = next(reversed(message.submessages), message.ts) + post_data = { + "channel": channel.identifier, + "thread_ts": message.ts, + "last_read": last_read, + } + s = SlackRequest(team, api, post_data, channel=channel) + EVENTROUTER.receive(s) + return w.WEECHAT_RC_OK_EAT -def process_manual_presence_change(message_json): - process_presence_change(message_json) -def process_presence_change(message_json): - server = servers.find(message_json["_server"]) - identifier = message_json.get("user", server.nick) - if message_json["presence"] == 'active': - server.users.find(identifier).set_active() - else: - server.users.find(identifier).set_inactive() +@slack_buffer_required +@utf8_decode +def command_subscribe(data, current_buffer, args): + """ + /slack subscribe + Subscribe to a thread, so that you are alerted to new messages. When in a + thread buffer, you can omit the thread id. + This command only works when using a session token, see the readme: https://github.com/wee-slack/wee-slack#4-add-your-slack-api-tokens + """ + return subscribe_helper( + current_buffer, + args, + "Usage: /slack subscribe ", + "subscriptions.thread.add", + ) -def process_channel_marked(message_json): - channel = channels.find(message_json["channel"]) - channel.mark_read(False) - w.buffer_set(channel.channel_buffer, "hotlist", "-1") +command_subscribe.completion = "%(threads) %-" -def process_group_marked(message_json): - channel = channels.find(message_json["channel"]) - channel.mark_read(False) - w.buffer_set(channel.channel_buffer, "hotlist", "-1") +@slack_buffer_required +@utf8_decode +def command_unsubscribe(data, current_buffer, args): + """ + /slack unsubscribe + Unsubscribe from a thread that has been previously subscribed to, so that + you are not alerted to new messages. When in a thread buffer, you can omit + the thread id. -def process_channel_created(message_json): - server = servers.find(message_json["_server"]) - item = message_json["channel"] - if server.channels.find(message_json["channel"]["name"]): - server.channels.find(message_json["channel"]["name"]).open(False) - else: - item = message_json["channel"] - server.add_channel(Channel(server, item["name"], item["id"], False, prepend_name="#")) - server.buffer_prnt("New channel created: {}".format(item["name"])) + This command only works when using a session token, see the readme: https://github.com/wee-slack/wee-slack#4-add-your-slack-api-tokens + """ + return subscribe_helper( + current_buffer, + args, + "Usage: /slack unsubscribe ", + "subscriptions.thread.remove", + ) -def process_channel_left(message_json): - server = servers.find(message_json["_server"]) - server.channels.find(message_json["channel"]).close(False) +command_unsubscribe.completion = "%(threads) %-" -def process_channel_join(message_json): - server = servers.find(message_json["_server"]) - channel = server.channels.find(message_json["channel"]) - text = unfurl_refs(message_json["text"], ignore_alt_text=False) - channel.buffer_prnt(w.prefix("join").rstrip(), text, message_json["ts"]) - channel.user_join(message_json["user"]) +@slack_buffer_required +@utf8_decode +def command_reply(data, current_buffer, args): + """ + /reply [-alsochannel] [] + When in a channel buffer: + /reply [-alsochannel] + Reply in a thread on the message. Specify either the message id or a count + upwards to the message from the last message. -def process_channel_topic(message_json): - server = servers.find(message_json["_server"]) - channel = server.channels.find(message_json["channel"]) - text = unfurl_refs(message_json["text"], ignore_alt_text=False) - channel.buffer_prnt(w.prefix("network").rstrip(), text, message_json["ts"]) - channel.set_topic(message_json["topic"]) + When in a thread buffer: + /reply [-alsochannel] + Reply to the current thread. This can be used to send the reply to the + rest of the channel. + + In either case, -alsochannel also sends the reply to the parent channel. + """ + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + + parts = args.split(None, 1) + if len(parts) < 1: + w.prnt( + "", 'Too few arguments for command "/reply" (help on command: /help reply)' + ) + return w.WEECHAT_RC_ERROR + if parts[0] == "-alsochannel": + args = parts[1] + broadcast = True + else: + broadcast = False -def process_channel_joined(message_json): - server = servers.find(message_json["_server"]) - if server.channels.find(message_json["channel"]["name"]): - server.channels.find(message_json["channel"]["name"]).open(False) + if isinstance(channel, SlackThreadChannel): + text = args + message = channel.parent_message else: - item = message_json["channel"] - server.add_channel(Channel(server, item["name"], item["id"], item["is_open"], item["last_read"], "#", item["members"], item["topic"]["value"])) + try: + msg_id, text = args.split(None, 1) + except ValueError: + w.prnt( + "", + "Usage (when in a channel buffer): /reply [-alsochannel] ", + ) + return w.WEECHAT_RC_OK_EAT + message = channel.message_from_hash_or_index(msg_id) + + if not message: + print_message_not_found_error(args) + return w.WEECHAT_RC_OK_EAT + + if isinstance(message, SlackThreadMessage): + parent_id = str(message.parent_message.ts) + elif message: + parent_id = str(message.ts) + + channel.send_message( + text, request_dict_ext={"thread_ts": parent_id, "reply_broadcast": broadcast} + ) + return w.WEECHAT_RC_OK_EAT -def process_channel_leave(message_json): - server = servers.find(message_json["_server"]) - channel = server.channels.find(message_json["channel"]) - text = unfurl_refs(message_json["text"], ignore_alt_text=False) - channel.buffer_prnt(w.prefix("quit").rstrip(), text, message_json["ts"]) - channel.user_leave(message_json["user"]) +command_reply.completion = "%(threads)|-alsochannel %(threads)" -def process_channel_archive(message_json): - server = servers.find(message_json["_server"]) - channel = server.channels.find(message_json["channel"]) - channel.detach_buffer() +@slack_buffer_required +@utf8_decode +def command_rehistory(data, current_buffer, args): + """ + /rehistory [-remote] + Reload the history in the current channel. + With -remote the history will be downloaded again from Slack. + """ + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + if args == "-remote": + channel.get_history(full=True, no_log=True) + else: + channel.reprint_messages(force_render=True) + return w.WEECHAT_RC_OK_EAT -def process_group_join(message_json): - process_channel_join(message_json) +command_rehistory.completion = "-remote" -def process_group_leave(message_json): - process_channel_leave(message_json) +@slack_buffer_required +@utf8_decode +def command_hide(data, current_buffer, args): + """ + /hide + Hide the current channel if it is marked as distracting. + """ + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + name = channel.formatted_name(style="long_default") + if name in config.distracting_channels: + w.buffer_set(channel.channel_buffer, "hidden", "1") + return w.WEECHAT_RC_OK_EAT -def process_group_topic(message_json): - process_channel_topic(message_json) +@utf8_decode +def slack_command_cb(data, current_buffer, args): + split_args = args.split(" ", 1) + cmd_name = split_args[0] + cmd_args = split_args[1] if len(split_args) > 1 else "" + cmd = EVENTROUTER.cmds.get(cmd_name or "help") + if not cmd: + w.prnt("", "Command not found: " + cmd_name) + return w.WEECHAT_RC_OK + return cmd(data, current_buffer, cmd_args) -def process_group_left(message_json): - server = servers.find(message_json["_server"]) - server.channels.find(message_json["channel"]).close(False) +@utf8_decode +def command_help(data, current_buffer, args): + """ + /slack help [command] + Print help for /slack commands. + """ + if args: + cmd = EVENTROUTER.cmds.get(args) + if cmd: + cmds = {args: cmd} + else: + w.prnt("", "Command not found: " + args) + return w.WEECHAT_RC_OK + else: + cmds = EVENTROUTER.cmds + w.prnt("", "\n{}".format(colorize_string("bold", "Slack commands:"))) + + script_prefix = "{0}[{1}python{0}/{1}slack{0}]{1}".format( + w.color("green"), w.color("reset") + ) + + for _, cmd in sorted(cmds.items()): + name, cmd_args, description = parse_help_docstring(cmd) + w.prnt( + "", + "\n{} {} {}\n\n{}".format( + script_prefix, colorize_string("white", name), cmd_args, description + ), + ) + return w.WEECHAT_RC_OK -def process_group_joined(message_json): - server = servers.find(message_json["_server"]) - if server.channels.find(message_json["channel"]["name"]): - server.channels.find(message_json["channel"]["name"]).open(False) +@slack_buffer_required +@utf8_decode +def command_distracting(data, current_buffer, args): + """ + /slack distracting + Add or remove the current channel from distracting channels. You can hide + or unhide these channels with /slack nodistractions. + """ + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + fullname = channel.formatted_name(style="long_default") + if fullname in config.distracting_channels: + config.distracting_channels.remove(fullname) else: - item = message_json["channel"] - server.add_channel(GroupChannel(server, item["name"], item["id"], item["is_open"], item["last_read"], "#", item["members"], item["topic"]["value"])) - + config.distracting_channels.append(fullname) + w.config_set_plugin("distracting_channels", ",".join(config.distracting_channels)) + return w.WEECHAT_RC_OK_EAT -def process_group_archive(message_json): - channel = server.channels.find(message_json["channel"]) - channel.detach_buffer() +@slack_buffer_required +@utf8_decode +def command_slash(data, current_buffer, args): + """ + /slack slash /customcommand arg1 arg2 arg3 + Run a custom slack command. + """ + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + team = channel.team + + split_args = args.split(" ", 1) + command = split_args[0] + text = split_args[1] if len(split_args) > 1 else "" + text_linkified = linkify_text(text, team, only_users=True) + + s = SlackRequest( + team, + "chat.command", + {"command": command, "text": text_linkified, "channel": channel.identifier}, + channel=channel, + metadata={"command": command, "command_args": text}, + ) + EVENTROUTER.receive(s) + return w.WEECHAT_RC_OK_EAT -def process_im_close(message_json): - server = servers.find(message_json["_server"]) - server.channels.find(message_json["channel"]).close(False) +@slack_buffer_required +@utf8_decode +def command_mute(data, current_buffer, args): + """ + /slack mute + Toggle mute on the current channel. + """ + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + team = channel.team + team.muted_channels ^= {channel.identifier} + muted_str = "Muted" if channel.identifier in team.muted_channels else "Unmuted" + team.buffer_prnt("{} channel {}".format(muted_str, channel.name)) + s = SlackRequest( + team, + "users.prefs.set", + {"name": "muted_channels", "value": ",".join(team.muted_channels)}, + channel=channel, + ) + EVENTROUTER.receive(s) + return w.WEECHAT_RC_OK_EAT -def process_im_open(message_json): - server = servers.find(message_json["_server"]) - server.channels.find(message_json["channel"]).open() +@slack_buffer_required +@utf8_decode +def command_linkarchive(data, current_buffer, args): + """ + /slack linkarchive [message_id] + Place a link to the channel or message in the input bar. + Use cursor or mouse mode to get the id. + """ + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + url = "https://{}/".format(channel.team.domain) + + if isinstance(channel, SlackChannelCommon): + url += "archives/{}/".format(channel.identifier) + if args: + message = channel.message_from_hash_or_index(args) + if message: + url += "p{}{:0>6}".format(message.ts.majorstr(), message.ts.minorstr()) + if isinstance(message, SlackThreadMessage): + url += "?thread_ts={}&cid={}".format( + message.parent_message.ts, channel.identifier + ) + else: + print_message_not_found_error(args) + return w.WEECHAT_RC_OK_EAT -def process_im_marked(message_json): - channel = channels.find(message_json["channel"]) - channel.mark_read(False) - if channel.channel_buffer is not None: - w.buffer_set(channel.channel_buffer, "hotlist", "-1") + w.command(current_buffer, "/input insert {}".format(url)) + return w.WEECHAT_RC_OK_EAT -def process_im_created(message_json): - server = servers.find(message_json["_server"]) - item = message_json["channel"] - channel_name = server.users.find(item["user"]).name - if server.channels.find(channel_name): - server.channels.find(channel_name).open(False) - else: - item = message_json["channel"] - server.add_channel(DmChannel(server, channel_name, item["id"], item["is_open"], item["last_read"])) - server.buffer_prnt("New direct message channel created: {}".format(item["name"])) +command_linkarchive.completion = "%(threads) %-" -def process_user_typing(message_json): - server = servers.find(message_json["_server"]) - channel = server.channels.find(message_json["channel"]) - if channel: - channel.set_typing(server.users.find(message_json["user"]).name) +@utf8_decode +def command_nodistractions(data, current_buffer, args): + """ + /slack nodistractions + Hide or unhide all channels marked as distracting. + """ + global hide_distractions + hide_distractions = not hide_distractions + channels = [ + channel + for channel in EVENTROUTER.weechat_controller.buffers.values() + if channel in config.distracting_channels + ] + for channel in channels: + w.buffer_set(channel.channel_buffer, "hidden", str(int(hide_distractions))) + return w.WEECHAT_RC_OK_EAT -def process_bot_enable(message_json): - process_bot_integration(message_json) +@slack_buffer_required +@utf8_decode +def command_upload(data, current_buffer, args): + """ + /slack upload + Uploads a file to the current buffer. + """ + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + weechat_dir = w.info_get("weechat_data_dir", "") or w.info_get("weechat_dir", "") + file_path = os.path.join(weechat_dir, os.path.expanduser(args)) + if channel.type == "team": + w.prnt("", "ERROR: Can't upload a file to the team buffer") + return w.WEECHAT_RC_ERROR -def process_bot_disable(message_json): - process_bot_integration(message_json) + if not os.path.isfile(file_path): + unescaped_file_path = file_path.replace(r"\ ", " ") + if os.path.isfile(unescaped_file_path): + file_path = unescaped_file_path + else: + w.prnt("", "ERROR: Could not find file: {}".format(file_path)) + return w.WEECHAT_RC_ERROR + post_data = { + "channels": channel.identifier, + } + if isinstance(channel, SlackThreadChannel): + post_data["thread_ts"] = channel.thread_ts + + request = SlackRequest(channel.team, "files.upload", post_data, channel=channel) + options = request.options_as_cli_args() + [ + "-s", + "-Ffile=@{}".format(file_path), + request.request_string(), + ] + + proxy_string = ProxyWrapper().curl() + if proxy_string: + options.append(proxy_string) + + options_hashtable = {"arg{}".format(i + 1): arg for i, arg in enumerate(options)} + w.hook_process_hashtable( + "curl", options_hashtable, config.slack_timeout, "upload_callback", "" + ) + return w.WEECHAT_RC_OK_EAT -def process_bot_integration(message_json): - server = servers.find(message_json["_server"]) - channel = server.channels.find(message_json["channel"]) - time = message_json['ts'] - text = "{} {}".format(server.users.find(message_json['user']).formatted_name(), - render_message(message_json)) - bot_name = get_user(message_json, server) - bot_name = bot_name.encode('utf-8') - channel.buffer_prnt(bot_name, text, time) +command_upload.completion = "%(filename) %-" -# todo: does this work? -def process_error(message_json): - pass +@utf8_decode +def upload_callback(data, command, return_code, out, err): + if return_code != 0: + w.prnt( + "", + "ERROR: Couldn't upload file. Got return code {}. Error: {}".format( + return_code, err + ), + ) + return w.WEECHAT_RC_OK_EAT -def process_reaction_added(message_json): - if message_json["item"].get("type") == "message": - channel = channels.find(message_json["item"]["channel"]) - channel.add_reaction(message_json["item"]["ts"], message_json["reaction"], message_json["user"]) - else: - dbg("Reaction to item type not supported: " + str(message_json)) + try: + response = json.loads(out) + except JSONDecodeError: + w.prnt( + "", "ERROR: Couldn't process response from file upload. Got: {}".format(out) + ) + return w.WEECHAT_RC_OK_EAT -def process_reaction_removed(message_json): - if message_json["item"].get("type") == "message": - channel = channels.find(message_json["item"]["channel"]) - channel.remove_reaction(message_json["item"]["ts"], message_json["reaction"], message_json["user"]) - else: - dbg("Reaction to item type not supported: " + str(message_json)) + if not response["ok"]: + w.prnt("", "ERROR: Couldn't upload file. Error: {}".format(response["error"])) + return w.WEECHAT_RC_OK_EAT -def create_reaction_string(reactions): - count = 0 - if not isinstance(reactions, list): - reaction_string = " [{}]".format(reactions) - else: - reaction_string = ' [' - for r in reactions: - if len(r["users"]) > 0: - count += 1 - if show_reaction_nicks: - nicks = [resolve_ref("@{}".format(user)) for user in r["users"]] - users = "({})".format(",".join(nicks)) - else: - users = len(r["users"]) - reaction_string += ":{}:{} ".format(r["name"], users) - reaction_string = reaction_string[:-1] + ']' - if count == 0: - reaction_string = '' - return reaction_string - -def modify_buffer_line(buffer, new_line, time): - time = int(float(time)) - # get a pointer to this buffer's lines - own_lines = w.hdata_pointer(w.hdata_get('buffer'), buffer, 'own_lines') - if own_lines: - #get a pointer to the last line - line_pointer = w.hdata_pointer(w.hdata_get('lines'), own_lines, 'last_line') - #hold the structure of a line and of line data - struct_hdata_line = w.hdata_get('line') - struct_hdata_line_data = w.hdata_get('line_data') - - while line_pointer: - #get a pointer to the data in line_pointer via layout of struct_hdata_line - data = w.hdata_pointer(struct_hdata_line, line_pointer, 'data') - if data: - date = w.hdata_time(struct_hdata_line_data, data, 'date') - prefix = w.hdata_string(struct_hdata_line_data, data, 'prefix') - - if int(date) == int(time): - #w.prnt("", "found matching time date is {}, time is {} ".format(date, time)) - w.hdata_update(struct_hdata_line_data, data, {"message": new_line}) - break - else: - pass - #move backwards one line and try again - exit the while if you hit the end - line_pointer = w.hdata_move(struct_hdata_line, line_pointer, -1) - return w.WEECHAT_RC_OK -def render_message(message_json, force=False): - global unfurl_ignore_alt_text - #If we already have a rendered version in the object, just return that. - if not force and message_json.get("_rendered_text", ""): - return message_json["_rendered_text"] +@utf8_decode +def away_command_cb(data, current_buffer, args): + all_servers, message = re.match("^/away( -all)? ?(.*)", args).groups() + if all_servers: + team_buffers = [team.channel_buffer for team in EVENTROUTER.teams.values()] + elif current_buffer in EVENTROUTER.weechat_controller.buffers: + team_buffers = [current_buffer] else: - server = servers.find(message_json["_server"]) + return w.WEECHAT_RC_OK - if "fallback" in message_json: - text = message_json["fallback"] - elif "text" in message_json: - if message_json['text'] is not None: - text = message_json["text"] - else: - text = u"" + for team_buffer in team_buffers: + if message: + command_away(data, team_buffer, args) else: - text = u"" - - text = unfurl_refs(text, ignore_alt_text=unfurl_ignore_alt_text) - - text_before = (len(text) > 0) - text += unfurl_refs(unwrap_attachments(message_json, text_before), ignore_alt_text=unfurl_ignore_alt_text) + command_back(data, team_buffer, args) + return w.WEECHAT_RC_OK - text = text.lstrip() - text = text.replace("\t", " ") - text = text.replace("<", "<") - text = text.replace(">", ">") - text = text.replace("&", "&") - text = text.encode('utf-8') - if "reactions" in message_json: - text += create_reaction_string(message_json["reactions"]) - message_json["_rendered_text"] = text - return text +@slack_buffer_required +@utf8_decode +def command_away(data, current_buffer, args): + """ + /slack away + Sets your status as 'away'. + """ + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + s = SlackRequest(team, "users.setPresence", {"presence": "away"}) + EVENTROUTER.receive(s) + return w.WEECHAT_RC_OK -def process_message(message_json, cache=True): - try: - # send these subtype messages elsewhere - known_subtypes = ["message_changed", 'message_deleted', 'channel_join', 'channel_leave', 'channel_topic', 'group_join', 'group_leave', 'group_topic', 'bot_enable', 'bot_disable'] - if "subtype" in message_json and message_json["subtype"] in known_subtypes: - proc[message_json["subtype"]](message_json) +@slack_buffer_required +@utf8_decode +def command_status(data, current_buffer, args): + """ + /slack status [ []|-delete] + Lets you set your Slack Status (not to be confused with away/here). + Prints current status if no arguments are given, unsets the status if -delete is given. + """ + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + + split_args = args.split(" ", 1) + if not split_args[0]: + profile = team.users[team.myidentifier].profile + team.buffer_prnt( + "Status: {} {}".format( + replace_string_with_emoji(profile.get("status_emoji", "")), + profile.get("status_text", ""), + ) + ) + return w.WEECHAT_RC_OK - else: - server = servers.find(message_json["_server"]) - channel = channels.find(message_json["channel"]) + emoji = "" if split_args[0] == "-delete" else split_args[0] + text = split_args[1] if len(split_args) > 1 else "" + new_profile = {"status_text": text, "status_emoji": emoji} - #do not process messages in unexpected channels - if not channel.active: - channel.open(False) - dbg("message came for closed channel {}".format(channel.name)) - return + s = SlackRequest(team, "users.profile.set", {"profile": new_profile}) + EVENTROUTER.receive(s) + return w.WEECHAT_RC_OK - time = message_json['ts'] - text = render_message(message_json) - name = get_user(message_json, server) - name = name.encode('utf-8') - #special case with actions. - if text.startswith("_") and text.endswith("_"): - text = text[1:-1] - if name != channel.server.nick: - text = name + " " + text - channel.buffer_prnt(w.prefix("action").rstrip(), text, time) +command_status.completion = "-delete|%(emoji) %-" - else: - suffix = '' - if 'edited' in message_json: - suffix = ' (edited)' - channel.buffer_prnt(name, text + suffix, time) - - - if cache: - channel.cache_message(message_json) - - except Exception: - channel = channels.find(message_json["channel"]) - dbg("cannot process message {}\n{}".format(message_json, traceback.format_exc())) - if channel and ("text" in message_json) and message_json['text'] is not None: - channel.buffer_prnt('unknown', message_json['text']) - - -def process_message_changed(message_json): - m = message_json["message"] - if "message" in message_json: - if "attachments" in m: - message_json["attachments"] = m["attachments"] - if "text" in m: - if "text" in message_json: - message_json["text"] += m["text"] - dbg("added text!") - else: - message_json["text"] = m["text"] - if "fallback" in m: - if "fallback" in message_json: - message_json["fallback"] += m["fallback"] - else: - message_json["fallback"] = m["fallback"] - text_before = (len(m['text']) > 0) - m["text"] += unwrap_attachments(message_json, text_before) - channel = channels.find(message_json["channel"]) - if "edited" in m: - channel.change_message(m["ts"], m["text"], ' (edited)') +@utf8_decode +def line_event_cb(data, signal, hashtable): + tags = hashtable["_chat_line_tags"].split(",") + for tag in tags: + if tag.startswith("slack_ts_"): + ts = SlackTS(tag[9:]) + break else: - channel.change_message(m["ts"], m["text"]) - + return w.WEECHAT_RC_OK -def process_message_deleted(message_json): - channel = channels.find(message_json["channel"]) - channel.change_message(message_json["deleted_ts"], "(deleted)") + buffer_pointer = hashtable["_buffer"] + channel = EVENTROUTER.weechat_controller.buffers.get(buffer_pointer) + if isinstance(channel, SlackChannelCommon): + message_hash = channel.hashed_messages[ts] + if message_hash is None: + return w.WEECHAT_RC_OK + message_hash = "$" + message_hash -def unwrap_attachments(message_json, text_before): - attachment_text = '' - if "attachments" in message_json: - if text_before: - attachment_text = u'\n' - for attachment in message_json["attachments"]: - # Attachments should be rendered roughly like: - # - # $pretext - # $author: (if rest of line is non-empty) $title ($title_link) OR $from_url - # $author: (if no $author on previous line) $text - # $fields - t = [] - prepend_title_text = '' - if 'author_name' in attachment: - prepend_title_text = attachment['author_name'] + ": " - if 'pretext' in attachment: - t.append(attachment['pretext']) - if "title" in attachment: - if 'title_link' in attachment: - t.append('%s%s (%s)' % (prepend_title_text, attachment["title"], attachment["title_link"],)) - else: - t.append(prepend_title_text + attachment["title"]) - prepend_title_text = '' - elif "from_url" in attachment: - t.append(attachment["from_url"]) - if "text" in attachment: - tx = re.sub(r' *\n[\n ]+', '\n', attachment["text"]) - t.append(prepend_title_text + tx) - prepend_title_text = '' - if 'fields' in attachment: - for f in attachment['fields']: - if f['title'] != '': - t.append('%s %s' % (f['title'], f['value'],)) - else: - t.append(f['value']) - if t == [] and "fallback" in attachment: - t.append(attachment["fallback"]) - attachment_text += "\n".join([x.strip() for x in t if x]) - return attachment_text + if data == "auto": + reaction = EMOJI_CHAR_OR_NAME_REGEX.match(hashtable["_chat_eol"]) + if reaction: + emoji = reaction.group("emoji_char") or reaction.group("emoji_name") + channel.send_change_reaction("toggle", message_hash, emoji) + else: + data = "message" + if data == "message": + w.command(buffer_pointer, "/cursor stop") + w.command(buffer_pointer, "/input insert {}".format(message_hash)) + elif data == "delete": + w.command(buffer_pointer, "/input send {}s///".format(message_hash)) + elif data == "linkarchive": + w.command(buffer_pointer, "/cursor stop") + w.command(buffer_pointer, "/slack linkarchive {}".format(message_hash)) + elif data == "reply": + w.command(buffer_pointer, "/cursor stop") + w.command( + buffer_pointer, "/input insert /reply {}\\x20".format(message_hash) + ) + elif data == "thread": + w.command(buffer_pointer, "/cursor stop") + w.command(buffer_pointer, "/thread {}".format(message_hash)) + return w.WEECHAT_RC_OK -def resolve_ref(ref): - if ref.startswith('@U'): - if users.find(ref[1:]): - try: - return "@{}".format(users.find(ref[1:]).name) - except: - dbg("NAME: {}".format(ref)) - elif ref.startswith('#C'): - if channels.find(ref[1:]): - try: - return "{}".format(channels.find(ref[1:]).name) - except: - dbg("CHANNEL: {}".format(ref)) +@utf8_decode +def info_slack_message_cb(data, info_name, args): + current_channel = EVENTROUTER.weechat_controller.buffers.get(w.current_buffer()) + message = current_channel.message_from_hash_or_index(args) - # Something else, just return as-is - return ref + if not message: + print_message_not_found_error(args) + return "" + return message.render() -def unfurl_ref(ref, ignore_alt_text=False): - id = ref.split('|')[0] - display_text = ref - if ref.find('|') > -1: - if ignore_alt_text: - display_text = resolve_ref(id) - else: - if id.startswith("#C") or id.startswith("@U"): - display_text = ref.split('|')[1] - else: - url, desc = ref.split('|', 1) - display_text = u"{} ({})".format(url, desc) - else: - display_text = resolve_ref(ref) - return display_text -def unfurl_refs(text, ignore_alt_text=False): +@slack_buffer_required +@utf8_decode +def command_back(data, current_buffer, args): """ - input : <@U096Q7CQM|someuser> has joined the channel - ouput : someuser has joined the channel + /slack back + Sets your status as 'back'. """ - # Find all strings enclosed by <> - # - - # - <#C2147483705|#otherchannel> - # - <@U2147483697|@othernick> - # Test patterns lives in ./_pytest/test_unfurl.py - matches = re.findall(r"(<[@#]?(?:[^<]*)>)", text) - for m in matches: - # Replace them with human readable strings - text = text.replace(m, unfurl_ref(m[1:-1], ignore_alt_text)) - return text + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + s = SlackRequest(team, "users.setPresence", {"presence": "auto"}) + EVENTROUTER.receive(s) + set_own_presence_active(team) + return w.WEECHAT_RC_OK -def get_user(message_json, server): - if 'bot_id' in message_json and message_json['bot_id'] is not None: - name = u"{} :]".format(server.bots.find(message_json["bot_id"]).formatted_name()) - elif 'user' in message_json: - u = server.users.find(message_json['user']) - if u.is_bot: - name = u"{} :]".format(u.formatted_name()) - else: - name = u.name - elif 'username' in message_json: - name = u"-{}-".format(message_json["username"]) - elif 'service_name' in message_json: - name = u"-{}-".format(message_json["service_name"]) - else: - name = u"" - return name - -# END Websocket handling methods - - -def typing_bar_item_cb(data, buffer, args): - typers = [x for x in channels if x.is_someone_typing()] - if len(typers) > 0: - direct_typers = [] - channel_typers = [] - for dm in channels.find_by_class(DmChannel): - direct_typers.extend(dm.get_typing_list()) - direct_typers = ["D/" + x for x in direct_typers] - current_channel = w.current_buffer() - channel = channels.find(current_channel) - try: - if channel and channel.__class__ != DmChannel: - channel_typers = channels.find(current_channel).get_typing_list() - except: - w.prnt("", "Bug on {}".format(channel)) - typing_here = ", ".join(channel_typers + direct_typers) - if len(typing_here) > 0: - color = w.color('yellow') - return color + "typing: " + typing_here - return "" +@slack_buffer_required +@utf8_decode +def command_label(data, current_buffer, args): + """ + /label [-full] |-unset + Rename a channel or thread buffer. Note that this is not permanent, it will + only last as long as you keep the buffer and wee-slack open. Changes the + short_name by default, and the name and full_name if you use the -full + option. If you haven't set the short_name explicitly, that will also be + changed when using the -full option. Use the -unset option to set it back + to the default. + """ + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + split_args = args.split(None, 1) + if split_args[0] == "-full": + channel.label_full_drop_prefix = False + channel.label_full = split_args[1] if split_args[1] != "-unset" else None + else: + channel.label_short_drop_prefix = False + channel.label_short = args if args != "-unset" else None -def typing_update_cb(data, remaining_calls): - w.bar_item_update("slack_typing_notice") + channel.rename() return w.WEECHAT_RC_OK -def buffer_list_update_cb(data, remaining_calls): - global buffer_list_update - - now = time.time() - if buffer_list_update and previous_buffer_list_update + 1 < now: - gray_check = False - if len(servers) > 1: - gray_check = True - for channel in channels: - channel.rename() - buffer_list_update = False - return w.WEECHAT_RC_OK +command_label.completion = "-unset|-full -unset %-" -def buffer_list_update_next(): - global buffer_list_update - buffer_list_update = True -def hotlist_cache_update_cb(data, remaining_calls): - # this keeps the hotlist dupe up to date for the buffer switch, but is prob technically a race condition. (meh) - global hotlist - prev_hotlist = hotlist - hotlist = w.infolist_get("hotlist", "", "") - w.infolist_free(prev_hotlist) +@utf8_decode +def set_unread_cb(data, current_buffer, command): + for channel in EVENTROUTER.weechat_controller.buffers.values(): + channel.mark_read() return w.WEECHAT_RC_OK -def buffer_closing_cb(signal, sig_type, data): - if channels.find(data): - channels.find(data).closed() +@slack_buffer_or_ignore +@utf8_decode +def set_unread_current_buffer_cb(data, current_buffer, command): + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] + channel.mark_read() return w.WEECHAT_RC_OK -def buffer_switch_cb(signal, sig_type, data): - global previous_buffer, hotlist - # this is to see if we need to gray out things in the buffer list - if channels.find(previous_buffer): - channels.find(previous_buffer).mark_read() - - channel_name = current_buffer_name() - previous_buffer = data - return w.WEECHAT_RC_OK - +###### NEW EXCEPTIONS -def typing_notification_cb(signal, sig_type, data): - if len(w.buffer_get_string(data, "input")) > 8: - global typing_timer - now = time.time() - if typing_timer + 4 < now: - channel = channels.find(current_buffer_name()) - if channel: - identifier = channel.identifier - request = {"type": "typing", "channel": identifier} - channel.server.send_to_websocket(request, expect_reply=False) - typing_timer = now - return w.WEECHAT_RC_OK -def slack_ping_cb(data, remaining): +class InvalidType(Exception): """ - Periodic websocket ping to detect broken connection. + Raised when we do type checking to ensure objects of the wrong + type are not used improperly. """ - servers.find(data).ping() - return w.WEECHAT_RC_OK + def __init__(self, type_str): + super(InvalidType, self).__init__(type_str) -def slack_connection_persistence_cb(data, remaining_calls): - """ - Reconnect if a connection is detected down - """ - for server in servers: - if not server.connected: - server.buffer_prnt("Disconnected from slack, trying to reconnect..") - if server.ws_hook is not None: - w.unhook(server.ws_hook) - server.connect_to_slack() - return w.WEECHAT_RC_OK + +###### New but probably old and need to migrate -def slack_never_away_cb(data, remaining): - global never_away - if never_away: - for server in servers: - identifier = server.channels.find("slackbot").identifier - request = {"type": "typing", "channel": identifier} - #request = {"type":"typing","channel":"slackbot"} - server.send_to_websocket(request, expect_reply=False) +def closed_slack_debug_buffer_cb(data, buffer): + global slack_debug + slack_debug = None return w.WEECHAT_RC_OK -def nick_completion_cb(data, completion_item, buffer, completion): - """ - Adds all @-prefixed nicks to completion list - """ +def create_slack_debug_buffer(): + global slack_debug, debug_string + if slack_debug is None: + debug_string = None + slack_debug = w.buffer_new( + "slack-debug", "", "", "closed_slack_debug_buffer_cb", "" + ) + w.buffer_set(slack_debug, "print_hooks_enabled", "0") + w.buffer_set(slack_debug, "notify", "0") + w.buffer_set(slack_debug, "highlight_tags_restrict", "highlight_force") - channel = channels.find(buffer) - if channel is None or channel.members is None: - return w.WEECHAT_RC_OK - for m in channel.members: - user = channel.server.users.find(m) - w.hook_completion_list_add(completion, "@" + user.name, 1, w.WEECHAT_LIST_POS_SORT) - return w.WEECHAT_RC_OK +def load_emoji(): + try: + weechat_dir = w.info_get("weechat_data_dir", "") or w.info_get( + "weechat_dir", "" + ) + weechat_sharedir = w.info_get("weechat_sharedir", "") + local_weemoji, global_weemoji = ( + "{}/weemoji.json".format(path) for path in (weechat_dir, weechat_sharedir) + ) + path = ( + global_weemoji + if os.path.exists(global_weemoji) and not os.path.exists(local_weemoji) + else local_weemoji + ) + with open(path, "r") as ef: + emojis = json.loads(ef.read()) + if "emoji" in emojis: + print_error( + "The weemoji.json file is in an old format. Please update it." + ) + else: + emoji_unicode = {key: value["unicode"] for key, value in emojis.items()} + + emoji_skin_tones = { + skin_tone["name"]: skin_tone["unicode"] + for emoji in emojis.values() + for skin_tone in emoji.get("skinVariations", {}).values() + } + + emoji_with_skin_tones = chain( + emoji_unicode.items(), emoji_skin_tones.items() + ) + emoji_with_skin_tones_reverse = {v: k for k, v in emoji_with_skin_tones} + return emoji_unicode, emoji_with_skin_tones_reverse + except: + dbg("Couldn't load emoji list: {}".format(format_exc_only()), 5) + return {}, {} + + +def parse_help_docstring(cmd): + doc = textwrap.dedent(cmd.__doc__).strip().split("\n", 1) + cmd_line = doc[0].split(None, 1) + args = "".join(cmd_line[1:]) + return cmd_line[0], args, doc[1].strip() + + +def setup_hooks(): + w.bar_item_new("slack_typing_notice", "(extra)typing_bar_item_cb", "") + w.bar_item_new("away", "(extra)away_bar_item_cb", "") + w.bar_item_new("slack_away", "(extra)away_bar_item_cb", "") + + w.hook_timer(5000, 0, 0, "ws_ping_cb", "") + w.hook_timer(1000, 0, 0, "typing_update_cb", "") + w.hook_timer(1000, 0, 0, "buffer_list_update_callback", "") + w.hook_timer(3000, 0, 0, "reconnect_callback", "EVENTROUTER") + w.hook_timer(1000 * 60 * 5, 0, 0, "slack_never_away_cb", "") + + w.hook_signal("buffer_closing", "buffer_closing_callback", "") + w.hook_signal("buffer_renamed", "buffer_renamed_cb", "") + w.hook_signal("buffer_switch", "buffer_switch_callback", "") + w.hook_signal("window_switch", "buffer_switch_callback", "") + w.hook_signal("quit", "quit_notification_callback", "") + if config.send_typing_notice: + w.hook_signal("input_text_changed", "typing_notification_cb", "") + + command_help.completion = "|".join(EVENTROUTER.cmds.keys()) + completions = "||".join( + "{} {}".format(name, getattr(cmd, "completion", "")) + for name, cmd in EVENTROUTER.cmds.items() + ) + + w.hook_command( + # Command name and description + "slack", + "Plugin to allow typing notification and sync of read markers for slack.com", + # Usage + " []", + # Description of arguments + "Commands:\n" + + "\n".join(sorted(EVENTROUTER.cmds.keys())) + + "\nUse /slack help to find out more\n", + # Completions + completions, + # Function name + "slack_command_cb", + "", + ) + + w.hook_command_run("/me", "me_command_cb", "") + w.hook_command_run("/query", "join_query_command_cb", "") + w.hook_command_run("/join", "join_query_command_cb", "") + w.hook_command_run("/part", "part_command_cb", "") + w.hook_command_run("/topic", "topic_command_cb", "") + w.hook_command_run("/msg", "msg_command_cb", "") + w.hook_command_run("/invite", "invite_command_cb", "") + w.hook_command_run("/input complete_next", "complete_next_cb", "") + w.hook_command_run("/input set_unread", "set_unread_cb", "") + w.hook_command_run( + "/input set_unread_current_buffer", "set_unread_current_buffer_cb", "" + ) + w.hook_command_run("/buffer set unread", "set_unread_current_buffer_cb", "") + w.hook_command_run("/away", "away_command_cb", "") + w.hook_command_run("/whois", "whois_command_cb", "") + + for cmd_name in ["hide", "label", "rehistory", "reply", "thread"]: + cmd = EVENTROUTER.cmds[cmd_name] + _, args, description = parse_help_docstring(cmd) + completion = getattr(cmd, "completion", "") + w.hook_command( + cmd_name, description, args, "", completion, "command_" + cmd_name, "" + ) + + w.hook_completion( + "irc_channel_topic", "complete topic for slack", "topic_completion_cb", "" + ) + w.hook_completion( + "irc_channels", "complete channels for slack", "channel_completion_cb", "" + ) + w.hook_completion( + "irc_privates", "complete dms/mpdms for slack", "dm_completion_cb", "" + ) + w.hook_completion("nicks", "complete @-nicks for slack", "nick_completion_cb", "") + w.hook_completion( + "threads", "complete thread ids for slack", "thread_completion_cb", "" + ) + w.hook_completion( + "usergroups", "complete @-usergroups for slack", "usergroups_completion_cb", "" + ) + w.hook_completion("emoji", "complete :emoji: for slack", "emoji_completion_cb", "") + + w.key_bind( + "mouse", + { + "@chat(python.*):button2": "hsignal:slack_mouse", + }, + ) + w.key_bind( + "cursor", + { + "@chat(python.*):D": "hsignal:slack_cursor_delete", + "@chat(python.*):L": "hsignal:slack_cursor_linkarchive", + "@chat(python.*):M": "hsignal:slack_cursor_message", + "@chat(python.*):R": "hsignal:slack_cursor_reply", + "@chat(python.*):T": "hsignal:slack_cursor_thread", + }, + ) + + w.hook_hsignal("slack_mouse", "line_event_cb", "auto") + w.hook_hsignal("slack_cursor_delete", "line_event_cb", "delete") + w.hook_hsignal("slack_cursor_linkarchive", "line_event_cb", "linkarchive") + w.hook_hsignal("slack_cursor_message", "line_event_cb", "message") + w.hook_hsignal("slack_cursor_reply", "line_event_cb", "reply") + w.hook_hsignal("slack_cursor_thread", "line_event_cb", "thread") + + w.hook_info( + "slack_message", + "get contents of a slack message", + "id or count to the message", + "info_slack_message_cb", + "", + ) + + # Hooks to fix/implement + # w.hook_signal('buffer_opened', "buffer_opened_cb", "") + # w.hook_signal('window_scrolled', "scrolled_cb", "") + # w.hook_timer(3000, 0, 0, "slack_connection_persistence_cb", "") + + +##### END NEW + + +def dbg(message, level=0, main_buffer=False, fout=False): + """ + send debug output to the slack-debug buffer and optionally write to a file. + """ + # TODO: do this smarter + if level >= config.debug_level: + global debug_string + message = "DEBUG: {}".format(message) + if fout: + with open("/tmp/debug.log", "a+") as log_file: + log_file.writelines(message + "\n") + if main_buffer: + w.prnt("", "slack: " + message) + else: + if slack_debug and (not debug_string or debug_string in message): + w.prnt(slack_debug, message) + + +###### Config code +class PluginConfig(object): + Setting = namedtuple("Setting", ["default", "desc"]) + # Default settings. + # These are, initially, each a (default, desc) tuple; the former is the + # default value of the setting, in the (string) format that weechat + # expects, and the latter is the user-friendly description of the setting. + # At __init__ time these values are extracted, the description is used to + # set or update the setting description for use with /help, and the default + # value is used to set the default for any settings not already defined. + # Following this procedure, the keys remain the same, but the values are + # the real (python) values of the settings. + default_settings = { + "auto_open_threads": Setting( + default="false", + desc="Automatically open threads when mentioned or in" + " response to own messages.", + ), + "background_load_all_history": Setting( + default="true", + desc="Load the history for all channels in the background when the script is loaded," + " rather than waiting until the buffer is switched to. You can set this to false if" + " you experience performance issues, however that causes some loss of functionality," + " see known issues in the readme.", + ), + "channel_name_typing_indicator": Setting( + default="true", + desc="Change the prefix of a channel from # to > when someone is" + " typing in it. Note that this will (temporarily) affect the sort" + " order if you sort buffers by name rather than by number.", + ), + "color_buflist_muted_channels": Setting( + default="darkgray", desc="Color to use for muted channels in the buflist" + ), + "color_deleted": Setting( + default="red", desc="Color to use for deleted messages and files." + ), + "color_edited_suffix": Setting( + default="095", + desc="Color to use for (edited) suffix on messages that have been edited.", + ), + "color_reaction_suffix": Setting( + default="darkgray", + desc="Color to use for the [:wave:(@user)] suffix on messages that" + " have reactions attached to them.", + ), + "color_reaction_suffix_added_by_you": Setting( + default="blue", desc="Color to use for reactions that you have added." + ), + "color_thread_suffix": Setting( + default="lightcyan", + desc="Color to use for the [thread: XXX] suffix on messages that" + ' have threads attached to them. The special value "multiple" can' + " be used to use a different color for each thread.", + ), + "color_typing_notice": Setting( + default="yellow", desc="Color to use for the typing notice." + ), + "colorize_attachments": Setting( + default="prefix", + desc='Whether to colorize attachment lines. Values: "prefix": Only colorize' + ' the prefix, "all": Colorize the whole line, "none": Don\'t colorize.', + ), + "colorize_private_chats": Setting( + default="false", desc="Whether to use nick-colors in DM windows." + ), + "debug_level": Setting( + default="3", + desc="Show only this level of debug info (or higher) when" + " debug_mode is on. Lower levels -> more messages.", + ), + "debug_mode": Setting( + default="false", + desc="Open a dedicated buffer for debug messages and start logging" + " to it. How verbose the logging is depends on log_level.", + ), + "distracting_channels": Setting(default="", desc="List of channels to hide."), + "external_user_suffix": Setting( + default="*", desc="The suffix appended to nicks to indicate external users." + ), + "files_download_location": Setting( + default="", + desc="If set, file attachments will be automatically downloaded" + ' to this location. "%h" will be replaced by WeeChat home,' + ' "~/.weechat" by default. Requires WeeChat 2.2 or newer.', + ), + "group_name_prefix": Setting( + default="&", + desc="The prefix of buffer names for groups (private channels).", + ), + "history_fetch_count": Setting( + default="200", + desc="The number of messages to fetch for each channel when fetching" + " history, between 1 and 1000.", + ), + "link_previews": Setting( + default="true", desc="Show previews of website content linked by teammates." + ), + "log_channel_created": Setting( + default="true", + desc='Log "Channel created" in the Server buffer.', + ), + "map_underline_to": Setting( + default="_", + desc="When sending underlined text to slack, use this formatting" + ' character for it. The default ("_") sends it as italics. Use' + ' "*" to send bold instead.', + ), + "muted_channels_activity": Setting( + default="personal_highlights", + desc="Control which activity you see from muted channels, either" + " none, personal_highlights, all_highlights or all. none: Don't" + " show any activity. personal_highlights: Only show personal" + " highlights, i.e. not @channel and @here. all_highlights: Show" + " all highlights, but not other messages. all: Show all activity," + " like other channels.", + ), + "never_away": Setting( + default="false", + desc='Poke Slack every five minutes so that it never marks you "away".', + ), + "notify_subscribed_threads": Setting( + default="auto", + desc="Control if you want to see a notification in the team buffer when a" + " thread you're subscribed to receives a new message, either auto, true or" + " false. auto means that you only get a notification if auto_open_threads" + " and thread_messages_in_channel both are false. Defaults to auto.", + ), + "notify_usergroup_handle_updated": Setting( + default="false", + desc="Control if you want to see a notification in the team buffer when a" + "usergroup's handle has changed, either true or false.", + ), + "record_events": Setting( + default="false", desc="Log all traffic from Slack to disk as JSON." + ), + "render_bold_as": Setting( + default="bold", + desc="When receiving bold text from Slack, render it as this in WeeChat.", + ), + "render_emoji_as_string": Setting( + default="false", + desc="Render emojis as :emoji_name: instead of emoji characters. Enable this" + " if your terminal doesn't support emojis, or set to 'both' if you want to" + " see both renderings. Note that even though this is" + " disabled by default, you need to place {}/blob/master/weemoji.json in your" + " WeeChat directory to enable rendering emojis as emoji characters.".format( + REPO_URL + ), + ), + "render_italic_as": Setting( + default="italic", + desc="When receiving bold text from Slack, render it as this in WeeChat." + ' If your terminal lacks italic support, consider using "underline" instead.', + ), + "send_typing_notice": Setting( + default="true", + desc="Alert Slack users when you are typing a message in the input bar " + "(Requires reload)", + ), + "server_aliases": Setting( + default="", + desc="A comma separated list of `subdomain:alias` pairs. The alias" + " will be used instead of the actual name of the slack (in buffer" + " names, logging, etc). E.g `work:no_fun_allowed` would make your" + " work slack show up as `no_fun_allowed` rather than `work.slack.com`.", + ), + "shared_name_prefix": Setting( + default="%", desc="The prefix of buffer names for shared channels." + ), + "short_buffer_names": Setting( + default="false", + desc="Use `foo.#channel` rather than `foo.slack.com.#channel` as the" + " internal name for Slack buffers.", + ), + "show_buflist_presence": Setting( + default="true", + desc="Display a `+` character in the buffer list for present users.", + ), + "show_reaction_nicks": Setting( + default="false", + desc="Display the name of the reacting user(s) alongside each reactji.", + ), + "slack_api_token": Setting( + default="INSERT VALID KEY HERE!", + desc="List of Slack API tokens, one per Slack instance you want to" + " connect to; see the README for details on how to get these" + " (note: content is evaluated, see /help eval).", + ), + "slack_timeout": Setting( + default="20000", desc="How long (ms) to wait when communicating with Slack." + ), + "switch_buffer_on_join": Setting( + default="true", + desc="When /joining a channel, automatically switch to it as well.", + ), + "thread_broadcast_prefix": Setting( + default="+ ", + desc="Prefix to distinguish thread messages that were also sent " + "to the channel, when thread_messages_in_channel is enabled.", + ), + "thread_messages_in_channel": Setting( + default="false", + desc="When enabled shows thread messages in the parent channel.", + ), + "unfurl_auto_link_display": Setting( + default="both", + desc='When displaying ("unfurling") links to channels/users/etc,' + " determine what is displayed when the text matches the url" + " without the protocol. This happens when Slack automatically" + " creates links, e.g. from words separated by dots or email" + ' addresses. Set it to "text" to only display the text written by' + ' the user, "url" to only display the url or "both" (the default)' + " to display both.", + ), + "unfurl_ignore_alt_text": Setting( + default="false", + desc='When displaying ("unfurling") links to channels/users/etc,' + ' ignore the "alt text" present in the message and instead use the' + " canonical name of the thing being linked to.", + ), + "unhide_buffers_with_activity": Setting( + default="false", + desc="When activity occurs on a buffer, unhide it even if it was" + " previously hidden (whether by the user or by the" + " distracting_channels setting).", + ), + "use_full_names": Setting( + default="false", + desc="Use full names as the nicks for all users. When this is" + " false (the default), display names will be used if set, with a" + " fallback to the full name if display name is not set.", + ), + "use_usernames": Setting( + default="false", + desc="Use usernames as the nicks for all users. Takes priority" + " over use_full_names. Default false.", + ), + } -def complete_next_cb(data, buffer, command): - """Extract current word, if it is equal to a nick, prefix it with @ and - rely on nick_completion_cb adding the @-prefixed versions to the - completion lists, then let Weechat's internal completion do its - thing + # Set missing settings to their defaults. Load non-missing settings from + # weechat configs. + def __init__(self): + self.settings = {} + # Set all descriptions, replace the values in the dict with the + # default setting value rather than the (setting,desc) tuple. + for key, (default, desc) in self.default_settings.items(): + w.config_set_desc_plugin(key, desc) + self.settings[key] = default + + # Migrate settings from old versions of Weeslack... + self.migrate() + # ...and then set anything left over from the defaults. + for key, default in self.settings.items(): + if not w.config_get_plugin(key): + w.config_set_plugin(key, default) + self.config_changed(None, None, None) - """ + def __str__(self): + return "".join( + [x + "\t" + str(self.settings[x]) + "\n" for x in self.settings.keys()] + ) + + def config_changed(self, data, full_key, value): + if full_key is None: + for key in self.settings: + self.settings[key] = self.fetch_setting(key) + else: + key = full_key.replace(CONFIG_PREFIX + ".", "") + self.settings[key] = self.fetch_setting(key) - channel = channels.find(buffer) - if channel is None or channel.members is None: + if ( + full_key is None or full_key == CONFIG_PREFIX + ".debug_mode" + ) and self.debug_mode: + create_slack_debug_buffer() return w.WEECHAT_RC_OK - input = w.buffer_get_string(buffer, "input") - current_pos = w.buffer_get_integer(buffer, "input_pos") - 1 - input_length = w.buffer_get_integer(buffer, "input_length") - word_start = 0 - word_end = input_length - # If we're on a non-word, look left for something to complete - while current_pos >= 0 and input[current_pos] != '@' and not input[current_pos].isalnum(): - current_pos = current_pos - 1 - if current_pos < 0: - current_pos = 0 - for l in range(current_pos, 0, -1): - if input[l] != '@' and not input[l].isalnum(): - word_start = l + 1 - break - for l in range(current_pos, input_length): - if not input[l].isalnum(): - word_end = l - break - word = input[word_start:word_end] - for m in channel.members: - user = channel.server.users.find(m) - if user.name == word: - # Here, we cheat. Insert a @ in front and rely in the @ - # nicks being in the completion list - w.buffer_set(buffer, "input", input[:word_start] + "@" + input[word_start:]) - w.buffer_set(buffer, "input_pos", str(w.buffer_get_integer(buffer, "input_pos") + 1)) - return w.WEECHAT_RC_OK_EAT - return w.WEECHAT_RC_OK -# Slack specific requests - -# NOTE: switched to async because sync slowed down the UI -def async_slack_api_request(domain, token, request, post_data, priority=False): - if not STOP_TALKING_TO_SLACK: - post_data["token"] = token - url = 'url:https://{}/api/{}?{}'.format(domain, request, urllib.urlencode(post_data)) - context = pickle.dumps({"request": request, "token": token, "post_data": post_data}) - params = { 'useragent': 'wee_slack {}'.format(SCRIPT_VERSION) } - dbg("URL: {} context: {} params: {}".format(url, context, params)) - w.hook_process_hashtable(url, params, 20000, "url_processor_cb", context) - -def async_slack_api_upload_request(token, request, post_data, priority=False): - if not STOP_TALKING_TO_SLACK: - url = 'https://slack.com/api/{}'.format(request) - file_path = os.path.expanduser(post_data["file"]) - command = 'curl -F file=@{} -F channels={} -F token={} {}'.format(file_path, post_data["channels"], token, url) - context = pickle.dumps({"request": request, "token": token, "post_data": post_data}) - w.hook_process(command, 20000, "url_processor_cb", context) - -# funny, right? -big_data = {} - -def url_processor_cb(data, command, return_code, out, err): - global big_data - data = pickle.loads(data) - identifier = sha.sha("{}{}".format(data, command)).hexdigest() - if identifier not in big_data: - big_data[identifier] = '' - big_data[identifier] += out - if return_code == 0: + def fetch_setting(self, key): try: - my_json = json.loads(big_data[identifier]) + return getattr(self, "get_" + key)(key) + except AttributeError: + # Most settings are on/off, so make get_boolean the default + return self.get_boolean(key) except: - dbg("request failed, doing again...") - dbg("response length: {} identifier {}\n{}".format(len(big_data[identifier]), identifier, data)) - my_json = False + # There was setting-specific getter, but it failed. + print(format_exc_tb()) + return self.settings[key] - big_data.pop(identifier, None) + def __getattr__(self, key): + try: + return self.settings[key] + except KeyError: + raise AttributeError(key) + + def get_boolean(self, key): + return w.config_string_to_boolean(w.config_get_plugin(key)) + + def get_string(self, key): + return w.config_get_plugin(key) + + def get_int(self, key): + return int(w.config_get_plugin(key)) + + def is_default(self, key): + default = self.default_settings.get(key).default + return w.config_get_plugin(key) == default + + get_color_buflist_muted_channels = get_string + get_color_deleted = get_string + get_color_edited_suffix = get_string + get_color_reaction_suffix = get_string + get_color_reaction_suffix_added_by_you = get_string + get_color_thread_suffix = get_string + get_color_typing_notice = get_string + get_colorize_attachments = get_string + get_debug_level = get_int + get_external_user_suffix = get_string + get_files_download_location = get_string + get_group_name_prefix = get_string + get_history_fetch_count = get_int + get_map_underline_to = get_string + get_muted_channels_activity = get_string + get_thread_broadcast_prefix = get_string + get_render_bold_as = get_string + get_render_italic_as = get_string + get_shared_name_prefix = get_string + get_slack_timeout = get_int + get_unfurl_auto_link_display = get_string + + def get_distracting_channels(self, key): + return [x.strip() for x in w.config_get_plugin(key).split(",") if x] + + def get_server_aliases(self, key): + alias_list = w.config_get_plugin(key) + return dict(item.split(":") for item in alias_list.split(",") if ":" in item) + + def get_slack_api_token(self, key): + token = w.config_get_plugin("slack_api_token") + if token.startswith("${sec.data"): + return w.string_eval_expression(token, {}, {}, {}) + else: + return token - if my_json: - if data["request"] == 'rtm.start': - servers.find(data["token"]).connected_to_slack(my_json) - servers.update_hashtable() + def get_string_or_boolean(self, key, *valid_strings): + value = w.config_get_plugin(key) + if value in valid_strings: + return value + return w.config_string_to_boolean(value) - else: - if "channel" in data["post_data"]: - channel = data["post_data"]["channel"] - token = data["token"] - if "messages" in my_json: - messages = my_json["messages"].reverse() - for message in my_json["messages"]: - message["_server"] = servers.find(token).domain - message["channel"] = servers.find(token).channels.find(channel).identifier - process_message(message) - if "channel" in my_json: - if "members" in my_json["channel"]: - channels.find(my_json["channel"]["id"]).members = set(my_json["channel"]["members"]) - else: - if return_code != -1: - big_data.pop(identifier, None) - dbg("return code: {}, data: {}, output: {}, error: {}".format(return_code, data, out, err)) + def get_notify_subscribed_threads(self, key): + return self.get_string_or_boolean(key, "auto") - return w.WEECHAT_RC_OK + def get_render_emoji_as_string(self, key): + return self.get_string_or_boolean(key, "both") -def cache_write_cb(data, remaining): - cache_file = open("{}/{}".format(WEECHAT_HOME, CACHE_NAME), 'w') - cache_file.write(CACHE_VERSION + "\n") - for channel in channels: - if channel.active: - for message in channel.messages: - cache_file.write("{}\n".format(json.dumps(message.message_json))) + def migrate(self): + """ + This is to migrate the extension name from slack_extension to slack + """ + if not w.config_get_plugin("migrated"): + for k in self.settings.keys(): + if not w.config_is_set_plugin(k): + p = w.config_get("{}_extension.{}".format(CONFIG_PREFIX, k)) + data = w.config_string(p) + if data != "": + w.config_set_plugin(k, data) + w.config_set_plugin("migrated", "true") + + old_thread_color_config = w.config_get_plugin("thread_suffix_color") + new_thread_color_config = w.config_get_plugin("color_thread_suffix") + if old_thread_color_config and not new_thread_color_config: + w.config_set_plugin("color_thread_suffix", old_thread_color_config) + + +def config_server_buffer_cb(data, key, value): + for team in EVENTROUTER.teams.values(): + team.buffer_merge(value) return w.WEECHAT_RC_OK -def cache_load(): - global message_cache - try: - file_name = "{}/{}".format(WEECHAT_HOME, CACHE_NAME) - cache_file = open(file_name, 'r') - if cache_file.readline() == CACHE_VERSION + "\n": - dbg("Loading messages from cache.", main_buffer=True) - for line in cache_file: - j = json.loads(line) - message_cache[j["channel"]].append(line) - dbg("Completed loading messages from cache.", main_buffer=True) - except ValueError: - w.prnt("", "Failed to load cache file, probably illegal JSON.. Ignoring") - pass - except IOError: - w.prnt("", "cache file not found") - pass - -# END Slack specific requests -# Utility Methods +# to Trace execution, add `setup_trace()` to startup +# and to a function and sys.settrace(trace_calls) to a function +def setup_trace(): + global f + now = time.time() + f = open("{}/{}-trace.json".format(RECORD_DIR, now), "w") -def current_domain_name(): - buffer = w.current_buffer() - if servers.find(buffer): - return servers.find(buffer).domain +def trace_calls(frame, event, arg): + global f + if event != "call": + return + co = frame.f_code + func_name = co.co_name + if func_name == "write": + # Ignore write() calls from print statements + return + func_line_no = frame.f_lineno + func_filename = co.co_filename + caller = frame.f_back + caller_line_no = caller.f_lineno + caller_filename = caller.f_code.co_filename + print( + "Call to %s on line %s of %s from line %s of %s" + % (func_name, func_line_no, func_filename, caller_line_no, caller_filename), + file=f, + ) + f.flush() + return + + +def get_rtm_connect_request(token, retries=3, team=None, callback=None): + return SlackRequest( + team, + "rtm.connect", + {"batch_presence_aware": 1}, + retries=retries, + token=token, + callback=callback, + ) + + +def get_next_page(response_json): + next_cursor = response_json.get("response_metadata", {}).get("next_cursor") + if next_cursor: + request = response_json["wee_slack_request_metadata"] + request.post_data["cursor"] = next_cursor + request.reset() + EVENTROUTER.receive(request) + return True else: - #number = w.buffer_get_integer(buffer, "number") - name = w.buffer_get_string(buffer, "name") - name = ".".join(name.split(".")[:-1]) - return name + return False -def current_buffer_name(short=False): - buffer = w.current_buffer() - #number = w.buffer_get_integer(buffer, "number") - name = w.buffer_get_string(buffer, "name") - if short: - try: - name = name.split('.')[-1] - except: - pass - return name +def initiate_connection(token): + initial_data = { + "channels": [], + "members": [], + "usergroups": [], + "remaining": { + "channels": 2, + "members": 1, + "usergroups": 1, + "prefs": 1, + "presence": 1, + }, + "errors": [], + } + def handle_initial(data_type): + def handle(response_json, eventrouter, team, channel, metadata): + if not response_json["ok"]: + if response_json["error"] == "user_is_restricted": + w.prnt( + "", + "You are a restricted user in this team, " + "{} not loaded".format(data_type), + ) + else: + initial_data["errors"].append( + "{}: {}".format(data_type, response_json["error"]) + ) + initial_data["remaining"][data_type] -= 1 + create_team(token, initial_data) + return -def closed_slack_buffer_cb(data, buffer): - global slack_buffer - slack_buffer = None - return w.WEECHAT_RC_OK + initial_data[data_type].extend(response_json[data_type]) + if not get_next_page(response_json): + initial_data["remaining"][data_type] -= 1 + create_team(token, initial_data) -def create_slack_buffer(): - global slack_buffer - slack_buffer = w.buffer_new("slack", "", "", "closed_slack_buffer_cb", "") - w.buffer_set(slack_buffer, "notify", "0") - #w.buffer_set(slack_buffer, "display", "1") - return w.WEECHAT_RC_OK + return handle + def handle_prefs(response_json, eventrouter, team, channel, metadata): + if not response_json["ok"]: + initial_data["errors"].append("prefs: {}".format(response_json["error"])) + initial_data["remaining"]["prefs"] -= 1 + create_team(token, initial_data) + return -def closed_slack_debug_buffer_cb(data, buffer): - global slack_debug - slack_debug = None - return w.WEECHAT_RC_OK + initial_data["prefs"] = response_json["prefs"] + initial_data["remaining"]["prefs"] -= 1 + create_team(token, initial_data) + def handle_getPresence(response_json, eventrouter, team, channel, metadata): + if not response_json["ok"]: + initial_data["errors"].append("presence: {}".format(response_json["error"])) + initial_data["remaining"]["presence"] -= 1 + create_team(token, initial_data) + return -def create_slack_debug_buffer(): - global slack_debug, debug_string - if slack_debug is not None: - w.buffer_set(slack_debug, "display", "1") + initial_data["presence"] = response_json + initial_data["remaining"]["presence"] -= 1 + create_team(token, initial_data) + + s = SlackRequest( + None, + "conversations.list", + { + "exclude_archived": True, + "types": "public_channel,private_channel,im", + "limit": 1000, + }, + token=token, + callback=handle_initial("channels"), + ) + EVENTROUTER.receive(s) + s = SlackRequest( + None, + "conversations.list", + { + "exclude_archived": True, + "types": "mpim", + "limit": 1000, + }, + token=token, + callback=handle_initial("channels"), + ) + EVENTROUTER.receive(s) + s = SlackRequest( + None, + "users.list", + {"limit": 1000}, + token=token, + callback=handle_initial("members"), + ) + EVENTROUTER.receive(s) + s = SlackRequest( + None, + "usergroups.list", + {"include_users": True}, + token=token, + callback=handle_initial("usergroups"), + ) + EVENTROUTER.receive(s) + s = SlackRequest( + None, + "users.prefs.get", + token=token, + callback=handle_prefs, + ) + EVENTROUTER.receive(s) + s = SlackRequest( + None, + "users.getPresence", + token=token, + callback=handle_getPresence, + ) + EVENTROUTER.receive(s) + + +def create_channel_from_info(eventrouter, channel_info, team, myidentifier, users): + if channel_info.get("is_im"): + return SlackDMChannel( + eventrouter, users, myidentifier, team=team, **channel_info + ) + elif channel_info.get("is_mpim"): + return SlackMPDMChannel( + eventrouter, users, myidentifier, team=team, **channel_info + ) + elif channel_info.get("is_shared"): + return SlackSharedChannel(eventrouter, team=team, **channel_info) + elif channel_info.get("is_private"): + return SlackPrivateChannel(eventrouter, team=team, **channel_info) else: - debug_string = None - slack_debug = w.buffer_new("slack-debug", "", "", "closed_slack_debug_buffer_cb", "") - w.buffer_set(slack_debug, "notify", "0") - + return SlackChannel(eventrouter, team=team, **channel_info) + + +def create_team(token, initial_data): + if not any(initial_data["remaining"].values()): + if initial_data["errors"]: + w.prnt( + "", + "ERROR: Failed connecting to Slack with token {}: {}".format( + token_for_print(token), ", ".join(initial_data["errors"]) + ), + ) + if not re.match(r"^xo\w\w(-\d+){3}-[0-9a-f]+(:.*)?$", token): + w.prnt( + "", + "ERROR: Token does not look like a valid Slack token. " + "Ensure it is a valid token and not just a OAuth code.", + ) -def config_changed_cb(data, option, value): - global slack_api_token, distracting_channels, colorize_nicks, colorize_private_chats, slack_debug, debug_mode, \ - unfurl_ignore_alt_text, colorize_messages, show_reaction_nicks + return - slack_api_token = w.config_get_plugin("slack_api_token") + def handle_rtmconnect(response_json, eventrouter, team, channel, metadata): + if not response_json["ok"]: + print(response_json["error"]) + return - if slack_api_token.startswith('${sec.data'): - slack_api_token = w.string_eval_expression(slack_api_token, {}, {}, {}) + team_id = response_json["team"]["id"] + myidentifier = response_json["self"]["id"] - distracting_channels = [x.strip() for x in w.config_get_plugin("distracting_channels").split(',')] - colorize_nicks = w.config_get_plugin('colorize_nicks') == "1" - colorize_messages = w.config_get_plugin("colorize_messages") == "1" - debug_mode = w.config_get_plugin("debug_mode").lower() - if debug_mode != '' and debug_mode != 'false': - create_slack_debug_buffer() - colorize_private_chats = w.config_string_to_boolean(w.config_get_plugin("colorize_private_chats")) - show_reaction_nicks = w.config_string_to_boolean(w.config_get_plugin("show_reaction_nicks")) + users = {} + bots = {} + for member in initial_data["members"]: + if member.get("is_bot"): + bots[member["id"]] = SlackBot(team_id, **member) + else: + users[member["id"]] = SlackUser(team_id, **member) - unfurl_ignore_alt_text = False - if w.config_get_plugin('unfurl_ignore_alt_text') != "0": - unfurl_ignore_alt_text = True + self_nick = nick_from_profile( + users[myidentifier].profile, response_json["self"]["name"] + ) - return w.WEECHAT_RC_OK + channels = {} + for channel_info in initial_data["channels"]: + channels[channel_info["id"]] = create_channel_from_info( + eventrouter, channel_info, None, myidentifier, users + ) -def quit_notification_cb(signal, sig_type, data): - stop_talking_to_slack() - return w.WEECHAT_RC_OK + subteams = {} + for usergroup in initial_data["usergroups"]: + is_member = myidentifier in usergroup["users"] + subteams[usergroup["id"]] = SlackSubteam( + team_id, is_member=is_member, **usergroup + ) -def script_unloaded(): - stop_talking_to_slack() - return w.WEECHAT_RC_OK + manual_presence = ( + "away" if initial_data["presence"]["manual_away"] else "active" + ) -def stop_talking_to_slack(): - """ - Prevents a race condition where quitting closes buffers - which triggers leaving the channel because of how close - buffer is handled - """ - global STOP_TALKING_TO_SLACK - STOP_TALKING_TO_SLACK = True - cache_write_cb("", "") - return w.WEECHAT_RC_OK + try: + all_notifications_prefs = json.loads( + initial_data["prefs"].get("all_notifications_prefs") + ) + global_keywords = all_notifications_prefs.get("global", {}).get( + "global_keywords" + ) + except json.decoder.JSONDecodeError: + global_keywords = None + + if global_keywords is None: + print_error( + "global_keywords not found in users.prefs.get", warning=True + ) + dbg( + "global_keywords not found in users.prefs.get. Response of users.prefs.get: {}".format( + json.dumps(initial_data["prefs"]) + ), + level=5, + ) + global_keywords = "" + + team_info = { + "id": team_id, + "name": response_json["team"]["id"], + "domain": response_json["team"]["domain"], + } + + team_hash = SlackTeam.generate_team_hash( + team_id, response_json["team"]["domain"] + ) + if not eventrouter.teams.get(team_hash): + team = SlackTeam( + eventrouter, + token, + team_hash, + response_json["url"], + team_info, + subteams, + self_nick, + myidentifier, + manual_presence, + users, + bots, + channels, + muted_channels=initial_data["prefs"]["muted_channels"], + highlight_words=global_keywords, + ) + eventrouter.register_team(team) + team.connect() + else: + team = eventrouter.teams.get(team_hash) + if team.myidentifier != myidentifier: + print_error( + "The Slack team {} has tokens for two different users, this is not supported. The " + "token {} is for user {}, and the token {} is for user {}. Please remove one of " + "them.".format( + team.team_info["name"], + token_for_print(team.token), + team.nick, + token_for_print(token), + self_nick, + ) + ) + else: + print_error( + "Ignoring duplicate Slack tokens for the same team ({}) and user ({}). The two " + "tokens are {} and {}.".format( + team.team_info["name"], + team.nick, + token_for_print(team.token), + token_for_print(token), + ), + warning=True, + ) -def scrolled_cb(signal, sig_type, data): - try: - if w.window_get_integer(data, "scrolling") == 1: - channels.find(w.current_buffer()).set_scrolling() - else: - channels.find(w.current_buffer()).unset_scrolling() - except: - pass - return w.WEECHAT_RC_OK + s = get_rtm_connect_request(token, callback=handle_rtmconnect) + EVENTROUTER.receive(s) -# END Utility Methods -# Main if __name__ == "__main__": - - if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, "script_unloaded", ""): - - version = w.info_get("version_number", "") or 0 - if int(version) < 0x1030000: - w.prnt("", "\nERROR: Weechat version 1.3+ is required to use {}.\n\n".format(SCRIPT_NAME)) + w = WeechatWrapper(weechat) + + if w.register( + SCRIPT_NAME, + SCRIPT_AUTHOR, + SCRIPT_VERSION, + SCRIPT_LICENSE, + SCRIPT_DESC, + "script_unloaded", + "", + ): + weechat_version = int(w.info_get("version_number", "") or 0) + weechat_upgrading = w.info_get("weechat_upgrading", "") + + completion_get_string = ( + w.hook_completion_get_string + if weechat_version < 0x2090000 + else w.completion_get_string + ) + + completion_list_add = ( + w.hook_completion_list_add + if weechat_version < 0x2090000 + else w.completion_list_add + ) + + if weechat_version < 0x2020000: + w.prnt( + "", + "\nERROR: WeeChat version 2.2+ is required to use {}.\n\n".format( + SCRIPT_NAME + ), + ) + elif weechat_upgrading == "1": + w.prnt( + "", + "NOTE: wee-slack will not work after running /upgrade until it's" + " reloaded. Please run `/python reload slack` to continue using it. You" + " will not receive any new messages in wee-slack buffers until doing this.", + ) else: + EVENTROUTER = EventRouter() - WEECHAT_HOME = w.info_get("weechat_dir", "") - CACHE_NAME = "slack.cache" - STOP_TALKING_TO_SLACK = False - - if not w.config_get_plugin('slack_api_token'): - w.config_set_plugin('slack_api_token', "INSERT VALID KEY HERE!") - if not w.config_get_plugin('distracting_channels'): - w.config_set_plugin('distracting_channels', "") - if not w.config_get_plugin('debug_mode'): - w.config_set_plugin('debug_mode', "") - if not w.config_get_plugin('colorize_nicks'): - w.config_set_plugin('colorize_nicks', "1") - if not w.config_get_plugin('colorize_messages'): - w.config_set_plugin('colorize_messages', "0") - if not w.config_get_plugin('colorize_private_chats'): - w.config_set_plugin('colorize_private_chats', "0") - if not w.config_get_plugin('trigger_value'): - w.config_set_plugin('trigger_value', "0") - if not w.config_get_plugin('unfurl_ignore_alt_text'): - w.config_set_plugin('unfurl_ignore_alt_text', "0") - if not w.config_get_plugin('switch_buffer_on_join'): - w.config_set_plugin('switch_buffer_on_join', "1") - if not w.config_get_plugin('show_reaction_nicks'): - w.config_set_plugin('show_reaction_nicks', "0") - - if w.config_get_plugin('channels_not_on_current_server_color'): - w.config_option_unset('channels_not_on_current_server_color') + receive_httprequest_callback = EVENTROUTER.receive_httprequest_callback + receive_ws_callback = EVENTROUTER.receive_ws_callback # Global var section slack_debug = None - config_changed_cb("", "", "") - - cmds = {k[8:]: v for k, v in globals().items() if k.startswith("command_")} - proc = {k[8:]: v for k, v in globals().items() if k.startswith("process_")} + config = PluginConfig() + config_changed_cb = config.config_changed typing_timer = time.time() - domain = None - previous_buffer = None - slack_buffer = None - - buffer_list_update = False - previous_buffer_list_update = 0 - never_away = False hide_distractions = False - hotlist = w.infolist_get("hotlist", "", "") - main_weechat_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, "DOESNOTEXIST!@#$")) - - message_cache = collections.defaultdict(list) - cache_load() - - servers = SearchList() - for token in slack_api_token.split(','): - server = SlackServer(token) - servers.append(server) - channels = SearchList() - users = SearchList() - - w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", "config_changed_cb", "") - w.hook_timer(3000, 0, 0, "slack_connection_persistence_cb", "") - - # attach to the weechat hooks we need - w.hook_timer(1000, 0, 0, "typing_update_cb", "") - w.hook_timer(1000, 0, 0, "buffer_list_update_cb", "") - w.hook_timer(1000, 0, 0, "hotlist_cache_update_cb", "") - w.hook_timer(1000 * 60 * 29, 0, 0, "slack_never_away_cb", "") - w.hook_timer(1000 * 60 * 5, 0, 0, "cache_write_cb", "") - w.hook_signal('buffer_closing', "buffer_closing_cb", "") - w.hook_signal('buffer_switch', "buffer_switch_cb", "") - w.hook_signal('window_switch', "buffer_switch_cb", "") - w.hook_signal('input_text_changed', "typing_notification_cb", "") - w.hook_signal('quit', "quit_notification_cb", "") - w.hook_signal('window_scrolled', "scrolled_cb", "") - w.hook_command( - # Command name and description - 'slack', 'Plugin to allow typing notification and sync of read markers for slack.com', - # Usage - '[command] [command options]', - # Description of arguments - 'Commands:\n' + - '\n'.join(cmds.keys()) + - '\nUse /slack help [command] to find out more\n', - # Completions - '|'.join(cmds.keys()), - # Function name - 'slack_command_cb', '') - # w.hook_command('me', 'me_command_cb', '') - w.hook_command('me', '', '', '', '', 'me_command_cb', '') - w.hook_command_run('/query', 'join_command_cb', '') - w.hook_command_run('/join', 'join_command_cb', '') - w.hook_command_run('/part', 'part_command_cb', '') - w.hook_command_run('/leave', 'part_command_cb', '') - w.hook_command_run('/topic', 'topic_command_cb', '') - w.hook_command_run('/msg', 'msg_command_cb', '') - w.hook_command_run("/input complete_next", "complete_next_cb", "") - w.hook_completion("nicks", "complete @-nicks for slack", - "nick_completion_cb", "") - w.bar_item_new('slack_typing_notice', 'typing_bar_item_cb', '') - # END attach to the weechat hooks we need + + w.hook_config(CONFIG_PREFIX + ".*", "config_changed_cb", "") + w.hook_config("irc.look.server_buffer", "config_server_buffer_cb", "") + if weechat_version < 0x2090000: + w.hook_modifier("input_text_for_buffer", "input_text_for_buffer_cb", "") + + EMOJI, EMOJI_WITH_SKIN_TONES_REVERSE = load_emoji() + setup_hooks() + + if config.record_events: + EVENTROUTER.record() + + hdata = Hdata(w) + + auto_connect = weechat.info_get("auto_connect", "") != "0" + + if auto_connect: + tokens = [ + token.strip() + for token in config.slack_api_token.split(",") + if token + ] + w.prnt( + "", + "Connecting to {} slack team{}.".format( + len(tokens), "" if len(tokens) == 1 else "s" + ), + ) + for t in tokens: + if t.startswith("xoxc-") and ":" not in t: + w.prnt( + "", + "{}When using an xoxc token, you need to also provide the d cookie in the format token:cookie".format( + w.prefix("error") + ), + ) + else: + initiate_connection(t) + EVENTROUTER.handle_next() diff --git a/python/snotify.py b/python/snotify.py index c74cb3de..749375eb 100644 --- a/python/snotify.py +++ b/python/snotify.py @@ -28,24 +28,26 @@ # along with this program. If not, see . # # History: -# 2010-10-17: version 0.1: initial release -# +# 2010-10-17: version 0.1: initial release # 2010-10-17: version 0.1.1: # * change: If a buffer + sound is added and psound is empty, then -# psound is set to given sound +# psound is set to given sound # * fix: bugs in "on"- and "off"-functions # * fix: "on"- and "off"-statusses were not used # 2010-11-15: Version 0.1.3 -# * change: No sound is played anymore on messages from yourself. -# * fix: a new sound for an entry already in the list wasn't set. -# * change: Replaced "weechat.hook_process" with Pythons' "subprocess.Popen" +# * change: No sound is played anymore on messages from yourself. +# * fix: a new sound for an entry already in the list wasn't set. +# * change: Replaced "weechat.hook_process" with Pythons' "subprocess.Popen" +# 2022-01-25: Version 0.1.4 +# * fix: replace tabs by spaces for indentation, +# make script compatible with Python 3 import subprocess as s from os.path import expanduser SCR_NAME = "snotify" SCR_AUTHOR = "Stephan Huebner " -SCR_VERSION = "0.1.3" +SCR_VERSION = "0.1.4" SCR_LICENSE = "GPL3" SCR_DESC = "Play a soundfile for messages in choosable channels or queries" SCR_COMMAND = "snotify" @@ -56,179 +58,179 @@ muted = False try: - import weechat as w + import weechat as w except: - print "Script must be run under weechat. http://www.weechat.org" - import_ok = False + print("Script must be run under weechat. http://www.weechat.org") + import_ok = False settings = { - "player" : "mplayer", # Application used to play a soundfile - "psound" : "", # Sound that should be played on private messages - "hsound" : "", # Sound that should be played on highlights - "buffers" : "" + "player" : "mplayer", # Application used to play a soundfile + "psound" : "", # Sound that should be played on private messages + "hsound" : "", # Sound that should be played on highlights + "buffers" : "" } def errMsg(myMsg): - alert("ERR: " + myMsg) - return + alert("ERR: " + myMsg) + return def fn_hook_process(data, command, rc, stdout, stderr): - alert(stderr) - return w.WEECHAT_RC_OK - + alert(stderr) + return w.WEECHAT_RC_OK + def fn_privmsg(data, bufferp, time, tags, display, is_hilight, prefix, msg): - global bfList - global settings - servername = (w.buffer_get_string(bufferp, "name").split("."))[0] - ownNick = w.info_get("irc_nick", servername) - mySound = "" - if not muted and prefix != ownNick: - if settings["player"] == "": - errMsg("'Player' isn't set!") - else: - for lEntry in bfList: - if lEntry["buffer"] == w.buffer_get_string(bufferp, "name"): - # we found a buffer of that name - if lEntry["status"] == "on": - if lEntry["sound"] == "": - if settings["psound"] == "": - errMsg("No sound defined! Please set either the " + - "regular 'psound'-option or the 'sound'-" + - "option for this buffer.") - else: - mySound = settings["psound"] - else: - mySound = lEntry["sound"] - s.Popen([settings["player"], expanduser(mySound)], - stderr=s.STDOUT, stdout=s.PIPE) - break - return w.WEECHAT_RC_OK + global bfList + global settings + servername = (w.buffer_get_string(bufferp, "name").split("."))[0] + ownNick = w.info_get("irc_nick", servername) + mySound = "" + if not muted and prefix != ownNick: + if settings["player"] == "": + errMsg("'Player' isn't set!") + else: + for lEntry in bfList: + if lEntry["buffer"] == w.buffer_get_string(bufferp, "name"): + # we found a buffer of that name + if lEntry["status"] == "on": + if lEntry["sound"] == "": + if settings["psound"] == "": + errMsg("No sound defined! Please set either the " + + "regular 'psound'-option or the 'sound'-" + + "option for this buffer.") + else: + mySound = settings["psound"] + else: + mySound = lEntry["sound"] + s.Popen([settings["player"], expanduser(mySound)], + stderr=s.STDOUT, stdout=s.PIPE) + break + return w.WEECHAT_RC_OK def fn_command(data, buffer, args): - global bfList - global muted - args = args.split() - myStatus = "on" - mySound = "" - myBuffer = "" - if len(args)>0: - try: # set a valid buffer and soundfile - myArg = args[1] - myBuffer = w.buffer_search("irc", myArg) - if myBuffer == "": - # no buffer with that name, so so assume it's the soundfile - mySound = myArg - myBuffer = w.current_buffer() - else: # we have a buffer, now look for a soundfile - try: - mySound = args[2] - except: - mySound = "" - except: # no further arguments, so set valid buffer + sound - myBuffer = w.current_buffer() - mySound = settings["psound"] - myBuffer = w.buffer_get_string(myBuffer, "name") - if args[0] == "test": - if settings["psound"] == "": - errMsg("No sound defined! Please set 'psound'-option!") - else: - s.Popen([settings["player"], expanduser(mySound)], - stderr=s.STDOUT, stdout=s.PIPE) - elif args[0] == "list": - for lEntry in bfList: - alert("BUFFER: " + lEntry["buffer"] + " | " + - "STATUS: " + lEntry["status"] + " | " + - "SOUND: " + lEntry["sound"]) - if len(bfList) == 0: - errMsg("No buffers configured!") - elif args[0] == "muteall": - muted = True - alert("All buffers are muted") - elif args[0] == "demuteall": - muted = False - alert("Sounds will be played") - elif args[0] == "add": - for listIndex in range(len(bfList)): - if bfList[listIndex]["buffer"] == myBuffer: - bfList.pop(listIndex) - break - bfList += [{"buffer":myBuffer,"status":myStatus,"sound":mySound}] - if settings["psound"] == "": - settings["psound"] = mySound - w.config_set_plugin("psound", mySound) - w.config_set_plugin("buffers", fn_createBufferString()) - elif args[0] == "remove": - for listIndex in range(len(bfList)): - if bfList[listIndex]["buffer"] == myBuffer: - bfList.pop(listIndex) - break - w.config_set_plugin("buffers", fn_createBufferString()) - elif args[0] == "on": - for listIndex in range(len(bfList)): - if bfList[listIndex]["buffer"] == myBuffer: - bfList[listIndex]["status"] = "on" - w.config_set_plugin("buffers", fn_createBufferString()) - break - elif args[0] == "off": - for lIndex in range(len(bfList)): - if bfList[lIndex]["buffer"] == myBuffer: - bfList[lIndex]["status"] = "off" - w.config_set_plugin("buffers", fn_createBufferString()) - break - return w.WEECHAT_RC_OK + global bfList + global muted + args = args.split() + myStatus = "on" + mySound = "" + myBuffer = "" + if len(args)>0: + try: # set a valid buffer and soundfile + myArg = args[1] + myBuffer = w.buffer_search("irc", myArg) + if myBuffer == "": + # no buffer with that name, so so assume it's the soundfile + mySound = myArg + myBuffer = w.current_buffer() + else: # we have a buffer, now look for a soundfile + try: + mySound = args[2] + except: + mySound = "" + except: # no further arguments, so set valid buffer + sound + myBuffer = w.current_buffer() + mySound = settings["psound"] + myBuffer = w.buffer_get_string(myBuffer, "name") + if args[0] == "test": + if settings["psound"] == "": + errMsg("No sound defined! Please set 'psound'-option!") + else: + s.Popen([settings["player"], expanduser(mySound)], + stderr=s.STDOUT, stdout=s.PIPE) + elif args[0] == "list": + for lEntry in bfList: + alert("BUFFER: " + lEntry["buffer"] + " | " + + "STATUS: " + lEntry["status"] + " | " + + "SOUND: " + lEntry["sound"]) + if len(bfList) == 0: + errMsg("No buffers configured!") + elif args[0] == "muteall": + muted = True + alert("All buffers are muted") + elif args[0] == "demuteall": + muted = False + alert("Sounds will be played") + elif args[0] == "add": + for listIndex in range(len(bfList)): + if bfList[listIndex]["buffer"] == myBuffer: + bfList.pop(listIndex) + break + bfList += [{"buffer":myBuffer,"status":myStatus,"sound":mySound}] + if settings["psound"] == "": + settings["psound"] = mySound + w.config_set_plugin("psound", mySound) + w.config_set_plugin("buffers", fn_createBufferString()) + elif args[0] == "remove": + for listIndex in range(len(bfList)): + if bfList[listIndex]["buffer"] == myBuffer: + bfList.pop(listIndex) + break + w.config_set_plugin("buffers", fn_createBufferString()) + elif args[0] == "on": + for listIndex in range(len(bfList)): + if bfList[listIndex]["buffer"] == myBuffer: + bfList[listIndex]["status"] = "on" + w.config_set_plugin("buffers", fn_createBufferString()) + break + elif args[0] == "off": + for lIndex in range(len(bfList)): + if bfList[lIndex]["buffer"] == myBuffer: + bfList[lIndex]["status"] = "off" + w.config_set_plugin("buffers", fn_createBufferString()) + break + return w.WEECHAT_RC_OK def fn_createBufferString(): - global bfList - myString = "" - for lEntry in bfList: - myString += "{'" + lEntry["buffer"] + "','" + lEntry["status"] - myString += "','" + lEntry["sound"] + "'}" - return myString + global bfList + myString = "" + for lEntry in bfList: + myString += "{'" + lEntry["buffer"] + "','" + lEntry["status"] + myString += "','" + lEntry["sound"] + "'}" + return myString def alert(myString): - w.prnt("", myString) - return + w.prnt("", myString) + return def fn_configchange(data, option, value): - global settings - fields = option.split(".") - myOption = fields[-1] - try: - settings[myOption] = value - #alert("Option {0} has been changed to {1}".format(myOption, - # settings[myOption])) - except KeyError: - errMsg("There is no option named %s" %myOption) - return w.WEECHAT_RC_OK + global settings + fields = option.split(".") + myOption = fields[-1] + try: + settings[myOption] = value + #alert("Option {0} has been changed to {1}".format(myOption, + # settings[myOption])) + except KeyError: + errMsg("There is no option named %s" %myOption) + return w.WEECHAT_RC_OK if __name__ == "__main__" and import_ok: - if w.register(SCR_NAME, SCR_AUTHOR, SCR_VERSION, SCR_LICENSE, - SCR_DESC, "", ""): - # synchronize weechat- and scriptsettings - for option, default_value in settings.items(): - if not w.config_is_set_plugin(option): - w.config_set_plugin(option, default_value) - else: - settings[option] = w.config_get_plugin(option) - if option == "buffers": # need to set buffers seperately - myBuffers = settings[option][2:-2] - try: - myBuffers = myBuffers.split("'}{'") - for tmp in myBuffers: - myBuffer, myStatus, mySound = tmp.split("','") - bfList += [{"buffer":myBuffer,"status":myStatus, - "sound":mySound}] - except: - myBuffers = "" - w.hook_print("", "", "", 1, "fn_privmsg", "") # catch prvmsg - w.hook_config("plugins.var.python." + SCR_NAME + ".*", - "fn_configchange", "") # catch configchanges - w.hook_command( # help-text - SCR_COMMAND, SCR_DESC, - "[test] | [add] [buffer] [sound[] | [remove] buffer | " + - "[on] [buffer] | [off] [buffer] | [muteall] | " + - "[demuteall] | [list]", - "Attention:" + + if w.register(SCR_NAME, SCR_AUTHOR, SCR_VERSION, SCR_LICENSE, + SCR_DESC, "", ""): + # synchronize weechat- and scriptsettings + for option, default_value in settings.items(): + if not w.config_is_set_plugin(option): + w.config_set_plugin(option, default_value) + else: + settings[option] = w.config_get_plugin(option) + if option == "buffers": # need to set buffers seperately + myBuffers = settings[option][2:-2] + try: + myBuffers = myBuffers.split("'}{'") + for tmp in myBuffers: + myBuffer, myStatus, mySound = tmp.split("','") + bfList += [{"buffer":myBuffer,"status":myStatus, + "sound":mySound}] + except: + myBuffers = "" + w.hook_print("", "", "", 1, "fn_privmsg", "") # catch prvmsg + w.hook_config("plugins.var.python." + SCR_NAME + ".*", + "fn_configchange", "") # catch configchanges + w.hook_command( # help-text + SCR_COMMAND, SCR_DESC, + "[test] | [add] [buffer] [sound[] | [remove] buffer | " + + "[on] [buffer] | [off] [buffer] | [muteall] | " + + "[demuteall] | [list]", + "Attention:" + """ * in places where you can enter a buffername, its best to use the 'Tab'-key to cycle through available buffers. If no buffer is chosen at all or the @@ -238,13 +240,13 @@ def fn_configchange(data, option, value): command will be executed just as if it were entered on the commandline. The suggestion for common players would be 'mplayer', 'vlc' or 'ogg123'; without the quotes of course). - + Available options are: - player: The application used to play a soundfile - psound: The soundfile that should be played - buffers: The saved buffers (it's best to not edit that setting directly. Rather choose one of the above mentioned commands). - + Commands: - test: Test soundplaying with current settings - add: add/edit a buffer. If no sound is chosen, the standard on is being @@ -255,10 +257,10 @@ def fn_configchange(data, option, value): - muteall: Turn off sounds for all buffers - demuteall: Turn on sounds for all buffers that were activated before - list: List all saved buffers""", - "|| add %(buffers_names)" - "|| remove %(buffers_names)" - "|| on %(buffers_names)" - "|| off %(buffers_names)" - "mute" - "muteall", "fn_command", "" - ) + "|| add %(buffers_names)" + "|| remove %(buffers_names)" + "|| on %(buffers_names)" + "|| off %(buffers_names)" + "mute" + "muteall", "fn_command", "" + ) diff --git a/python/soju.py b/python/soju.py new file mode 100644 index 00000000..73b44884 --- /dev/null +++ b/python/soju.py @@ -0,0 +1,143 @@ +# Copyright (c) 2021 Simon Ser +# +# License: GNU Affero General Public License version 3 +# https://www.gnu.org/licenses/agpl-3.0.en.html + +import weechat +import datetime + +weechat.register("soju", "soju", "0.5.1", "AGPL3", "soju bouncer integration", "", "") + +BOUNCER_CAP = "soju.im/bouncer-networks" + +weechat_version = int(weechat.info_get("version_number", "") or 0) + +if weechat_version < 0x04000000: + caps_option = weechat.config_get("irc.server_default.capabilities") + caps = weechat.config_string(caps_option) + if BOUNCER_CAP not in caps: + if caps != "": + caps += "," + caps += BOUNCER_CAP + weechat.config_option_set(caps_option, caps, 1) + +main_server = None +added_networks = {} + +def server_by_name(server_name): + hdata = weechat.hdata_get("irc_server") + server_list = weechat.hdata_get_list(hdata, "irc_servers") + if weechat_version >= 0x03040000: + return weechat.hdata_search( + hdata, + server_list, + "${irc_server.name} == ${name}", + {}, + {"name": server_name}, + {}, + 1, + ) + else: + return weechat.hdata_search( + hdata, + server_list, + "${irc_server.name} == " + server_name, + 1, + ) + +def handle_isupport_end_msg(data, signal, signal_data): + global main_server + + server_name = signal.split(",")[0] + netid = weechat.info_get("irc_server_isupport_value", server_name + ",BOUNCER_NETID") + + if netid != "": + added_networks[netid] = True + + server = server_by_name(server_name) + + hdata = weechat.hdata_get("irc_server") + cap_list = weechat.hdata_hashtable(hdata, server, "cap_list") + if not BOUNCER_CAP in cap_list: + return weechat.WEECHAT_RC_OK + + if main_server is not None: + return weechat.WEECHAT_RC_OK + main_server = server_name + + weechat.command(weechat.buffer_search("irc", "server." + server_name), "/quote BOUNCER LISTNETWORKS") + + return weechat.WEECHAT_RC_OK + +def handle_bouncer_msg(data, signal, signal_data): + server_name = signal.split(",")[0] + msg = weechat.info_get_hashtable("irc_message_parse", { "message": signal_data }) + + args = msg["arguments"].split(" ") + if args[0] != "NETWORK": + return weechat.WEECHAT_RC_OK + + # Don't connect twice to the same network + netid = args[1] + if netid in added_networks: + return weechat.WEECHAT_RC_OK_EAT + + # Retrieve the network name from the attributes + net_name = None + raw_attr_list = args[2].split(";") + for raw_attr in raw_attr_list: + k, v = raw_attr.split("=") + if k == "name": + net_name = v + break + + check_char = lambda ch: ch.isalnum() or ch in ".-_" + net_name = "".join(ch if check_char(ch) else "_" for ch in net_name) + + addr = weechat.config_string(weechat.config_get("irc.server." + server_name + ".addresses")) + + if weechat.config_get("irc.server." + net_name + ".addresses"): + weechat.command(weechat.buffer_search("core", "weechat"), "/server del " + net_name) + + add_server = [ + "/server", + "add", + net_name, + addr, + ] + + if weechat_version >= 0x04000000: + add_server.append("-tls") + else: + add_server.append("-ssl") + + # User name settings need to be adapted for new networks + for k in ["username", "sasl_username"]: + v = weechat.config_string(weechat.config_get("irc.server." + server_name + "." + k)) + if not v: + continue + username = v.split("/", maxsplit=1)[0] + "/" + net_name + add_server.append("-" + k + "=" + username) + + for k in ["password", "sasl_mechanism", "sasl_password"]: + v = weechat.config_string(weechat.config_get("irc.server." + server_name + "." + k)) + if not v: + continue + add_server.append("-" + k + "=" + v) + + weechat.command(weechat.buffer_search("core", "weechat"), " ".join(add_server)) + weechat.command(weechat.buffer_search("core", "weechat"), "/connect " + net_name) + + return weechat.WEECHAT_RC_OK_EAT + +def handle_cap_sync_req(data, modifier, modifier_data, requested): + supported = modifier_data.split(",")[1].split(" ") + if BOUNCER_CAP in supported: + requested += " " + BOUNCER_CAP + return requested + +weechat.hook_signal("*,irc_raw_in_376", "handle_isupport_end_msg", "") # RPL_ENDOFMOTD +weechat.hook_signal("*,irc_raw_in_422", "handle_isupport_end_msg", "") # ERR_NOMOTD +weechat.hook_signal("*,irc_raw_in_bouncer", "handle_bouncer_msg", "") +if weechat_version >= 0x04000000: + weechat.hook_modifier("irc_cap_sync_req", "handle_cap_sync_req", "") diff --git a/python/spell_correction.py b/python/spell_correction.py index 777ff3f9..cefbc923 100644 --- a/python/spell_correction.py +++ b/python/spell_correction.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2013 by nils_2 +# Copyright (c) 2013-2019 by nils_2 # # a simple spell correction for a "misspelled" word # @@ -17,6 +17,31 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +# 2024-07-20: nils_2, (libera.#weechat) +# 1.1 : fix SyntaxWarning with Python 3.12 (akyag) +# +# 2019-07-16: nils_2, (freenode.#weechat) +# 1.0 : fix bug when misspelled word don't have a suggestion (rafasc) +# : fixed typo in /help text +# +# 2019-03-01: nils_2, (freenode.#weechat) +# 0.9 : fix bug with auto popup 'spell_suggestion' item (StarlitGhost) +# +# 2019-02-24: nils_2, (freenode.#weechat) +# 0.8.1: fix ValueError when a misspelled word don't have a suggestion and you move cursor in input_line +# +# 2019-02-08: nils_2, (freenode.#weechat) +# 0.8 : add: make addword function shortkey compatible (ldlework) +# : -> https://github.com/weechat/weechat/pull/1288 (WeeChat >=2.4) +# : add: selected-suggestion color +# : -> https://github.com/weechat/scripts/issues/203 +# : support for "spell" plugin (WeeChat >=2.5) +# : item "spell_suggest" renamed to "spell_suggestion" +# : -> https://github.com/weechat/weechat/issues/1299 +# +# 2013-10-04: nils_2, (freenode.#weechat) +# 0.7 : add: addword function (idea by Nei) +# : localvar wasn't removed after /input return # # 2013-10-04: nils_2, (freenode.#weechat) # 0.6 : add: new option replace_mode @@ -56,9 +81,9 @@ SCRIPT_NAME = "spell_correction" SCRIPT_AUTHOR = "nils_2 " -SCRIPT_VERSION = "0.6" +SCRIPT_VERSION = "1.1" SCRIPT_LICENSE = "GPL" -SCRIPT_DESC = "a simple spell correction for a 'misspelled' word" +SCRIPT_DESC = "a spell correction script to use with spell/aspell plugin" OPTIONS = { 'auto_pop_up_item' : ('off','automatic pop-up suggestion item on a misspelled word'), 'auto_replace' : ('on','replaces misspelled word with selected suggestion, automatically. If you use "off" you will have to bind command "/%s replace" to a key' % SCRIPT_NAME), @@ -67,13 +92,15 @@ 'suggest_item' : ('${white}%S${default}', 'item format (%S = suggestion, %D = dict). Colors are allowed with format "${color}". note: since WeeChat 0.4.2 content is evaluated, see /help eval.'), 'hide_single_dict' : ('on','will hide dict in item if you have a single dict for buffer only'), 'complete_near' : ('0','show suggestions item only if you are n-chars near the misspelled word (0 = off). Using \'replace_mode\' cursor has to be n-chars near misspelled word to cycle through suggestions.'), - 'replace_mode' : ('off','misspelled word will be replaced directly by suggestions. Use option \'complete_near\' to specify range and item \'spell_suggest\' to show possible suggestions.'), + 'replace_mode' : ('off','misspelled word will be replaced directly by suggestions. Use option \'complete_near\' to specify range and item \'spell_suggestion\' to show possible suggestions.'), } Hooks = {'catch_input_completion': '', 'catch_input_return': ''} -regex_color=re.compile('\$\{([^\{\}]+)\}') -regex_optional_tags=re.compile('%\{[^\{\}]+\}') +regex_color=re.compile(r'\$\{([^\{\}]+)\}') +regex_optional_tags=re.compile(r'%\{[^\{\}]+\}') multiline_input = 0 +plugin_name = "spell" # WeeChat >= 2.5 +old_plugin_name = "aspell" # WeeChat < 2.5 # ================================[ weechat options & description ]=============================== def init_options(): for option,value in OPTIONS.items(): @@ -106,6 +133,8 @@ def toggle_refresh(pointer, name, value): # called from command and when TAB is pressed def auto_suggest_cmd_cb(data, buffer, args): + arguments = args.split(' ') + input_line = weechat.buffer_get_string(buffer, 'input') weechat.buffer_set(buffer, 'localvar_set_spell_correction_suggest_input_line', '%s' % input_line) @@ -120,11 +149,32 @@ def auto_suggest_cmd_cb(data, buffer, args): if not position: position = -1 + if arguments[0].lower() == 'addword' and len(arguments) >= 2: + found_dicts = get_aspell_dict_for(buffer) + if len(found_dicts.split(",")) == 1 and len(arguments) == 2: + word = arguments[1] + weechat.command("","/%s addword %s" % (plugin_name,word) ) + elif arguments[1] in found_dicts.split(",") and len(arguments) == 3: + word = arguments[2] + weechat.command("","/%s addword %s %s" % (plugin_name,arguments[1],word)) + # get localvar for misspelled_word and suggestions from buffer or return localvar_aspell_suggest = get_localvar_aspell_suggest(buffer) if not localvar_aspell_suggest: return weechat.WEECHAT_RC_OK + if arguments[0].lower() == 'addword' and len(arguments) == 1: + found_dicts = get_aspell_dict_for(buffer) + if not ":" in localvar_aspell_suggest and not "," in found_dicts: + weechat.command("","/%s addword %s" % (plugin_name,localvar_aspell_suggest)) + else: + misspelled_word,aspell_suggestions = localvar_aspell_suggest.split(':') + weechat.command("","/%s addword %s" % (plugin_name,misspelled_word)) + return weechat.WEECHAT_RC_OK + + if not ":" in localvar_aspell_suggest: + return weechat.WEECHAT_RC_OK + misspelled_word,aspell_suggestions = localvar_aspell_suggest.split(':') aspell_suggestions = aspell_suggestions.replace('/',',') @@ -132,6 +182,7 @@ def auto_suggest_cmd_cb(data, buffer, args): if len(aspell_suggestion_list) == 0: position = -1 weechat.bar_item_update('spell_correction') + weechat.bar_item_update('spell_suggestion') return weechat.WEECHAT_RC_OK # append an empty entry to suggestions to quit without changes. @@ -175,43 +226,18 @@ def show_spell_correction_item_cb (data, item, window): if not position or not aspell_suggest_item: return '' -# config_spell_suggest_item = weechat.config_get_plugin('suggest_item') -# if config_spell_suggest_item: -# show_item = config_spell_suggest_item.replace('%S',aspell_suggest_item) -# show_item = substitute_colors(show_item) -# return '%s' % (show_item) -# else: -# return aspell_suggest_item - - # get spell dict - localvar_aspell_suggest = get_localvar_aspell_suggest(buffer) - dicts_found = localvar_aspell_suggest.count("/") config_spell_suggest_item = weechat.config_get_plugin('suggest_item') - if dicts_found: - # aspell.dict.full_name = en_GB,de_DE-neu - # localvar_dict = en_GB,de_DE-neu - dictionary = get_aspell_dict_for(buffer) - if not dictionary: + + dict_found = search_dict(buffer,position) + + if dict_found: + if config_spell_suggest_item: + show_item = config_spell_suggest_item.replace('%S',aspell_suggest_item) + show_item = show_item.replace('%D',dict_found) + show_item = substitute_colors(show_item) + return '%s' % (show_item) + else: return aspell_suggest_item - dictionary_list = dictionary.split(',') - # more then one dict? - if len(dictionary_list) > 1: - undef,aspell_suggestions = localvar_aspell_suggest.split(':') - dictionary = aspell_suggestions.split('/') - words = 0 - i = -1 - for a in dictionary: - i += 1 - words += a.count(',')+1 - if words > int(position): - break - if config_spell_suggest_item: - show_item = config_spell_suggest_item.replace('%S',aspell_suggest_item) - show_item = show_item.replace('%D',dictionary_list[i]) - show_item = substitute_colors(show_item) - return '%s' % (show_item) - else: - return aspell_suggest_item else: if config_spell_suggest_item: show_item = config_spell_suggest_item.replace('%S',aspell_suggest_item) @@ -235,15 +261,16 @@ def input_text_changed_cb(data, signal, signal_data): if not buffer: return weechat.WEECHAT_RC_OK -# if OPTIONS['replace_mode'].lower() == "on" and weechat.buffer_get_string(buffer,'localvar_inline_replace_mode'): -# tab_complete,position,aspell_suggest_items = weechat.buffer_get_string(buffer,'localvar_inline_suggestions').split(':',2) -# weechat.buffer_set(buffer, 'localvar_set_inline_suggestions', '%s:%s:%s' % ('0',position,aspell_suggest_items)) -# return weechat.WEECHAT_RC_OK - - return weechat.WEECHAT_RC_OK - tab_complete,position,aspell_suggest_item = get_position_and_suggest_item(buffer) if not position or not aspell_suggest_item: + cursor_pos = weechat.buffer_get_integer(buffer,'input_pos') + if get_localvar_aspell_suggest(buffer) and cursor_pos >0: # save cursor position of misspelled word + weechat.buffer_set(buffer, 'localvar_set_current_cursor_pos', '%s' % cursor_pos) + else: + saved_cursor_pos = weechat.buffer_get_string(buffer, 'localvar_current_cursor_pos') + if saved_cursor_pos != '': + if int(cursor_pos) > int(saved_cursor_pos) + int(OPTIONS['complete_near']) + 3: # +3 to be sure! + delete_localvar_replace_mode(buffer) return weechat.WEECHAT_RC_OK # 1 = cursor etc., 2 = TAB, 3 = replace_mode @@ -251,7 +278,6 @@ def input_text_changed_cb(data, signal, signal_data): if not aspell_suggest_item: aspell_suggest_item = '' weechat.buffer_set(buffer, 'localvar_set_spell_correction_suggest_item', '%s:%s:%s' % ('0',position,aspell_suggest_item)) - weechat.bar_item_update('spell_correction') return weechat.WEECHAT_RC_OK if OPTIONS['auto_replace'].lower() == "on": @@ -260,6 +286,7 @@ def input_text_changed_cb(data, signal, signal_data): # weechat.buffer_set(buffer, 'localvar_set_spell_correction_suggest_item', '%s:%s:' % ('0','-1')) weechat.bar_item_update('spell_correction') + weechat.bar_item_update('spell_suggestion') return weechat.WEECHAT_RC_OK # also remove localvar_suggest_item @@ -269,6 +296,7 @@ def replace_misspelled_word(buffer): # remove spell_correction item weechat.buffer_set(buffer, 'localvar_del_spell_correction_suggest_item', '') weechat.bar_item_update('spell_correction') + weechat.bar_item_update('spell_suggestion') return if OPTIONS['eat_input_char'].lower() == 'off' or input_line == '': input_pos = weechat.buffer_get_integer(buffer,'input_pos') @@ -300,7 +328,6 @@ def replace_misspelled_word(buffer): input_line = input_line + ' ' weechat.buffer_set(buffer,'input',input_line) - weechat.bar_item_update('spell_correction') # set new cursor position. check if suggestion is longer or smaller than misspelled word input_pos = weechat.buffer_get_integer(buffer,'input_pos') + 1 @@ -313,22 +340,51 @@ def replace_misspelled_word(buffer): weechat.buffer_set(buffer,'input_pos',str(new_position)) weechat.buffer_set(buffer, 'localvar_del_spell_correction_suggest_item', '') + weechat.bar_item_update('spell_suggestion') + weechat.bar_item_update('spell_correction') + +# ================================[ subroutines ]=============================== +# get aspell dict for suggestion +def search_dict(buffer,position): + localvar_aspell_suggest = get_localvar_aspell_suggest(buffer) + dicts_found = localvar_aspell_suggest.count("/") + if not dicts_found: + return 0 + + # aspell.dict.full_name = en_GB,de_DE-neu + # localvar_dict = en_GB,de_DE-neu + dictionary = get_aspell_dict_for(buffer) + if not dictionary: + return 0 + dictionary_list = dictionary.split(',') + # more then one dict? + if len(dictionary_list) > 1: + undef,aspell_suggestions = localvar_aspell_suggest.split(':') + dictionary = aspell_suggestions.split('/') + words = 0 + i = -1 + for a in dictionary: + i += 1 + words += a.count(',')+1 + if words > int(position): + break + return dictionary_list[i] # format of localvar aspell_suggest (using two dicts): diehs:die hs,die-hs,dies/dies,Diebs,Viehs def get_localvar_aspell_suggest(buffer): - return weechat.buffer_get_string(buffer, 'localvar_aspell_suggest') + return weechat.buffer_get_string(buffer, 'localvar_%s_suggest' % plugin_name) def get_aspell_dict_for(buffer): # this should never happens, but to be sure. Otherwise WeeChat will crash if buffer == '': return '' if int(version) >= 0x00040100: - return weechat.info_get("aspell_dict", buffer) + return weechat.info_get("%s_dict" % plugin_name, buffer) # this is a "simple" work around and it only works for buffers with given dictionary # no fallback for partial name like "aspell.dict.irc". Get your hands on WeeChat 0.4.1 full_name = weechat.buffer_get_string(buffer,'full_name') - return weechat.config_string(weechat.config_get('aspell.dict.%s' % weechat.buffer_get_string(buffer,'full_name'))) + return weechat.config_string(weechat.config_get('%s.dict.%s' % (plugin_name,weechat.buffer_get_string(buffer,'full_name')))) def substitute_colors(text): if int(version) >= 0x00040200: @@ -351,16 +407,18 @@ def aspell_suggest_cb(data, signal, signal_data): # aspell says, suggested word is also misspelled. check out if we already have a suggestion list and don't use the new misspelled word! if weechat.buffer_get_string(buffer,'localvar_inline_suggestions'): return weechat.WEECHAT_RC_OK + if not ":" in localvar_aspell_suggest: + return weechat.WEECHAT_RC_OK misspelled_word,aspell_suggestions = localvar_aspell_suggest.split(':') aspell_suggestions = aspell_suggestions.replace('/',',') weechat.buffer_set(buffer, 'localvar_set_inline_suggestions', '%s:%s:%s' % ('2','0',aspell_suggestions)) - weechat.bar_item_update('spell_suggest') + weechat.bar_item_update('spell_suggestion') return weechat.WEECHAT_RC_OK if OPTIONS['auto_pop_up_item'].lower() == 'on': auto_suggest_cmd_cb('', buffer, '') weechat.buffer_set(buffer, 'localvar_del_spell_correction_suggest_input_line', '') - weechat.bar_item_update('spell_suggest') + weechat.bar_item_update('spell_suggestion') return weechat.WEECHAT_RC_OK def get_last_position_of_misspelled_word(misspelled_word, buffer): @@ -400,6 +458,8 @@ def input_complete_cb(data, buffer, command): if OPTIONS['replace_mode'].lower() == "on" and not weechat.buffer_get_string(buffer,'localvar_inline_replace_mode') and int(OPTIONS['complete_near']) >= 0: weechat.buffer_set(buffer, 'localvar_set_inline_replace_mode', '1') + if not ":" in localvar_aspell_suggest: + return weechat.WEECHAT_RC_OK misspelled_word,aspell_suggestions = localvar_aspell_suggest.split(':') begin_last_position, end_last_position, input_pos = get_last_position_of_misspelled_word(misspelled_word, buffer) @@ -433,11 +493,13 @@ def input_complete_cb(data, buffer, command): weechat.buffer_set(buffer,'input',input_line) input_pos = int(input_pos) + word_differ weechat.buffer_set(buffer,'input_pos',str(input_pos)) - weechat.bar_item_update('spell_suggest') + weechat.bar_item_update('spell_suggestion') return weechat.WEECHAT_RC_OK # after first [TAB] on a misspelled word in "replace mode" if OPTIONS['replace_mode'].lower() == "on" and weechat.buffer_get_string(buffer,'localvar_inline_replace_mode') == "1" and int(OPTIONS['complete_near']) >= 0: + if not ":" in weechat.buffer_get_string(buffer,'localvar_inline_suggestions'): + return weechat.WEECHAT_RC_OK tab_complete,position,aspell_suggest_items = weechat.buffer_get_string(buffer,'localvar_inline_suggestions').split(':',2) if not position or not aspell_suggest_items: @@ -486,20 +548,24 @@ def input_complete_cb(data, buffer, command): weechat.buffer_set(buffer,'input_pos',str(input_pos)) weechat.buffer_set(buffer, 'localvar_set_inline_suggestions', '%s:%s:%s' % ('2',str(position),aspell_suggest_items)) - weechat.bar_item_update('spell_suggest') + weechat.bar_item_update('spell_suggestion') return weechat.WEECHAT_RC_OK if int(OPTIONS['complete_near']) > 0: + if not ":" in localvar_aspell_suggest: + weechat.bar_item_update('spell_suggestion') + return weechat.WEECHAT_RC_OK misspelled_word,aspell_suggestions = localvar_aspell_suggest.split(':') begin_last_position, end_last_position, input_pos = get_last_position_of_misspelled_word(misspelled_word, buffer) if input_pos - end_last_position > int(OPTIONS['complete_near']): + weechat.bar_item_update('spell_suggestion') return weechat.WEECHAT_RC_OK tab_complete,position,aspell_suggest_item = get_position_and_suggest_item(buffer) weechat.buffer_set(buffer, 'localvar_set_spell_correction_suggest_item', '%s:%s:%s' % ('2',position,aspell_suggest_item)) auto_suggest_cmd_cb('', buffer, command) - weechat.bar_item_update('spell_suggest') + weechat.bar_item_update('spell_suggestion') return weechat.WEECHAT_RC_OK @@ -508,19 +574,19 @@ def delete_localvar_replace_mode(buffer): weechat.buffer_set(buffer, 'localvar_del_inline_replace_mode', '') weechat.buffer_set(buffer, 'localvar_del_inline_suggestions', '') weechat.buffer_set(buffer, 'localvar_del_save_position_of_word', '') - weechat.bar_item_update('spell_suggest') + weechat.buffer_set(buffer, 'localvar_del_current_cursor_pos', '') + weechat.bar_item_update('spell_suggestion') # if a suggestion is selected and you press [RETURN] replace misspelled word! def input_return_cb(data, signal, signal_data): buffer = signal - tab_complete,position,aspell_suggest_item = get_position_and_suggest_item(buffer) if not position or not aspell_suggest_item: + delete_localvar_replace_mode(buffer) return weechat.WEECHAT_RC_OK - if OPTIONS['auto_replace'].lower() == "on" and aspell_suggest_item: replace_misspelled_word(buffer) - + delete_localvar_replace_mode(buffer) return weechat.WEECHAT_RC_OK # /input delete_* @@ -530,6 +596,7 @@ def input_delete_cb(data, signal, signal_data): weechat.buffer_set(buffer, 'localvar_del_spell_correction_suggest_item', '') weechat.buffer_set(buffer, 'localvar_del_spell_correction_suggest_input_line', '') weechat.bar_item_update('spell_correction') + weechat.bar_item_update('spell_suggestion') return weechat.WEECHAT_RC_OK # /input move_* (cursor position) @@ -539,14 +606,12 @@ def input_move_cb(data, signal, signal_data): if OPTIONS['replace_mode'].lower() == "on" and weechat.buffer_get_string(buffer,'localvar_inline_replace_mode') == "1": delete_localvar_replace_mode(buffer) weechat.buffer_set(buffer, 'localvar_del_spell_correction_suggest_item', '') -# tab_complete,position,aspell_suggest_items = weechat.buffer_get_string(buffer,'localvar_inline_suggestions').split(':',2) -# weechat.buffer_set(buffer, 'localvar_set_inline_suggestions', '%s:%s:%s' % ('1',position,aspell_suggest_items)) return weechat.WEECHAT_RC_OK tab_complete,position,aspell_suggest_item = get_position_and_suggest_item(buffer) localvar_aspell_suggest = get_localvar_aspell_suggest(buffer) - if not localvar_aspell_suggest: + if not localvar_aspell_suggest or not ":" in localvar_aspell_suggest: return weechat.WEECHAT_RC_OK misspelled_word,aspell_suggestions = localvar_aspell_suggest.split(':') @@ -558,11 +623,8 @@ def input_move_cb(data, signal, signal_data): return weechat.WEECHAT_RC_OK weechat.buffer_set(buffer, 'localvar_set_spell_correction_suggest_item', '%s:%s:%s' % ('1',position,aspell_suggest_item)) - return weechat.WEECHAT_RC_OK -# aspell_suggest: "mispelled:mi spelled,mi-spelled,misspelled" -# weechat.bar_item_update('spell_suggest') def show_spell_suggestion_item_cb (data, item, window): buffer = weechat.window_get_pointer(window,"buffer") if buffer == '': @@ -572,27 +634,35 @@ def show_spell_suggestion_item_cb (data, item, window): if not weechat.buffer_get_string(buffer,'localvar_inline_suggestions'): return '' tab_complete,position,aspell_suggest_items = weechat.buffer_get_string(buffer,'localvar_inline_suggestions').split(':',2) - return aspell_suggest_items + localvar_aspell_suggest = "dummy:%s" % aspell_suggest_items +# return aspell_suggest_items - tab_complete,position,aspell_suggest_item = get_position_and_suggest_item(buffer) - localvar_aspell_suggest = get_localvar_aspell_suggest(buffer) + else: + tab_complete,position,aspell_suggest_item = get_position_and_suggest_item(buffer) + localvar_aspell_suggest = get_localvar_aspell_suggest(buffer) # localvar_aspell_suggest = word,word2/wort,wort2 if localvar_aspell_suggest: - misspelled_word,aspell_suggestions = localvar_aspell_suggest.split(':') + try: + misspelled_word,aspell_suggestions = localvar_aspell_suggest.split(':') + except ValueError: # maybe no suggestion for misspelled word. then go back + return '' aspell_suggestions_orig = aspell_suggestions aspell_suggestions = aspell_suggestions.replace('/',',') aspell_suggestion_list = aspell_suggestions.split(',') if not position: return '' + if position == "-1": + return aspell_suggestions_orig if int(position) < len(aspell_suggestion_list): reset_color = weechat.color('reset') - color = weechat.color("red") + color = weechat.color(weechat.config_color(weechat.config_get("%s.color.misspelled" % plugin_name))) new_word = aspell_suggestion_list[int(position)].replace(aspell_suggestion_list[int(position)],'%s%s%s' % (color, aspell_suggestion_list[int(position)], reset_color)) + aspell_suggestion_list[int(position)] = new_word # replace word with colored word + aspell_suggestions_orig = ','.join(map(str, aspell_suggestion_list)) else: return '' - return aspell_suggestions_orig def window_switch_cb(data, signal, signal_data): @@ -614,23 +684,30 @@ def weechat_nicklist_search_nick(buffer, nick): if int(version) < 0x00040000: weechat.prnt('','%s%s %s' % (weechat.prefix('error'),SCRIPT_NAME,': needs version 0.4.0 or higher')) weechat.command('','/wait 1ms /python unload %s' % SCRIPT_NAME) - - weechat.hook_command(SCRIPT_NAME, SCRIPT_DESC, 'previous|replace', - '\n' - 'Add item "spell_correction" to a bar (i suggest the input bar).\n' - '\n' - 'On an misspelled word, press TAB to cycle through suggestions. Any key on suggestion will replace misspelled word\n' - 'with current suggestion.\n' - '\n' - 'You have to set "aspell.check.suggestions" to a value >= 0 (default: -1 (off)).\n' - 'Using "aspell.check.real_time" the nick-completion will not work, until all misspelled words in input_line are replaced.\n' - '\n' - 'You can bind following commands to key:\n' - ' /' + SCRIPT_NAME + ' : to cycle though next suggestion\n' - ' /' + SCRIPT_NAME + ' previous : to cycle though previous suggestion\n' - ' /' + SCRIPT_NAME + ' replace : to replace misspelled word\n' + if int(version) < 0x02050000: + plugin_name = old_plugin_name + + SCRIPT_HELP = """addword : add a word to personal aspell dictionary (does not work with multiple dicts) +previous : to cycle though previous suggestion +replace : to replace misspelled word + +Quick start: + You should add script item "spell_correction" to a bar (i suggest using the input_bar). + On an misspelled word, press TAB to cycle through suggestions. Press any key on suggestion + to replace misspelled word with current displayed suggestion. + Also check script options: /fset %(s)s + +IMPORTANT: + "%(p)s.check.suggestions" option has to be set to a value >= 0 (default: -1 (off)). + "%(p)s.color.misspelled" option is used to highlight current suggestion in "%(p)s_suggestion" item + Using "%(p)s.check.real_time" the nick-completion will not work. All misspelled words + in input_line have to be replaced first. +""" %dict(p=plugin_name, s=SCRIPT_NAME) + + weechat.hook_command(SCRIPT_NAME, SCRIPT_DESC, 'addword ||previous||replace', + SCRIPT_HELP+ '', - 'previous|replace', + 'previous|replace|addword', 'auto_suggest_cmd_cb', '') init_options() @@ -641,7 +718,7 @@ def weechat_nicklist_search_nick(buffer, nick): # multiline workaround weechat.hook_signal('input_flow_free', 'multiline_cb', '') - weechat.hook_signal ('aspell_suggest', 'aspell_suggest_cb', '') + weechat.hook_signal ('%s_suggest' % plugin_name, 'aspell_suggest_cb', '') weechat.hook_signal ('buffer_switch', 'buffer_switch_cb','') weechat.hook_signal ('window_switch', 'window_switch_cb','') @@ -651,5 +728,4 @@ def weechat_nicklist_search_nick(buffer, nick): Hooks['catch_input_return'] = weechat.hook_command_run('/input return', 'input_return_cb', '') weechat.hook_config('plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '') weechat.bar_item_new('spell_correction', 'show_spell_correction_item_cb', '') - weechat.bar_item_new('spell_suggest', 'show_spell_suggestion_item_cb', '') -# weechat.prnt("","%s" % sys.version_info) + weechat.bar_item_new('spell_suggestion', 'show_spell_suggestion_item_cb', '') diff --git a/python/spotify.py b/python/spotify.py index b444aae9..ac2f1f72 100644 --- a/python/spotify.py +++ b/python/spotify.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -#Copyright (c) 2009 by xt +# Copyright (c) 2009 by xt # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,14 +16,16 @@ # along with this program. If not, see . # -# -# # If someone posts a spotify track URL in a configured channel # this script will post back which track it is using spotify.url.fi service -# -# # History: +# 2019-06-23, butlerx +# version 0.10: Add support for spotify playlists +# 2017-09-30, butlerx +# version 0.9: Add support for oauth keys being stored in secure data +# 2017-06-02, butlerx +# version 0.8: add now required oauth support # 2016-01-22, creadak # version 0.7: Updated for the new spotify API # 2011-03-11, Sebastien Helleu @@ -39,99 +41,149 @@ # version 0.2: use spotify.url.fi # 2009-06-19, xt # version 0.1: initial -# -import weechat as w -import re -import json -import urllib import datetime +import re -SCRIPT_NAME = "spotify" -SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "0.7" +import spotipy +import weechat as w +from spotipy.oauth2 import SpotifyClientCredentials + +SCRIPT_NAME = "spotify" +SCRIPT_AUTHOR = "xt " +SCRIPT_VERSION = "0.10" SCRIPT_LICENSE = "GPL" -SCRIPT_DESC = "Look up spotify urls" +SCRIPT_DESC = "Look up spotify urls" settings = { - "buffers" : 'freenode.#mychan,', # comma separated list of buffers - "emit_notice" : 'off', # on or off, use notice or msg + "buffers": "freenode.#mychan,", # comma separated list of buffers + "emit_notice": "off", # on or off, use notice or msg + "client_id": "client_id", + "client_secret": "client_secret", } settings_help = { - "buffers": 'A comma separated list of buffers the script should check', - "emit_notice": 'If on, this script will use /notice, if off, it will use /msg to post info' + "buffers": "A comma separated list of buffers the script should check", + "emit_notice": "If on, this script will use /notice, if off, it will use /msg to post info", + "client_id": "required client id token go to https://developer.spotify.com/my-applications/#!/applications to generate your own", + "client_secret": "required client secret token go to https://developer.spotify.com/my-applications/#!/applications to generate your own", } -gateway = "https://api.spotify.com" -endpoints = { - "track": 'v1/tracks', - "album": 'v1/albums', - "artist": 'v1/artists' -} +def parse_spotify_uri(uri): + """ + parse spotify uri + --- + spotify:track: + spotify:artist: + spotify:album: + spotify:user::playlist: + """ + for regex in ( + re.compile(r"spotify:(?P\w+):(?P\w{22})"), + re.compile(r"https?://open.spotify.com/(?P\w+)/(?P\w{22})"), + re.compile(r"spotify:user:(?P\w+):(?P\w+):(?P\w{22})"), + re.compile( + r"https?://open.spotify.com/user/(?P\w+)/(?P\w+)/(?P\w{22})" + ), + ): + results = regex.search(uri) + if results is not None: + yield results.groupdict() + + +def get_oauth(arg): + """get oauth token from weechat conf or secure data""" + token = w.config_get_plugin(arg) + return ( + w.string_eval_expression(token, {}, {}, {}) + if token.startswith("${sec.data") + else token + ) + + +def parse_track(data): + """parse track data in to message""" + name = data["name"] + album = data["album"]["name"] + artist = data["artists"][0]["name"] + duration = str(datetime.timedelta(milliseconds=data["duration_ms"])).split(".")[0] + popularity = data["popularity"] + return "%s - %s / %s %s %d%%" % (artist, name, album, duration, popularity) + + +def parse_artist(data): + """parse artist data in to message""" + return "%s - %s followers" % (data["name"], data["followers"]["total"]) + + +def parse_album(data): + """parse album data in to message""" + name = data["name"] + artist = data["artists"][0]["name"] + tracks = data["tracks"]["total"] + released = data["release_date"].split("-")[0] + length = 0 + for track in data["tracks"]["items"]: + length += track["duration_ms"] + duration = str(datetime.timedelta(milliseconds=length)).split(".")[0] + return "%s - %s (%s) - %d tracks (%s)" % (artist, name, released, tracks, duration) + + +def parse_playlist(data): + """parse playlist data in to message""" + return "%s by %s - %s tracks - %s followers" % ( + data["name"], + data["owner"]["display_name"], + data["tracks"]["total"], + data["followers"]["total"], + ) + + +def search_spotify(spotify, uri): + """search spotify based on a uri""" + for results in parse_spotify_uri(uri): + if results["type"] == "album": + yield parse_album(spotify.album(results["id"])) + elif results["type"] == "track": + yield parse_track(spotify.track(results["id"])) + elif results["type"] == "artist": + yield parse_artist(spotify.artist(results["id"])) + elif "user" in results and results["type"] == "playlist": + yield parse_playlist(spotify.user_playlist(results["user"], results["id"])) -spotify_track_res = (re.compile(r'spotify:(?P\w+):(?P\w{22})'), - re.compile(r'https?://open.spotify.com/(?P\w+)/(?P\w{22})')) - -def get_spotify_ids(s): - for r in spotify_track_res: - for type, track in r.findall(s): - yield type, track - -def parse_response(data, type): - if type == 'track': - name = data['name'] - album = data['album']['name'] - artist = data['artists'][0]['name'] - duration = str(datetime.timedelta(milliseconds=data['duration_ms'])).split('.')[0] - popularity = data['popularity'] - return "%s - %s / %s %s %d%%" % (artist, name, album, duration, popularity) - elif type == 'album': - name = data['name'] - artist = data['artists'][0]['name'] - tracks = data['tracks']['total'] - released = data['release_date'].split('-')[0] - length = 0 - for track in data['tracks']['items']: - length += track['duration_ms'] - duration = str(datetime.timedelta(milliseconds=length)).split('.')[0] - return "%s - %s (%s) - %d tracks (%s)" % (artist, name, released, tracks, duration) - elif type == 'artist': - name = data['name'] - followers = data['followers']['total'] - return "%s - %s followers" % (name, followers) def spotify_print_cb(data, buffer, time, tags, displayed, highlight, prefix, message): - notice = w.config_get_plugin('emit_notice') buffer_name = w.buffer_get_string(buffer, "name") - server, channel = buffer_name.split('.') - buffers_to_check = w.config_get_plugin('buffers').split(',') - - command = "msg" - if notice == "on": - command = "notice" + server, channel = buffer_name.split(".") + command = "notice" if w.config_get_plugin("emit_notice") == "on" else "msg" - if buffer_name.lower() not in [buffer.lower() for buffer in buffers_to_check]: + if buffer_name.lower() not in [ + buffer.lower() for buffer in w.config_get_plugin("buffers").split(",") + ]: return w.WEECHAT_RC_OK - for type, id in get_spotify_ids(message): - data = json.load(urllib.urlopen('%s/%s/%s' % (gateway, endpoints[type], id))) - reply = parse_response(data, type) - w.command('', "/%s -server %s %s %s" % (command, server, channel, reply)) - + spotify = spotipy.Spotify( + client_credentials_manager=SpotifyClientCredentials( + get_oauth("client_id"), get_oauth("client_secret") + ) + ) + for reply in search_spotify(spotify, message): + w.command("", "/%s -server %s %s %s" % (command, server, channel, reply)) return w.WEECHAT_RC_OK + if __name__ == "__main__": - if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, "", ""): + if w.register( + SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "" + ): # Set default settings - for option, default in settings.iteritems(): + for option, default in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default) # Set help text - for option, description in settings_help.iteritems(): + for option, description in settings_help.items(): w.config_set_desc_plugin(option, description) - + w.hook_print("", "", "spotify", 1, "spotify_print_cb", "") diff --git a/python/spotify_nowplaying.py b/python/spotify_nowplaying.py index b410fde4..a3fd96de 100644 --- a/python/spotify_nowplaying.py +++ b/python/spotify_nowplaying.py @@ -19,6 +19,10 @@ # (this script requires Spotify for Mac v0.5.1.98 or newer) # # History: +# +# 2022-01-25, Sébastien Helleu +# version 0.1.2: fix mixed spaces and tabs for indentation +# # 2011-06-12, agreeabledragon # version 0.1.1: rewrote it to use weechat.hook_process() to prevent it from blocking weechat as requested by Sébastien # @@ -30,7 +34,7 @@ SCRIPT_NAME = "spotify_nowplaying" SCRIPT_AUTHOR = "agreeabledragon " -SCRIPT_VERSION = "0.1.1" +SCRIPT_VERSION = "0.1.2" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Current song script for Spotify (v0.5.1.98 or newer) on OS X" SCRIPT_COMMAND = "spotify" @@ -48,60 +52,60 @@ "spotify_exec", "") else: - w.prnt("", "WARNING: This now playing script for Spotify only works on OS X with Spotify version 0.5.1.98 (or newer)") + w.prnt("", "WARNING: This now playing script for Spotify only works on OS X with Spotify version 0.5.1.98 (or newer)") def spotify_process(data, command, rc, stdout, stderr): - global SCRIPT_BUFFER, SCRIPT_PROCESS - if stderr: - w.prnt("", "There was an error executing the script - make sure you meet the requirements (OS X with Spotify v0.5.1.98 or newer)") - SCRIPT_BUFFER = False - SCRIPT_PROCESS = False - return w.WEECHAT_RC_ERROR - else: - w.command(SCRIPT_BUFFER, stdout) - SCRIPT_BUFFER = False - SCRIPT_PROCESS = False - return w.WEECHAT_RC_OK + global SCRIPT_BUFFER, SCRIPT_PROCESS + if stderr: + w.prnt("", "There was an error executing the script - make sure you meet the requirements (OS X with Spotify v0.5.1.98 or newer)") + SCRIPT_BUFFER = False + SCRIPT_PROCESS = False + return w.WEECHAT_RC_ERROR + else: + w.command(SCRIPT_BUFFER, stdout) + SCRIPT_BUFFER = False + SCRIPT_PROCESS = False + return w.WEECHAT_RC_OK def spotify_exec(data, buffer, args): - global SCRIPT_TIMEOUT, SCRIPT_BUFFER, SCRIPT_PROCESS - if SCRIPT_PROCESS: - w.prnt("", "Please wait for the other command to finish") - return w.WEECHAT_RC_ERROR - else: - script = """set AppleScript's text item delimiters to ASCII character 10 - set spotify_active to false - set theString to \\"/me is not currently running Spotify.\\" - - tell application \\"Finder\\" - if (get name of every process) contains \\"Spotify\\" then set spotify_active to true - end tell - - if spotify_active then - set got_track to false - - tell application \\"Spotify\\" - if player state is playing then - set theTrack to name of the current track - set theArtist to artist of the current track - set theAlbum to album of the current track - set isStarred to starred of the current track - set got_track to true - end if - end tell - - set theString to \\"/me is not playing anything in Spotify.\\" - - if got_track then - if isStarred then - set theString to \\"/me is listening to one of my favorite tracks \\\\\\"\\" & theTrack & \\"\\\\\\" by \\" & theArtist & \\" (Album: \\" & theAlbum & \\")\\" - else - set theString to \\"/me is listening to \\\\\\"\\" & theTrack & \\"\\\\\\" by \\" & theArtist & \\" (Album: \\" & theAlbum & \\")\\" - end if - end if - end if - - return theString""" - SCRIPT_BUFFER = buffer; - SCRIPT_PROCESS = w.hook_process('arch -i386 osascript -e "' + script + '"', SCRIPT_TIMEOUT, "spotify_process", "") - return w.WEECHAT_RC_OK + global SCRIPT_TIMEOUT, SCRIPT_BUFFER, SCRIPT_PROCESS + if SCRIPT_PROCESS: + w.prnt("", "Please wait for the other command to finish") + return w.WEECHAT_RC_ERROR + else: + script = """set AppleScript's text item delimiters to ASCII character 10 + set spotify_active to false + set theString to \\"/me is not currently running Spotify.\\" + + tell application \\"Finder\\" + if (get name of every process) contains \\"Spotify\\" then set spotify_active to true + end tell + + if spotify_active then + set got_track to false + + tell application \\"Spotify\\" + if player state is playing then + set theTrack to name of the current track + set theArtist to artist of the current track + set theAlbum to album of the current track + set isStarred to starred of the current track + set got_track to true + end if + end tell + + set theString to \\"/me is not playing anything in Spotify.\\" + + if got_track then + if isStarred then + set theString to \\"/me is listening to one of my favorite tracks \\\\\\"\\" & theTrack & \\"\\\\\\" by \\" & theArtist & \\" (Album: \\" & theAlbum & \\")\\" + else + set theString to \\"/me is listening to \\\\\\"\\" & theTrack & \\"\\\\\\" by \\" & theArtist & \\" (Album: \\" & theAlbum & \\")\\" + end if + end if + end if + + return theString""" + SCRIPT_BUFFER = buffer + SCRIPT_PROCESS = w.hook_process('arch -i386 osascript -e "' + script + '"', SCRIPT_TIMEOUT, "spotify_process", "") + return w.WEECHAT_RC_OK diff --git a/python/stick_buffer.py b/python/stick_buffer.py index 36e90865..93ef1687 100644 --- a/python/stick_buffer.py +++ b/python/stick_buffer.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2013-2015 by nils_2 +# Copyright (c) 2013-2017 by nils_2 # Copyright (c) 2015 by Damien Bargiacchi # # stick buffer to a window, irssi like @@ -20,6 +20,15 @@ # # idea by shad0VV@freenode.#weechat # +# 2017-12-14: Sébastien Helleu +# 0.6 : rename command "/autosetbuffer" by "/buffer_autoset" in example +# +# 2017-04-02: nils_2, (freenode.#weechat) +# 0.5 : support of "/input jump_smart" and "/buffer +/-" (reported: squigz) +# +# 2017-03-25: nils_2, (freenode.#weechat) +# 0.4 : script did not work with /go script and buffer names (reported: squigz) +# # 2015-05-12: Damien Bargiacchi # 0.3 : Stop script from truncating localvar lookup to first character of the buffer number # : Clean up destination buffer number logic @@ -46,7 +55,7 @@ SCRIPT_NAME = "stick_buffer" SCRIPT_AUTHOR = "nils_2 " -SCRIPT_VERSION = "0.3" +SCRIPT_VERSION = "0.6" SCRIPT_LICENSE = "GPL" SCRIPT_DESC = "Stick buffers to particular windows, like irssi" @@ -82,7 +91,7 @@ def get_default_stick_window_number(): return None # ======================================[ buffer utils ]====================================== # -def infolist_get_buffer_name_and_ptr(str_buffer_number): +def infolist_get_buffer_name_and_ptr_by_number(str_buffer_number): infolist = weechat.infolist_get('buffer', '', '') full_name = '' ptr_buffer = '' @@ -92,9 +101,31 @@ def infolist_get_buffer_name_and_ptr(str_buffer_number): full_name = weechat.infolist_string(infolist, 'full_name') ptr_buffer = weechat.infolist_pointer(infolist, 'pointer') break - weechat.infolist_free(infolist) + weechat.infolist_free(infolist) return full_name, ptr_buffer +def infolist_get_buffer_name_and_ptr_by_name(str_buffer_name): + infolist = weechat.infolist_get('buffer', '', '*%s*' % str_buffer_name) + full_name = '' + ptr_buffer = '' + if infolist: + while weechat.infolist_next(infolist): + full_name = weechat.infolist_string(infolist, 'full_name') + ptr_buffer = weechat.infolist_pointer(infolist, 'pointer') + break + weechat.infolist_free(infolist) + return full_name, ptr_buffer + +def infolist_get_first_entry_from_hotlist(): + infolist = weechat.infolist_get('hotlist', '', '') + if infolist: + weechat.infolist_next(infolist) # go to first entry in hotlist + buffer_name = weechat.infolist_string(infolist, 'buffer_name') + buffer_number = weechat.infolist_integer(infolist, 'buffer_number') + ptr_buffer = weechat.infolist_pointer(infolist, 'buffer_pointer') + weechat.infolist_free(infolist) + return buffer_name, ptr_buffer, buffer_number + def get_current_buffer_number(): ptr_buffer = weechat.window_get_pointer(weechat.current_window(), 'buffer') return weechat.buffer_get_integer(ptr_buffer, 'number') @@ -122,34 +153,48 @@ def get_destination_buffer_number(arg): # ======================================[ callbacks ]====================================== # def buffer_switch_cb(data, buffer, command): +# weechat.prnt("","data: %s buffer: %s command: %s" % (data,buffer,command)) + # command exist? if command == '': return weechat.WEECHAT_RC_OK + # get command without leading command char! + cmd = command[1:].strip().split(' ',)[0:1] + # get number from command /buffer args = command.strip().split(' ',)[1:] - if len(args) != 1: - return weechat.WEECHAT_RC_OK + ptr_buffer = '' - destination_buffer = get_destination_buffer_number(args[0]) + if "input" in cmd and "jump_smart" in args: + buffer_name, ptr_buffer, buffer_number = infolist_get_first_entry_from_hotlist() - if not destination_buffer: - return weechat.WEECHAT_RC_OK - if destination_buffer < 1: - destination_buffer = 1 + if "buffer" in cmd: + if len(args) != 1: + return weechat.WEECHAT_RC_OK + + # check if argument is a buffer "number" + destination_buffer = get_destination_buffer_number(args[0]) + if destination_buffer: + if destination_buffer < 1: + destination_buffer = 1 + buffer_name, ptr_buffer = infolist_get_buffer_name_and_ptr_by_number(destination_buffer) + else: + # search for buffer name + buffer_name, ptr_buffer = infolist_get_buffer_name_and_ptr_by_name(args[0]) - buffer_name, ptr_buffer = infolist_get_buffer_name_and_ptr(destination_buffer) if not ptr_buffer: return weechat.WEECHAT_RC_OK if ptr_buffer == weechat.window_get_pointer(weechat.current_window(), 'buffer'): return weechat.WEECHAT_RC_OK - window_number = weechat.buffer_get_string(ptr_buffer, 'localvar_stick_buffer_to_window') if not window_number: window_number = get_default_stick_window_number() if window_number: weechat.command('', '/window %s' % window_number) - return weechat.WEECHAT_RC_OK - + weechat.command('', '/buffer %s' % buffer_name) + return weechat.WEECHAT_RC_OK_EAT + else: + return weechat.WEECHAT_RC_OK def cmd_cb(data, buffer, args): args = args.strip().lower().split(' ') @@ -197,7 +242,7 @@ def main(): Stick buffer #weechat to window 2: /buffer #weechat /buffer set localvar_set_stick_buffer_to_window 2 - /autosetbuffer add irc.freenode.#weechat stick_buffer_to_window 2 + /buffer_autoset add irc.freenode.#weechat stick_buffer_to_window 2 Set the default stick-to window to window 5: /set plugins.var.python.{script_name}.default_stick_window 5 List buffers with persistent stickiness: @@ -211,6 +256,7 @@ def main(): weechat.hook_command(SCRIPT_NAME, SCRIPT_DESC, 'list', description, 'list %-', 'cmd_cb', '') weechat.hook_command_run('/buffer *', 'buffer_switch_cb', '') + weechat.hook_command_run('/input jump_smart', 'buffer_switch_cb', '') if __name__ == '__main__': if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, diff --git a/python/styurl.py b/python/styurl.py new file mode 100644 index 00000000..69716505 --- /dev/null +++ b/python/styurl.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2019 Cole Helbling +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# Changelog: +# 2019-12-14, Cole Helbling +# version 1.0: initial release + +SCRIPT_NAME = "styurl" +SCRIPT_AUTHOR = "Cole Helbling " +SCRIPT_VERSION = "1.0" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = "Style URLs with a Python regex" + +import_ok = True +try: + import weechat as w +except ImportError: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org") + import_ok = False + +try: + import re +except ImportError as message: + print("Missing package for %s: %s" % (SCRIPT_NAME, message)) + import_ok = False + +# https://mathiasbynens.be/demo/url-regex +# If you don't want to create your own regex, see the above link for options or +# ideas on creating your own + +styurl_settings = { + "buffer_type": ( + "formatted", + "the type of buffers to run on (options are \"formatted\", \"free\", " + "or \"*\" for both)" + ), + "format": ( + "${color:*_32}", + "the style that should be applied to the URL" + "(evaluated, see /help eval)" + ), + "ignored_buffers": ( + "core.weechat,python.grep", + "comma-separated list of buffers to ignore URLs in " + "(full name like \"irc.freenode.#alacritty\")" + ), + "ignored_tags": ( + "irc_quit,irc_join", + "comma-separated list of tags to ignore URLs from" + ), + "regex": ( + r"((?:https?|ftp)://[^\s/$.?#].\S*)", + "the URL-parsing regex using Python syntax " + "(make sure capturing group 1 is the full URL)" + ), +} + +line_hook = None + + +def styurl_line_cb(data, line): + """ + Callback called when a line is displayed. + This parses the message for any URLs and styles them according to + styurl_settings["format"]. + """ + global styurl_settings + + # Don't style the line if it's not going to be displayed... duh + if line["displayed"] != "1": + return line + + tags = line["tags"].split(',') + ignored_tags = styurl_settings["ignored_tags"] + + # Ignore specified message tags + if ignored_tags: + if any(tag in tags for tag in ignored_tags.split(',')): + return line + + bufname = line["buffer_name"] + ignored_buffers = styurl_settings["ignored_buffers"] + + # Ignore specified buffers + if ignored_buffers and bufname in ignored_buffers.split(','): + return line + + message = line["message"] + + # TODO: enforce presence of a properly-formatted color object at + # styurl_settings["format"] (eval object would also be valid, if it eval'd + # to a color) + + regex = re.compile(styurl_settings["regex"]) + url_style = w.string_eval_expression(styurl_settings["format"], {}, {}, {}) + reset = w.color("reset") + + # Search for URLs and surround them with the defined URL styling + formatted = regex.sub(r"%s\1%s" % (url_style, reset), message) + line["message"] = line["message"].replace(message, formatted) + + return line + + +def styurl_config_cb(data, option, value): + """Callback called when a script option is changed.""" + global styurl_settings, line_hook + + pos = option.rfind('.') + if pos > 0: + name = option[pos+1:] + if name in styurl_settings: + # Changing the buffer target requires us to re-hook to prevent + # obsolete buffer types from getting styled + if name == "buffer_type": + if value in ("free", "formatted", "*"): + w.unhook(line_hook) + line_hook = w.hook_line(value, "", "", "styurl_line_cb", + "") + else: + # Don't change buffer type if it is invalid + w.prnt("", SCRIPT_NAME + ": Invalid buffer type: '%s', " + "not changing." % value) + w.config_set_plugin(name, styurl_settings[name]) + return w.WEECHAT_RC_ERROR + + styurl_settings[name] = value + + return w.WEECHAT_RC_OK + + +def styurl_unload_cb(): + """Callback called when the script is unloaded.""" + global line_hook + + w.unhook(line_hook) + del line_hook + return w.WEECHAT_RC_OK + + +if __name__ == "__main__" and import_ok: + if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, + SCRIPT_DESC, "styurl_unload_cb", ""): + + version = w.info_get("version_number", "") or 0 + + for option, value in styurl_settings.items(): + if w.config_is_set_plugin(option): + styurl_settings[option] = w.config_get_plugin(option) + else: + w.config_set_plugin(option, value[0]) + styurl_settings[option] = value[0] + if int(version) >= 0x00030500: + w.config_set_desc_plugin(option, "%s (default: \"%s\")" + % (value[1], value[0])) + + w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", + "styurl_config_cb", "") + + # Style URLs + line_hook = w.hook_line(styurl_settings["buffer_type"], "", "", + "styurl_line_cb", "") diff --git a/python/teknik.py b/python/teknik.py new file mode 100644 index 00000000..3c332f62 --- /dev/null +++ b/python/teknik.py @@ -0,0 +1,132 @@ +# Teknik created by Uncled1023 +from __future__ import print_function + +import_success = True + +import sys +import os +import threading +import json +import Tkinter as tk +import tkFileDialog + +try: + import weechat +except ImportError: + print('This script must be run under WeeChat.') + print('Get WeeChat now at: http://www.weechat.org/') + import_success = False + +# Requires Install +try: + from teknik import uploads as teknik +except ImportError as e: + print('Missing package(s) for %s: %s' % ('Teknik', e)) + import_success = False + +# Weechat Registration +weechat.register("Teknik", "Uncled1023", "1.0.0", "BSD", "Interact with the Teknik Services, including file uploads, pastes, and url shortening.", "script_closed", "") + +def upload_file(data): + try: + args = json.loads(data) + if args['file'] is not None and os.path.exists(args['file']): + # Try to upload the file + jObj = teknik.UploadFile(args['apiUrl'], args['file'], args['apiUsername'], args['apiToken']) + return json.dumps(jObj) + except: + e = sys.exc_info()[0] + print("Exception: %s" %e, file=sys.stderr) + return '' + +def process_upload(data, command, return_code, out, err): + if return_code == weechat.WEECHAT_HOOK_PROCESS_ERROR: + weechat.prnt("", "Error with command '%s'" % command) + return weechat.WEECHAT_RC_OK + if return_code > 0: + weechat.prnt("", "return_code = %d" % return_code) + if out != "": + results = json.loads(out) + # Either print the result to the input box, or write the error message to the window + if 'error' in results: + weechat.prnt("", 'Error: ' + results['error']['message']) + elif 'result' in results: + buffer = weechat.current_buffer() + weechat.buffer_set(buffer, 'input', results['result']['url']) + else: + weechat.prnt("", 'Unknown Error') + if err != "": + weechat.prnt("", "stderr: %s" % err) + return weechat.WEECHAT_RC_OK + +def teknik_set_url(url): + weechat.config_set_plugin('plugins.var.python.teknik.api_url', url) + +def teknik_set_token(token): + weechat.config_set_plugin('plugins.var.python.teknik.token', token) + +def teknik_set_username(username): + weechat.config_set_plugin('plugins.var.python.teknik.username', username) + +def script_closed(): + # Clean Up Session + return weechat.WEECHAT_RC_OK + +def teknik_command(data, buffer, args): + args = args.strip() + if args == "": + weechat.prnt("", "Error: You must specify a command") + else: + argv = args.split(" ") + command = argv[0].lower() + + # Upload a File + if command == 'upload': + if len(argv) < 2: + weechat.prnt("", "Error: You must specify a file") + else: + # Get current config values + apiUrl = weechat.config_string(weechat.config_get('plugins.var.python.teknik.api_url')) + apiUsername = weechat.config_string(weechat.config_get('plugins.var.python.teknik.username')) + apiToken = weechat.config_string(weechat.config_get('plugins.var.python.teknik.token')) + + data = {'file': argv[1], 'apiUrl': apiUrl, 'apiUsername': apiUsername, 'apiToken': apiToken} + hook = weechat.hook_process('func:upload_file', 0, "process_upload", json.dumps(data)) + + # Set a config option + elif command == 'set': + if len(argv) < 2: + weechat.prnt("", "Error: You must specify the option to set") + else: + option = argv[1].lower() + if option == 'username': + if len(argv) < 3: + weechat.prnt("", "Error: You must specify a username") + else: + teknik_set_username(argv[2]) + elif option == 'token': + if len(argv) < 3: + weechat.prnt("", "Error: You must specify an auth token") + else: + teknik_set_token(argv[2]) + elif option == 'url': + if len(argv) < 3: + weechat.prnt("", "Error: You must specify an api url") + else: + teknik_set_url(argv[2]) + else: + weechat.prnt("", "Error: Unrecognized Option") + else: + weechat.prnt("", "Error: Unrecognized Command") + + return weechat.WEECHAT_RC_OK + +if __name__ == "__main__" and import_success: + hook = weechat.hook_command("teknik", "Allows uploading of a file to Teknik and sharing the url directly to the chat.", + "[upload ] | [set username|token|url ]", + ' file: The file you want to upload' + ' username: The username for your Teknik account' + ' auth_token: The authentication token for your Teknik Account' + ' api_url: The URL for the Upload API', + "", + "teknik_command", "") diff --git a/python/telnot.py b/python/telnot.py new file mode 100644 index 00000000..6174b4f1 --- /dev/null +++ b/python/telnot.py @@ -0,0 +1,90 @@ +# ~*~ coding: utf-8 ~*~ +# Author: Frantisek Kolacek +# Homepage: https://github.com/fkolacek/weechat-telnot + +import weechat + +try: + # Python 3 + from urllib.parse import urlencode +except ImportError: + from urllib import urlencode + +weechat.register('telnot', + 'Frantisek Kolacek ', + '1.0', + 'MIT', + 'telnot: Send notification over Telegram using TelNot', + '', + '') + +settings = { + 'endpoint': 'Address of server running TelNot instance (including http:// or https://)', + 'token': 'User token', + 'bot': 'Name of Telegram bot', +} + +required_settings = [ + 'endpoint', + 'token', + 'bot', +] + +for option, description in list(settings.items()): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, '') + + if option in required_settings and weechat.config_get_plugin(option) == '': + weechat.prnt('', weechat.prefix('error') + 'telnot: Please set option: {}'.format(option)) + weechat.prnt('', 'telnot: /set plugins.var.python.telnot.{} STRING'.format(option)) + + weechat.config_set_desc_plugin(option, description) + +# buffer, tags, message, strip_colors, callback, callback_data +weechat.hook_print('', 'notify_message', '', 1, 'process_notification', '') +weechat.hook_print('', 'notify_private', '', 1, 'process_notification', '') + + +def process_notification(data, buffer, date, tags, displayed, highlight, prefix, message): + + if 'notify_message' in tags and not highlight: + return weechat.WEECHAT_RC_OK + + nick = weechat.buffer_get_string(buffer, 'localvar_nick') + name = weechat.buffer_get_string(buffer, 'name') + server = weechat.buffer_get_string(buffer, 'localvar_server') + channel = weechat.buffer_get_string(buffer, 'localvar_channel') + + if weechat.buffer_get_string(buffer, 'localvar_type') == 'private' and prefix != nick: + send_notification(server, channel, prefix, message) + elif int(highlight): + buff = weechat.buffer_get_string(buffer, 'short_name') or name + + send_notification(server, buff, prefix, message) + + return weechat.WEECHAT_RC_OK + + +def send_notification(server, channel, nick, message): + endpoint = weechat.config_get_plugin('endpoint') + token = weechat.config_get_plugin('token') + bot = weechat.config_get_plugin('bot') + + if channel == nick: + output = '[{}@{}] {}'.format(nick, server, message) + else: + output = '[{}@{}] {}: {}'.format(nick, server, channel, message) + + data = urlencode({ + 'message': output, + 'token': token, + 'bot': bot, + }) + + options = { + 'postfields': data, + 'ssl_verifypeer': '0', + 'ssl_verifyhost': '0', + } + + weechat.hook_process_hashtable('url:' + endpoint, options, 2000, '', '') diff --git a/python/terminal_title.py b/python/terminal_title.py new file mode 100644 index 00000000..b85dc964 --- /dev/null +++ b/python/terminal_title.py @@ -0,0 +1,129 @@ +# +# Copyright (C) 2010 by Guido Berhoerster +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import os +import sys +import re +import string +from collections import Mapping +import weechat + +SCRIPT_NAME = 'terminal-title' +VERSION = '1' +AUTHOR = 'Guido Berhoerster' +DESCRIPTION = 'Displays user defined information in the terminal title' +DEFAULT_SETTINGS = { + 'title': ('WeeChat %version [%buffer_count] %buffer_number: ' + '%buffer_name{%buffer_nicklist_count} [%hotlist]', + 'items displayed in the terminal title') +} +TERM_TEMPLATES = [ + ('xterm', "\033]0;%s\007"), + ('screen', "\033_%s\033\\") +] +TERM_TEMPLATE = None + + +class TermTitleMapping(Mapping): + substitutions = { + 'buffer_title': + lambda : weechat.buffer_get_string(weechat.current_buffer(), + "title"), + 'buffer_name': + lambda : weechat.buffer_get_string(weechat.current_buffer(), + "name"), + 'buffer_plugin': + lambda : weechat.buffer_get_string(weechat.current_buffer(), + "plugin"), + 'buffer_number': + lambda : weechat.buffer_get_integer(weechat.current_buffer(), + "number"), + 'buffer_nicklist_count': + lambda : weechat.buffer_get_integer(weechat.current_buffer(), + "nicklist_visible_count"), + 'buffer_count': lambda : buffer_count(), + 'hotlist': lambda : hotlist(), + 'version': lambda : weechat.info_get("version", "") + } + + def __getitem__(self, key): + return self.substitutions[key]() + + def __iter__(self): + return self.substitutions.iterkeys() + + def __len__(self): + return len(self.substitutions) + + +class TermTitleTemplate(string.Template): + delimiter = '%' + + +def buffer_count(): + buffer_count = 0 + buffer = weechat.infolist_get("buffer", "", "") + while weechat.infolist_next(buffer): + buffer_count += 1 + weechat.infolist_free(buffer) + return buffer_count + +def hotlist(): + hotlist_items = [] + hotlist = weechat.infolist_get("hotlist", "", "") + while weechat.infolist_next(hotlist): + buffer_number = weechat.infolist_integer(hotlist, "buffer_number") + buffer = weechat.infolist_pointer(hotlist, "buffer_pointer") + short_name = weechat.buffer_get_string(buffer, "short_name") + hotlist_items.append("%s:%s" % (buffer_number, short_name)) + weechat.infolist_free(hotlist) + return ",".join(hotlist_items) + +def set_term_title_hook(data, signal, signal_data): + title_template_str = weechat.config_get_plugin('title') + title_template = TermTitleTemplate(title_template_str) + title_str = title_template.safe_substitute(TermTitleMapping()) + sys.__stdout__.write(TERM_TEMPLATE % title_str) + sys.__stdout__.flush() + + return weechat.WEECHAT_RC_OK + +def config_hook(data, option, value): + set_term_title_hook("", "", "") + + return weechat.WEECHAT_RC_OK + +if __name__ == '__main__': + weechat.register(SCRIPT_NAME, AUTHOR, VERSION, 'GPL3', DESCRIPTION, '', '') + + for option, (value, description) in DEFAULT_SETTINGS.iteritems(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value) + weechat.config_set_desc_plugin(option, '%s (default: "%s")' % + (description, value)) + + term = os.environ.get("TERM", None) + if term: + for term_name, term_template in TERM_TEMPLATES: + if term.startswith(term_name): + TERM_TEMPLATE = term_template + for hook in ['buffer_switch', 'buffer_title_changed', + 'hotlist_changed', 'upgrade_ended']: + weechat.hook_signal(hook, 'set_term_title_hook', '') + weechat.hook_config('plugins.var.python.%s.*' % SCRIPT_NAME, + 'config_hook', '') + set_term_title_hook('', '', '') + break diff --git a/python/text_item.py b/python/text_item.py deleted file mode 100644 index 4b63a815..00000000 --- a/python/text_item.py +++ /dev/null @@ -1,216 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2012-2016 by nils_2 -# -# add a plain text or evaluated content to item bar -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# 2016-12-12: nils_2, (freenode.#weechat) -# 0.6 : fix problem with multiple windows (reported by Ram-Z) -# -# 2016-09-15: nils_2, (freenode.#weechat) -# 0.5 : add /help text (suggested by gb) -# -# 2014-05-19: nils_2, (freenode.#weechat) -# 0.4 : evaluate content of item (suggested by FlashCode) -# -# 2013-06-27: nils_2, (freenode.#weechat) -# 0.3 : fix: bug with root bar -# -# 2013-01-25: nils_2, (freenode.#weechat) -# 0.2 : make script compatible with Python 3.x -# -# 2012-12-23: nils_2, (freenode.#weechat) -# 0.1 : initial release -# -# requires: WeeChat version 0.3.0 -# -# Development is currently hosted at -# https://github.com/weechatter/weechat-scripts - -try: - import weechat,re - -except Exception: - print("This script must be run under WeeChat.") - print("Get WeeChat now at: http://www.weechat.org/") - quit() - -SCRIPT_NAME = "text_item" -SCRIPT_AUTHOR = "nils_2 " -SCRIPT_VERSION = "0.6" -SCRIPT_LICENSE = "GPL" -SCRIPT_DESC = "add a plain text or evaluated content to item bar" - -# regexp to match ${color} tags -regex_color=re.compile('\$\{([^\{\}]+)\}') - -hooks = {} - -# ================================[ hooks ]=============================== -def add_hook(signal, item): - global hooks - # signal already exists? - if signal in hooks: - return - hooks[item] = weechat.hook_signal(signal, "bar_item_update", "") - -def unhook(hook): - global hooks - if hook in hooks: - weechat.unhook(hooks[hook]) - del hooks[hook] - -def toggle_refresh(pointer, name, value): - option_name = name[len('plugins.var.python.' + SCRIPT_NAME + '.'):] # get optionname - - # option was removed? remove bar_item from struct! - if not weechat.config_get_plugin(option_name): - ptr_bar = weechat.bar_item_search(option_name) - if ptr_bar: - weechat.bar_item_remove(ptr_bar) - return weechat.WEECHAT_RC_OK - else: - return weechat.WEECHAT_RC_OK - - # check if option is new or simply changed - if weechat.bar_item_search(option_name): - weechat.bar_item_update(option_name) - else: - weechat.bar_item_new(option_name,'update_item',option_name) - - weechat.bar_item_update(option_name) - return weechat.WEECHAT_RC_OK - -# ================================[ items ]=============================== -def create_bar_items(): - ptr_infolist_option = weechat.infolist_get('option','','plugins.var.python.' + SCRIPT_NAME + '.*') - - if not ptr_infolist_option: - return - - while weechat.infolist_next(ptr_infolist_option): - option_full_name = weechat.infolist_string(ptr_infolist_option, 'full_name') - option_name = option_full_name[len('plugins.var.python.' + SCRIPT_NAME + '.'):] # get optionname - - if weechat.bar_item_search(option_name): - weechat.bar_item_update(option_name) - else: - weechat.bar_item_new(option_name,'update_item',option_name) - weechat.bar_item_update(option_name) - - weechat.infolist_free(ptr_infolist_option) - -def update_item (data, item, window): - if not data: - return "" - - # window empty? root bar! - if not window: - window = weechat.current_window() - - value = weechat.config_get_plugin(data) - - if value: - value = check_buffer_type(window, data, value) - else: - return "" - - if not value: - return "" - - return substitute_colors(value,window) - -# update item -def bar_item_update(signal, callback, callback_data): - ptr_infolist_option = weechat.infolist_get('option','','plugins.var.python.' + SCRIPT_NAME + '.*') - - if not ptr_infolist_option: - return - - while weechat.infolist_next(ptr_infolist_option): - option_full_name = weechat.infolist_string(ptr_infolist_option, 'full_name') - option_name = option_full_name[len('plugins.var.python.' + SCRIPT_NAME + '.'):] # get optionname - - # check if item exists in a bar and if we have a hook for it - if weechat.bar_item_search(option_name) and option_name in hooks: - weechat.bar_item_update(option_name) - - weechat.infolist_free(ptr_infolist_option) - return weechat.WEECHAT_RC_OK - - -# ================================[ subroutines ]=============================== -def substitute_colors(text,window): - if int(version) >= 0x00040200: - bufpointer = weechat.window_get_pointer(window,"buffer") - return weechat.string_eval_expression(text, {"buffer": bufpointer}, {}, {}) -# return weechat.string_eval_expression(text,{},{},{}) - # substitute colors in output - return re.sub(regex_color, lambda match: weechat.color(match.group(1)), text) - -def check_buffer_type(window, data, value): - bufpointer = weechat.window_get_pointer(window,"buffer") - if bufpointer == "": - return "" - - value = value.split(' ', 1) - if len(value) <= 1: - return "" - - # format is : buffer_type (channel,server,private,all) | signal (e.g: buffer_switch) - channel_type_and_signal = value[0] - if channel_type_and_signal.find('|') >= 0: - channel_type = channel_type_and_signal[0:channel_type_and_signal.find("|")] - signal_type = channel_type_and_signal[channel_type_and_signal.find("|")+1:] - unhook(data) - add_hook(signal_type, data) - else: - channel_type = value[0] - - value = value[1] - - if channel_type == 'all' or weechat.buffer_get_string(bufpointer,'localvar_type') == channel_type: - return value - return "" - -# ================================[ main ]=============================== -if __name__ == "__main__": - if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC,'',''): - weechat.hook_command(SCRIPT_NAME,SCRIPT_DESC, - '', - 'How to use:\n' - '===========\n' - 'Template:\n' - '/set plugins.var.python.text_item. | <${color:name/number}>\n\n' - ' type : all, channel, server, private\n' - ' (you can use: /buffer localvar)\n\n' - ' signal (eg.): buffer_switch, buffer_closing, print, \n' - ' (for a list of all possible signals, see API doc weechat_hook_signal())\n\n' - 'Example:\n' - '=======\n' - 'creates an option for a text item named "nick_text". The item will be created for "channel" buffers. ' - 'The text displayed in the status-bar is "Nicks:" (yellow colored!):\n' - ' /set plugins.var.python.text_item.nick_text "channel ${color:yellow}Nicks:"\n\n' - 'now you have to add the item "nick_text" to the bar.items (use auto-completion or iset.pl!)\n' - ' /set weechat.bar.status.items nick_text\n\n' - 'creates an option to display the terminal width and height in an item bar. item will be updated on signal "signal_sigwinch"\n' - ' /set plugins.var.python.text_item.dimension "all|signal_sigwinch width: ${info:term_width} height: ${info:term_height}"\n', - '', - '', - '') - version = weechat.info_get("version_number", "") or 0 - create_bar_items() - weechat.hook_config( 'plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '' ) diff --git a/python/text_replace.py b/python/text_replace.py index d9e7ef47..3a533513 100644 --- a/python/text_replace.py +++ b/python/text_replace.py @@ -20,6 +20,10 @@ # (this script requires WeeChat 0.3.0 or newer) # # History: +# 2020-06-05, Normen Hansen +# version 0.7: add option to change prefix of entered text +# 2019-06-17, Brad Hubbard +# version 0.6: replace iteritems with items for python3 compatability # 2011-07-17, Sébastien Helleu # version 0.5: allow empty value for pairs or words # 2011-02-01, xt @@ -36,7 +40,7 @@ SCRIPT_NAME = "text_replace" SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "0.5" +SCRIPT_VERSION = "0.7" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Replaces text you write with replacement text" @@ -44,13 +48,14 @@ settings = { 'replacement_pairs': '(:=:),):=:(', # pairs separated by , orig text and replacement separated by = 'replacement_words': 'hhe=heh', # words separated by , orig text and replacement separated by = + 'replacement_prefixes': ':=/', # strings separated by , orig prefix and replacement separated by = } if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - for option, default_value in settings.iteritems(): + for option, default_value in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_value) @@ -59,7 +64,7 @@ "input" : ("/input return", "command_run_input"), } # Hook all hooks ! - for hook, value in hook_command_run.iteritems(): + for hook, value in hook_command_run.items(): w.hook_command_run(value[0], value[1], "") @@ -85,6 +90,12 @@ def command_run_input(data, buffer, command): orig, replaced = replace_item.split('=') # Search for whitespace+word+whitespace and replace the word input_s = re.sub('(\s+|^)%s(\s+|$)' %orig, '\\1%s\\2' %replaced, input_s) + # Iterate prefixes + for replace_item in w.config_get_plugin('replacement_prefixes').split(','): + if replace_item: + orig, replaced = replace_item.split('=') + if input_s.startswith(orig): + input_s = input_s.replace(orig, replaced, 1) # Spit it out w.buffer_set(buffer, 'input', input_s) diff --git a/python/tinyurl.py b/python/tinyurl.py deleted file mode 100644 index e0326c28..00000000 --- a/python/tinyurl.py +++ /dev/null @@ -1,445 +0,0 @@ -# TinyUrl, version 3.8, for weechat version 0.3.0 or later -# -# Listens to all channels for long URLs, and submits them to ln-s.net or -# tinyurl.com for easier links. -# -# Usage: -# -# By default, any URL longer than 30 characters in length is grabbed, -# submitted to a service, and printed in the channel for your eyes only. For -# example, you may see something like this: -# -# [11:21] <@lack> http://www.cbc.ca/story/canada/national/2005/11/12/mcdona -# lds-051112.html?ref=rss -# [11:21] -P- [AKA] http://tinyurl.com/9dthl -# -# Now you can just cut&paste the easier, shorter URL into your favourite -# browser. -# -# If you want to be extra-helpful (or annoying) to certain channels you -# are in, you can actually have the script say the tinyurl.com equivalent -# of all long URLs, by adding the channel to the 'activechans' list. In -# that case, everyone in the channel would see the following: -# -# [11:25] http://www.cbc.ca/story/canada/national/2005/11/12/mcdona -# lds-051112.html?ref=rss -# [11:25] <@lack> [AKA] http://tinyurl.com/9dthl -# -# Configuration: -# -# Run '/help tinyurl' for the actual usage for setting these options: -# -# activechans -# A comma-delimited list of channels you will actually "say" the -# tinyurl in. By default the list is empty. Be warned, some channels -# won't appreciate extra help (or 'noise' as they like to call it), and -# some channels already have bots that do this. Please only enable -# this in channels where the ops have given you permission. -# -# urllength -# An integer, default value 30. Any URL this long or longer will -# trigger a tinyurl event. -# -# printall -# Either "on" or "off", default "on". When ON, you will see the -# tinyurl printed in your window for any channels not in your -# activechans list. When OFF, you will not see any tinyurls except in -# your activechans list. -# -# service -# Either "tinyurl" or "ln-s". tinyurl is the default. -# -# Requirements: -# -# - Designed to run with weechat version 0.3.0 or better. -# http://weechat.flashtux.org/ -# -# - Requires that 'curl' is in the path (tested with curl 7.15.0). -# http://curl.haxx.se/ -# -# Copyright (C) 2005 Jim Ramsay -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, -# USA. -# -# Changelog: -# -# Version 3.8, 12 December, 2009 -# Update WeeChat site -# by FlashCode -# -# Version 3.7, 2 May, 2009 -# Sync with last API changes -# by FlashCode -# -# Version 3.6, 12 March, 2009 -# Conversion to WeeChat 0.3.0+ -# by FlashCode -# -# Version 3.5d, 19 September, 2008 -# Updated with recent api changes -# by Chris Hills -# -# Version 3.5c, 18 July, 2008 -# Send the url to the correct channel in activechans -# by Chris Hills -# -# Version 3.5b, 18 July, 2008 -# Fixed a few prnt functional calls -# by Chris Hills -# -# Version 3.5a, 18 July, 2008 -# Updated for the new script api in weechat 0.3.0-dev -# by Chris Hills -# Updated to use the tinyurl api instead of screen-scraping -# by Chris Hills -# -# Version 3.5, June 3, 2008 -# Added ln-s.net support -# Turned off some ugly verbose output unless requested -# -# Version 3.4, June 3, 2008 -# tinyurl.com changed their output page slightly, and broke our parsing -# -# Version 3.3, July 4, 2006 -# Catches possible error in os.waitpid -# Properly prints tinyurls in query windows -# -# Version 3.2, June 15, 2006 -# Multiple configuration bugfixes, pointed out by Stalwart on #weechat. -# -# Version 3.1, June 15, 2006 -# Now kills any leftover curl processes when the script is unloaded. -# Thanks again to kolter for the great idea! -# Also cleaned up /tinyurl command, added comletion_template, updated -# help text, improved option parsing logic, etc. -# -# Version 3.0, June 15, 2006 -# Fixes "tinyurl script sometimes makes weechat freeze" issue by using -# the new timer handlers available in Weechat 0.1.9 -# Also includes URL detection fix from Raimund Specht -# . -# -# Version 2.0, Dec 13, 2005 -# Also catches https, ftp, and ftps URLs, thanks to kolter for the -# suggestion! -# -# Version 1.1, Dec 2, 2005 -# Fixed undefined 'urlend' thanks to kolter@irc.freenode.org#weechat -# -# TODO: -# -# - Handle outgoing messages and replace long urls with the tinyurl -# equivalent automatically. -# - On load, check that 'curl' is installed, and fail if not. -# - -import os, tempfile, re -try: - import urllib -except: - raise ImportError("You need to reload the python plugin to reload urllib") -import weechat - -class TryAgain(UserWarning): - def __init__(self, message): - super(UserWarning, self).__init__(message) - -# Register with weechat -weechat.register( "TinyUrl", "Jim Ramsay", "3.8", "GPL", "Waits for URLs and sends them to 'tinyurl' for you", "tinyurlShutdown", "" ) - -# Global variables -tinyurlParams = ("urllength","activechans","printall","service","debug") -tinyurlProcessList = {} - -# Set default settings values: -if weechat.config_get_plugin('urllength') == "": - weechat.config_set_plugin('urllength', "30") -if not weechat.config_get_plugin('printall') in ("on", "off"): - weechat.config_set_plugin('printall', "on") -if not weechat.config_get_plugin('service') in ("tinyurl", "ln-s"): - weechat.config_set_plugin('service', "tinyurl") -if not weechat.config_get_plugin('debug') in ("on", "off"): - weechat.config_set_plugin('debug', "off") - -# Start the timer thread and register handlers -weechat.hook_timer( 1000, 0, 0, "tinyurlCheckComplete", "" ) -weechat.hook_signal("*,irc_in_privmsg", "tinyurlHandleMessage", "") -weechat.hook_command("tinyurl", "Sets/Gets 'tinyurl' settings.", "urllength|activechans|printall|service|debug", \ - "[ [[=] ]]", -"""When run without arguments, displays all tinyurl settings - - : Sets or displays a single tinyurl setting. One of: - activechans [[=] #chan1[,#chan2...]] - List of channels where others will see your tinyurls. - Default: None - urllength [[=] length] - Will not create tinyurls for any URLs shorter than this. - Default: 30 - printall [[=] on|off] - When off, will not display private tinyurls, just those - displayed publicly in your "active channels" - Default: on - service [[=] tinyurl|ln-s] - Sets the service used for shortening the URL. - Default: tinyurl - debug [[=] on|off] - Creates extra debug output in the server window when on. - Default: off""", - "tinyurlMain", "" - ) - -def tinyurlShutdown(): - """Cleanup - Kills any leftover child processes""" - if len(tinyurlProcessList.keys()) > 0: - weechat.prnt( weechat.buffer_search("",""), "-TinyUrl- Cleaning up unfinished processes:" ) - for pid in tinyurlProcessList.keys(): - weechat.prnt( weechat.buffer_search("",""), " Process %d" % pid ) - try: - os.kill(pid, 9) - os.waitpid( pid, 0 ) - except: - weechat.prnt( weechat.buffer_search("",""), " Cleanup failed, skipping" ) - return weechat.WEECHAT_RC_OK - -def tinyurlGet( name = "" ): - """Gets a variable value""" - if name == "": - weechat.prnt( weechat.buffer_search("",""), "-TinyUrl- Get all:" ) - for name in tinyurlParams: - weechat.prnt( weechat.buffer_search("",""), " %s = %s" % (name, weechat.config_get_plugin(name)) ) - else: - weechat.prnt( weechat.buffer_search("",""), "-TinyUrl- Get:" ) - if name in tinyurlParams: - weechat.prnt( weechat.buffer_search("",""), " %s = %s" % (name, weechat.config_get_plugin(name)) ) - else: - weechat.prnt( weechat.buffer_search("",""), " Unknown parameter \"%s\", try '/help tinyurl'" % name ) - return - -def tinyurlSet( name, value ): - """Sets a variable value""" - if value == "": - tinyurlGet( name ) - else: - weechat.prnt( weechat.buffer_search("",""), "-TinyUrl- Set:" ) - if name in tinyurlParams: - if name == "printall" or name == "debug": - if value == "0" or value.lower() == "no" or value.lower() == "off": - value = "off" - elif value == "1" or value.lower() == "yes" or value.lower() == "on": - value = "on" - else: - weechat.prnt( weechat.buffer_search("",""), " %s must be one of 'on' or 'off'" % name ) - weechat.prnt( weechat.buffer_search("",""), " value = '%s'" % value ) - return - elif name == "service": - if value.lower() in ("tinyurl", "ln-s"): - value = value.lower() - else: - weechat.prnt( weechat.buffer_search("",""), " service must be one of 'tinyurl' or 'ln-s'" ) - weechat.prnt( weechat.buffer_search("",""), " value = '%s'" % value ) - return - elif name == "urllength": - try: - v = int(value) - if v < 0 or v > 100: - weechat.prnt( weechat.buffer_search("",""), " urllength must be between 0 and 100" ) - weechat.prnt( weechat.buffer_search("",""), " value = '%s'" % value ) - return - except: - weechat.prnt( weechat.buffer_search("",""), " urllength must be a valid integer" ) - weechat.prnt( weechat.buffer_search("",""), " value = '%s'" % value ) - return - elif name == "activechans": - vs = re.split(", |,| ", value) - values = [] - for v in vs: - if v.startswith("#"): - values.append(v) - value = ",".join(values) - weechat.config_set_plugin(name, value) - weechat.prnt( weechat.buffer_search("",""), " %s = %s" % (name, weechat.config_get_plugin(name)) ) - else: - weechat.prnt( weechat.buffer_search("",""), " Unknown parameter \'%s\'" % name ) - return - -def tinyurlMain( data, buffer, args ): - """Main handler for the /tinyurl command""" - args = args.split( " " ) - while '' in args: - args.remove('') - while ' ' in args: - args.remove(' ') - if len(args) == 0: - tinyurlGet() - else: - name = args[0] - value = "" - if len(args) > 1: - if args[1] == "=": - value = " ".join(args[2:]) - else: - value = " ".join(args[1:]) - tinyurlSet( args[0], value ) - else: - tinyurlGet( name ) - return weechat.WEECHAT_RC_OK - -def tinyurlGetUrl( url, channel, server ): - """Starts a background process which will query the appropriate service and - put the result in a file that the timer function 'tinyurlCheck' will find and - parse.""" - global tinyurlProcessList - handle, filename = tempfile.mkstemp( prefix="weechat-tinyurl.py-" ) - os.close(handle) - service = weechat.config_get_plugin('service') - if service == "tinyurl": - cmd = ("curl -d url=%s http://tinyurl.com/api-create.php --stderr /dev/null -o %s" % \ - (urllib.quote(url), filename)).split() - else: - cmd = ("curl http://ln-s.net/home/api.jsp?url=%s --stderr /dev/null -o %s" % \ - (urllib.quote(url), filename)).split() - try: - pid = os.spawnvp( os.P_NOWAIT, cmd[0], cmd ) - if weechat.config_get_plugin('debug') == "on": - weechat.prnt( weechat.buffer_search(server,""), "Setting ProcessList[%d] to (%s, %s, %s)" % \ - (pid, filename, channel, server)) - tinyurlProcessList[pid] = (filename, url, service, channel, server) - except Exception, e: - weechat.prnt( weechat.buffer_search("",""), "-TinyUrl- Error: Could not spawn curl: %s" % (e) ) - -def parseTinyurl( file ): - turl = None - for line in file: - if( line.startswith("http://tinyurl.com") ): - turl = line - break - if turl is None: - weechat.prnt( weechat.buffer_search("",""), "-TinyUrl- Error: Unrecognized response from server" ) - weechat.prnt( weechat.buffer_search("",""), " Maybe tinyurl.com changed their format again." ) - weechat.prnt( weechat.buffer_search("",""), " Try '/tinyurl service ln-s' to use ln-s.net instead" ) - return turl - -def parseLns( file ): - turl = None - for line in file: - (code, message) = line.split(" ", 2) - if code == "200": - return message.rstrip() - elif code == "503": - # Try again, respawn curl - weechat.prnt( weechat.buffer_search("",""), "-TinyUrl- Warning: ln-s.net is busy, trying again shortly" ) - raise TryAgain(line) - else: - weechat.prnt( weechat.buffer_search("",""), "-TinyUrl- Error: Error response from server: %s" % (line) ) - return None - -def tinyurlParsefile( filename, service ): - """Parses the given HTML file and pulls out the tinyurl.""" - turl = None - try: - html = open(filename, "r") - if service == "tinyurl": - turl = parseTinyurl(html) - else: - turl = parseLns(html) - html.close() - except Exception, e: - weechat.prnt( weechat.buffer_search("",""), "-TinyUrl- Error: Could not open result file %s: %s" % (filename, e) ) - return turl - -def tinyurlPrint( original, url, channel, server ): - """Prints the new tinyurl either to just you, or to the whole channel""" - where = "Unknown" - locstart = original.find("//") - if locstart > -1: - locend = original.find("/", locstart + 2) - if locend > -1: - where = original[locstart + 2:locend] - activeChans = weechat.config_get_plugin('activechans').split(',') - if channel in activeChans: - weechat.command( weechat.buffer_search("irc",server + "." + channel), "/msg %s [AKA] %s" % (channel, url) ) - else: - if weechat.config_get_plugin('debug') == "on": - weechat.prnt( weechat.buffer_search("irc",server + "." + channel), "Printing url to channel '%s', server '%s'" % \ - (channel, server) ) - weechat.prnt( weechat.buffer_search("irc",server + "." + channel), "[AKA] %s (%s)" % (url, where) ) - -def tinyurlFindUrlstart( msg, start = 0 ): - """Finds the beginnings of URLs""" - index = -1 - if start < 0 or start >= len(msg): - return index - for prefix in ( "http://", "https://", "ftp://", "ftps://" ): - index = msg.find( prefix, start ) - if index > -1: - break - return index - -def tinyurlFindUrlend( msg, urlstart ): - """Finds the ends of URLs (Strips following punctuation)""" - m = msg[urlstart:] - index = m.find( " " ) - if index == -1: - index = len(m) - while msg[index-1] in ( "?", ".", "!" ): - index -= 1 - return index + urlstart - -def tinyurlCheckComplete(data, remaining_calls): - """The periodic poll of all waiting processes""" - global tinyurlProcessList - for pid in tinyurlProcessList.keys(): - (filename, url, service, channel, server) = tinyurlProcessList[pid] - try: - (p, er) = os.waitpid( pid, os.WNOHANG ) - if p != 0: - if er == 0: - try: - tinyurl = tinyurlParsefile(filename, service) - if tinyurl is not None: - tinyurlPrint( url, tinyurl, channel, server ) - except TryAgain: - tinyurlGetUrl(url, channel, server) - else: - weechat.prnt( weechat.buffer_search("",""), "-TinyUrl- Error: 'curl' did not run properly" ) - os.unlink(filename) - del tinyurlProcessList[pid] - except OSError, e: - weechat.prnt( weechat.buffer_search("",""), "-TinyUrl- Error: 'curl' process not found: %s" % (e) ) - os.unlink(filename) - del tinyurlProcessList[pid] - return weechat.WEECHAT_RC_OK - -def tinyurlHandleMessage( data, signal, signal_data ): - """Handles IRC PRIVMSG and checks for URLs""" - (server,sig) = signal.split(",",1) - maxlen = int(weechat.config_get_plugin( "urllength" )) - activeChans = weechat.config_get_plugin('activechans').split(',') - onlyActiveChans = weechat.config_get_plugin('printall') == "off" - (source, type, channel, msg) = signal_data.split(" ", 3) - if onlyActiveChans and channel not in activeChans: - return weechat.WEECHAT_RC_OK - if not channel.startswith("#"): - channel = source.split("!", 2)[0][1:] - urlstart = tinyurlFindUrlstart( msg ) - while urlstart > -1 and urlstart is not None: - urlend = tinyurlFindUrlend( msg, urlstart ) - url = msg[urlstart:urlend] - if len(url) >= maxlen: - tinyurlGetUrl(url, channel, server) - # Check for more URLs - urlstart = tinyurlFindUrlstart( msg, urlend+1 ) - return weechat.WEECHAT_RC_OK diff --git a/python/title.py b/python/title.py index 7719d76b..e746c96f 100644 --- a/python/title.py +++ b/python/title.py @@ -22,6 +22,8 @@ # (this script requires WeeChat 0.3.0 or newer) # # History: +# 2016-03-31, devkev +# version 0.9, Make hotlist optional # 2016-05-01, Ferus # version 0.8, Add ability to prefix and suffix the title, current # buffer, and hotlist buffers. As well as specify hotlist separator @@ -44,7 +46,7 @@ SCRIPT_NAME = "title" SCRIPT_AUTHOR = "xt " -SCRIPT_VERSION = "0.8" +SCRIPT_VERSION = "0.9" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Set screen title to current buffer name + hotlist items with configurable priority level" @@ -61,6 +63,7 @@ "hotlist_buffer_suffix": '', "current_buffer_prefix": '', "current_buffer_suffix": '', + "show_hotlist" : 'on', } hooks = ( @@ -84,22 +87,23 @@ def update_title(data, signal, signal_data): title += w.buffer_get_string(w.current_buffer(), 'name') title += w.config_get_plugin('current_buffer_suffix') - # hotlist buffers - hotlist = w.infolist_get('hotlist', '', '') - pnumber = w.config_get_plugin('hotlist_number_prefix') - snumber = w.config_get_plugin('hotlist_number_suffix') - pname = w.config_get_plugin('hotlist_buffer_prefix') - sname = w.config_get_plugin('hotlist_buffer_suffix') - separator = w.config_get_plugin('hotlist_separator') - while w.infolist_next(hotlist): - priority = w.infolist_integer(hotlist, 'priority') - if priority >= int(w.config_get_plugin('title_priority')): - number = w.infolist_integer(hotlist, 'buffer_number') - thebuffer = w.infolist_pointer(hotlist, 'buffer_pointer') - name = w.buffer_get_string(thebuffer, 'short_name') - title += ' {0}{1}{2}{3}{4}{5}{6}'.format(pnumber, \ - number, snumber, separator, pname, name, sname) - w.infolist_free(hotlist) + if w.config_get_plugin('show_hotlist') == 'on': + # hotlist buffers + hotlist = w.infolist_get('hotlist', '', '') + pnumber = w.config_get_plugin('hotlist_number_prefix') + snumber = w.config_get_plugin('hotlist_number_suffix') + pname = w.config_get_plugin('hotlist_buffer_prefix') + sname = w.config_get_plugin('hotlist_buffer_suffix') + separator = w.config_get_plugin('hotlist_separator') + while w.infolist_next(hotlist): + priority = w.infolist_integer(hotlist, 'priority') + if priority >= int(w.config_get_plugin('title_priority')): + number = w.infolist_integer(hotlist, 'buffer_number') + thebuffer = w.infolist_pointer(hotlist, 'buffer_pointer') + name = w.buffer_get_string(thebuffer, 'short_name') + title += ' {0}{1}{2}{3}{4}{5}{6}'.format(pnumber, \ + number, snumber, separator, pname, name, sname) + w.infolist_free(hotlist) # suffix title += w.config_get_plugin('title_suffix') diff --git a/python/tmux_env.py b/python/tmux_env.py index afd82492..2ae35c3a 100644 --- a/python/tmux_env.py +++ b/python/tmux_env.py @@ -21,35 +21,27 @@ 2014-02-03 Aron Griffis version 2: python 2.6 compatible subprocess.check_output() + + 2020-01-03 dobbymoodge ( https://github.com/dobbymoodge/ ) + version 3: python 3.x compatibility + + 2020-10-30 Friedrich Delgado + version 3.1: fix python 3.6 compatibility and remove python 2 support """ from __future__ import absolute_import, unicode_literals +import weechat as w + import fnmatch import os import subprocess -if not hasattr(subprocess, 'check_output'): - def check_output(*popenargs, **kwargs): - process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) - output, unused_err = process.communicate() - retcode = process.poll() - if retcode: - cmd = kwargs.get("args") - if cmd is None: - cmd = popenargs[0] - raise subprocess.CalledProcessError(retcode, cmd) - return output - subprocess.check_output = check_output - del check_output - -import weechat as w - -SCRIPT_NAME = "tmux_env" -SCRIPT_AUTHOR = "Aron Griffis " -SCRIPT_VERSION = "2" +SCRIPT_NAME = "tmux_env" +SCRIPT_AUTHOR = "Aron Griffis " +SCRIPT_VERSION = "3.1" SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Update weechat environment from tmux" +SCRIPT_DESC = "Update weechat environment from tmux" settings = { 'interval': '30', # How often in seconds to check for updates @@ -58,7 +50,7 @@ def check_output(*popenargs, **kwargs): # environment updates, not removals. For removals, include the variable # name prefixed by a minus sign. For example, to add/remove exclusively # the DISPLAY variable, include="DISPLAY,-DISPLAY" - # + # # Globs are also accepted, so you can ignore all variable removals with # exclude="-*" @@ -68,6 +60,7 @@ def check_output(*popenargs, **kwargs): TIMER = None + def set_timer(): """Update timer hook with new interval""" @@ -75,7 +68,8 @@ def set_timer(): if TIMER: w.unhook(TIMER) TIMER = w.hook_timer(int(w.config_get_plugin('interval')) * 1000, - 0, 0, 'timer_cb', '') + 0, 0, 'timer_cb', '') + def config_cb(data, option, value): """Reset timer when interval option is updated""" @@ -84,18 +78,20 @@ def config_cb(data, option, value): set_timer() return w.WEECHAT_RC_OK + def timer_cb(buffer, args): """Check if tmux is attached, update environment""" - attached = os.access(SOCK, os.X_OK) # X bit indicates attached + attached = os.access(SOCK, os.X_OK) # X bit indicates attached if attached: update_environment() return w.WEECHAT_RC_OK + def update_environment(): """Updates environment from tmux showenv""" - env = subprocess.check_output(['tmux', 'showenv']) + env = subprocess.check_output(['tmux', 'showenv']).decode() for line in env.splitlines(): name = line.split('=', 1)[0] if check_include(name) and not check_exclude(name): @@ -104,50 +100,56 @@ def update_environment(): else: add_env(name, line.split('=', 1)[1]) + def check_include(name): globs = comma_split_config('include') return check_match(name, globs) + def check_exclude(name): globs = comma_split_config('exclude') return check_match(name, globs) + def check_match(name, globs): for g in globs: if fnmatch.fnmatch(name, g): return True + def comma_split_config(name): config = w.config_get_plugin(name) return filter(None, (s.strip() for s in config.split(','))) + def add_env(name, value): old = os.environ.get(name) if old != value: w.prnt("", "%s: add %s=%r (was %r)" % (SCRIPT_NAME, name, value, old)) os.environ[name] = value + def remove_env(name): old = os.environ.get(name) if old is not None: w.prnt("", "%s: remove %s (was %r)" % (SCRIPT_NAME, name, old)) del os.environ[name] + if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, - SCRIPT_DESC, '', ''): - for option, default_value in settings.iteritems(): + SCRIPT_DESC, '', ''): + for option, default_value in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_value) global SOCK SOCK = None - if 'TMUX' in os.environ.keys(): + if 'TMUX' in os.environ: # We are running under tmux socket_data = os.environ['TMUX'] - SOCK = socket_data.rsplit(',',2)[0] + SOCK = socket_data.rsplit(',', 2)[0] if SOCK: - w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", - "config_cb", "") + w.hook_config("plugins.var.python.{}.*".format(SCRIPT_NAME), "config_cb", "") set_timer() diff --git a/python/topicdiff.py b/python/topicdiff.py index 881a416d..e6225f4d 100644 --- a/python/topicdiff.py +++ b/python/topicdiff.py @@ -4,13 +4,16 @@ import re -from itertools import izip_longest +try: + from itertools import zip_longest +except ImportError: + from itertools import izip_longest as zip_longest import weechat SCRIPT_NAME = "topicdiff" SCRIPT_AUTHOR = "Dafydd Harries " -SCRIPT_VERSION = "0.3" +SCRIPT_VERSION = "0.4" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Show differences between old and new topics." @@ -25,7 +28,7 @@ def topic_changed(buffer, new_topic): old_chunks = topic_chunks(topics[buffer]) new_chunks = topic_chunks(new_topic) - for old_chunk, new_chunk in izip_longest(old_chunks, new_chunks): + for old_chunk, new_chunk in zip_longest(old_chunks, new_chunks): if old_chunk and old_chunk not in new_chunks: weechat.prnt(buffer, '%s-\t%s' % ( weechat.color('red'), diff --git a/python/topicdiff_alt.py b/python/topicdiff_alt.py new file mode 100644 index 00000000..c76fa228 --- /dev/null +++ b/python/topicdiff_alt.py @@ -0,0 +1,70 @@ +import weechat +import diff_match_patch +import re + +weechat.register('topicdiff_alt', 'Juerd <#####@juerd.nl>', '1.01', 'PD', "Announce topic with changes highlighted", '', '') + +def topic(data, tags, msg): + server = tags.split(",")[0] + + match = re.search(r':(\S+)\s+TOPIC\s+(\S+)\s+:(.*)', msg) + + if not match: + return weechat.WEECHAT_RC_ERROR + + usermask, channel, newtopic = match.groups() + nick, host = usermask.split("!", 1) + + buffer = weechat.buffer_search("irc", server + "." + channel) + weechat.prnt("", server + "." + channel) + + if not buffer: + return weechat.WEECHAT_RC_ERROR + + oldtopic = weechat.buffer_get_string(buffer, "title") + if oldtopic == None: + oldtopic = "" + + dmp = diff_match_patch.diff_match_patch() + diff = dmp.diff_main(oldtopic, newtopic) + dmp.diff_cleanupEfficiency(diff) + + topic = "" + + color_reset = weechat.color("reset") + color_ins = weechat.color(weechat.config_get_plugin("color_ins")) + color_del = weechat.color(weechat.config_get_plugin("color_del")) + + for chunk in diff: + changed, text = chunk + + topic += "%s%s%s" % ( + # 0 (unchanged), 1 (added), -1 (removed) + ["", color_ins, color_del][changed], + text, + ["", color_reset, color_reset][changed] + ) + + weechat.prnt_date_tags(buffer, 0, "irc_topicdiff", + "%s%s%s%s has changed topic for %s%s%s: %s" % ( + weechat.prefix("network"), + weechat.color(weechat.info_get("irc_nick_color", nick)) \ + if weechat.config_boolean("irc.look.color_nicks_in_server_messages") \ + else weechat.color("chat_nick"), + nick, + color_reset, + weechat.color("chat_channel"), + channel, + color_reset, + topic + )) + + return weechat.WEECHAT_RC_OK + +weechat.hook_signal("*,irc_in_topic", "topic", "") + +if not weechat.config_is_set_plugin("color_ins"): + weechat.config_set_plugin("color_ins", "lightcyan") + +if not weechat.config_is_set_plugin("color_del"): + weechat.config_set_plugin("color_del", "darkgray") diff --git a/python/triggerreply.py b/python/triggerreply.py index 352e40c8..1e7d6e4e 100644 --- a/python/triggerreply.py +++ b/python/triggerreply.py @@ -1,5 +1,5 @@ """ -Copyright (c) 2014 by Vlad Stoica +Copyright (c) 2014-2018 by Vlad Stoica This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,127 +21,398 @@ ability to ignore channels. 16-08-2015 - Vlad Stoica Fixed a bug where replies couldn't have `:' in them. +15-02-2018 - Vlad Stoica +Added regex support in triggers, and edited syntax of adding triggers. +The command is now 'add "trigger" "reply"'. Quote marks can be escaped +in triggers or replies by prefixing them with a backslash ('\'). For +example, 'add "\"picture\"" "say \"cheese\"!"' is a valid command and +will reply with 'say "cheese"!' whenever it finds '"picture"' sent. +29-04-2020 - Fisher +Some new functions: + - multiple matches ("match1|match2|match3" etc) + - random selected multiple replites ("reply1|reply2|reply3" etc) + - can ignore nicks (and ignore itself) for example bots + - matches now case insensitive + - utf-8 added + - cooldown (max. n replies in t time) + - random delay, so more human-like + - even more randomness: can specify randomness of the reply/replies group. +2020-09-05 - new function - actions +2021-05-06 - Sébastien Helleu +Add compatibility with WeeChat >= 3.2 (XDG directories). + +2022-07-22 - Nils Görs (libera.#weechat) + - fix bug: https://github.com/weechat/scripts/issues/459 + - add autocompletion + - add option for sqlite3 filename + - help text will be displayed using /help command Bugs: not that i'm aware of. """ -#pylint: disable-msg=too-many-arguments - try: import weechat import sqlite3 - IMPORT_ERR = 0 + import re + import random + import sys except ImportError: - IMPORT_ERR = 1 + raise ImportError("Failed importing weechat, sqlite3, re or random") import os SCRIPT_NAME = "triggerreply" SCRIPT_AUTHOR = "Vlad Stoica " -SCRIPT_VERSION = "0.2" +SCRIPT_VERSION = "0.4.4" SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Auto replies when someone sends a specified trigger." +SCRIPT_DESC = "Auto replies when someone sends a specified trigger. Now with 100% more regex!" +pcooldown = 1 +""" This is all I need so far :) """ +colorcodes = { "^Cb":"\x02","^CR":"\x0F","^Ci":"\x1D" } + +def cooldown_timer_cb(data, remaining_calls): + global pcooldown + if ( pcooldown > 0 ): + pcooldown -= 1 + return weechat.WEECHAT_RC_OK + +def print_help(): + weechat.prnt('','%s%s %s %s' % (weechat.prefix('error'),SCRIPT_NAME,': see /help',SCRIPT_NAME)) + return weechat.WEECHAT_RC_OK -def phelp(): - """ print the help message """ - weechat.prnt("", "Triggerreply (trigge.rs) plugin. Automatically \ -replies over specified triggers") - weechat.prnt("", "------------") - weechat.prnt("", "Usage: /triggerreply [list | add trigger:reply \ -| remove trigger | ignore server.#channel | parse server.#channel]") +def debug(mlevel, message): + if int(weechat.config_get_plugin('debug')) >= int(mlevel): + weechat.prnt("", "DEBUG: %s" % message) -def create_db(): +def create_db(delete=False): + debug(3, "Creating basic database.") """ create the sqlite database """ - tmpcon = sqlite3.connect(DBFILE) - cur = tmpcon.cursor() - cur.execute("CREATE TABLE triggers(id INTEGER PRIMARY KEY, trig VARCHAR, reply VARCHAR);") - cur.execute("INSERT INTO triggers(trig, reply) VALUES ('trigge.rs', 'Automatic reply');") + if delete: + os.remove(db_file) + temp_con = sqlite3.connect(db_file) + cur = temp_con.cursor() + cur.execute("CREATE TABLE triggers(id INTEGER PRIMARY KEY, trig VARCHAR, reply VARCHAR, prob INTEGER);") + cur.execute("INSERT INTO triggers(trig, reply, prob) VALUES ('trigge.rs', 'Automatic reply', '1');") cur.execute("CREATE TABLE banchans(id INTEGER PRIMARY KEY, ignored VARCHAR);") cur.execute("INSERT INTO banchans(ignored) VALUES ('rizon.#help');") - tmpcon.commit() + cur.execute("CREATE TABLE ignorenicks(id INTEGER PRIMARY KEY, ignored VARCHAR);") + cur.execute("INSERT INTO ignorenicks(ignored) VALUES ('dumanet.#DumaNet.Neo');") + temp_con.commit() cur.close() -def search_trig_cb(data, buffer, date, tags, displayed, highlight, prefix, message): + + +def check_db(): + temp_con = sqlite3.connect(db_file) + cur = temp_con.cursor() + + try: + """ Try to add record enchated with probability """ + cur.execute("INSERT INTO triggers(trig, reply, prob) VALUES (?,?,?)", ('JJORAIGPADMLOLYUGSBZ',"",1)) + except: + """ If it fails, hope the best and assume it is just an older schema """ + cur.execute("ALTER TABLE triggers ADD COLUMN prob INTEGER") + + """ Clean up the mess """ + cur.execute("DELETE FROM triggers WHERE trig='JJORAIGPADMLOLYUGSBZ'") + temp_con.commit() + cur.close() + + + +def search_trig_cb(data, buf, date, tags, displayed, highlight, prefix, message): """ function for parsing sent messages """ - database = sqlite3.connect(DBFILE) - database.text_factory = str + global pcooldown + + """ Prevent infinite loop/flood, no more messages than n (approx 3) in 300 secs """ + if ( pcooldown > 300 ): return weechat.WEECHAT_RC_OK + + """ Save some CPU cycles """ + if (prefix == '-->' or prefix == '<--' or prefix == '--' or prefix == ' *' or prefix == ""): return weechat.WEECHAT_RC_OK + + bufname = weechat.buffer_get_string(buf, "name") + + if bufname == 'weechat': return weechat.WEECHAT_RC_OK + + """ Ignore myself """ + mynick = weechat.buffer_get_string(buf, "localvar_nick") + if re.search('[@+~]?' + mynick, prefix): + """ weechat.prnt("", "Ignored myself.") """ + return weechat.WEECHAT_RC_OK + + + database = sqlite3.connect(db_file) cursor = database.cursor() - ignored_chan = False + pure = weechat.string_remove_color(message,"") + + debug(1, "Nick in question:'%s" % bufname + '.' + prefix.translate(None,'@+~') + "'") + + for row in cursor.execute("SELECT ignored from ignorenicks;"): + if re.search(row[0], bufname + '.' + prefix.translate(None,'@+~')): + """ weechat.prnt("", "Nick ignored: %s" % row[0]) """ + return weechat.WEECHAT_RC_OK + for row in cursor.execute("SELECT ignored from banchans;"): - if weechat.buffer_get_string(buffer, "name") == row[0]: - ignored_chan = True - if not ignored_chan: - for row in cursor.execute("SELECT reply FROM triggers WHERE trig = ?", (str(message),)): - weechat.command(buffer, "/say %s" % row) + if bufname == row[0]: + return weechat.WEECHAT_RC_OK + + for row in cursor.execute("SELECT * FROM triggers"): + delay = random.randint(4,9) + + pattern = row[1].encode('utf8') + pattern = pattern.replace("%N", mynick) + replydata = row[2].encode('utf8') + prob = int(row[3]) + + for ccode, chex in list(colorcodes.items()): + replydata = replydata.replace(ccode,chex) + + try: + nick = re.sub('^[+%@]','', prefix) + debug(2, "prefix: %s, mynick: %s, nick: %s, pattern: %s, prob: %s, pure: %s" % (prefix, mynick, nick, pattern, str(prob), pure)) + + r = re.compile(pattern,re.I | re.U) + + if r.search(pure) is not None: + weechat.prnt("", "Matched") + + """ Meh, not really sure how random it is, but probably good enough """ + if ( prob > 1 and random.randint(1,prob) == 1): + debug(1, "Randomly ignored.") + return weechat.WEECHAT_RC_OK + + weechat.prnt("", "Match: %s" % r.search(pure).group(0)) + myreply = "n/a" + if prob < 0: + """ -1 means this is action, not saying """ + delay = 0 + debug(1,"Command mode triggered.") + infolist = weechat.infolist_get("irc_nick", "", bufname.replace(".",",")) + while weechat.infolist_next(infolist): + _nick = weechat.infolist_string(infolist, 'name') + if _nick == nick: + hostinfo = weechat.infolist_string(infolist,'host') + break + mask = hostinfo.split('@')[1] + weechat.prnt("", "mask: %s" % mask) + weechat.infolist_free(infolist) + + for myreply in replydata.split('|'): + myreply = myreply.replace("%n", nick) + myreply = myreply.replace("%N", mynick) + myreply = myreply.replace("%m", mask) + myreply = myreply.replace("%c", bufname.split(".")[1]) + weechat.prnt("", "Command: %s" % myreply) + if delay > 0: + weechat.command(buf, "/wait %s %s" % (delay, myreply)) + else: + weechat.command(buf, "%s" % myreply) + delay++2 + + return weechat.WEECHAT_RC_OK + + myreply = random.choice(replydata.split('|')) + myreply = myreply.replace("%n", nick) + weechat.prnt("", "reply: %s" % myreply) + weechat.prnt("", "%s triggered." % pattern) + weechat.command(buf, "/wait %s /say %s" % (delay, myreply)) + pcooldown += 100 + except: + weechat.prnt("", "NOMatch") + if pattern == pure: + weechat.command(buf, "/wait %s /say %s" % (delay, myreply)) + pcooldown += 120 + return weechat.WEECHAT_RC_OK + def command_input_callback(data, buffer, argv): """ function called when `/triggerreply args' is run """ - database = sqlite3.connect(DBFILE) + database = sqlite3.connect(db_file) cursor = database.cursor() command = argv.split() + if len(command) == 0: - phelp() - elif command[0] == "list": + return weechat.WEECHAT_RC_ERROR + + if command[0] == "list": weechat.prnt("", "List of triggers with replies:") for row in cursor.execute("SELECT * FROM triggers;"): - weechat.prnt("", str(row[0]) + ". " + row[1] + " -> " + row[2]) + weechat.prnt("", (str(row[0]) + ". " + str(row[1]) + " -> " + str(row[2]) + " [Prob: " + str(row[3]) + "]")) +# weechat.prnt("", str(row[0]) + ". " + row[1].encode('utf8') + " -> " + row[2].encode('utf8') + " [Prob: " + str(row[3]) + "]") + weechat.prnt("", "\nList of ignored channels:") for row in cursor.execute("SELECT ignored FROM banchans;"): weechat.prnt("", row[0]) + + weechat.prnt("", "\nList of ignored nicks:") + for row in cursor.execute("SELECT ignored FROM ignorenicks;"): + weechat.prnt("", str(row[0])) + elif command[0] == "add": + if len(argv) == len(command[0]): + print_help() + return weechat.WEECHAT_RC_ERROR + + if argv.count('"') < 4: + print_help() + return weechat.WEECHAT_RC_ERROR + + pos = [] + for k, v in enumerate(argv): + if v == '"' and argv[k - 1] != '\\': + pos.append(k) + + if (len(pos) != 6 and len(pos) != 4): + print_help() + return weechat.WEECHAT_RC_ERROR + + trigger = argv[pos[0] + 1:pos[1]].replace('\\"', '"') + reply = argv[pos[2] + 1:pos[3]].replace('\\"', '"') + + prob = 1 + if (len(pos) == 6): + prob = int(argv[pos[4] + 1:pos[5]]) + try: - trigger = argv[4:].split(":")[0] - #reply = ''.join(argv[4:].split(":")[1:]) - reply = argv[4:].replace(trigger+":", '') - cursor.execute("INSERT INTO triggers(trig, reply) VALUES (?,?)", (trigger, reply,)) + cursor.execute("INSERT INTO triggers(trig, reply, prob) VALUES (?,?,?)", (trigger, reply, prob)) +# cursor.execute("INSERT INTO triggers(trig, reply, prob) VALUES (?,?,?)", (trigger.encode('utf8'), reply.encode('utf8'), prob)) +# cursor.execute("INSERT INTO triggers(trig, reply, prob) VALUES (?,?,?)", (trigger.decode('utf8'), reply.decode('utf8'), prob)) except: - weechat.prnt("", "Could not add trigger.\n") - weechat.prnt("", "Usage: /triggerreply add trigger:reply") - weechat.prnt("", "Example: /triggerreply add lol:hue hue") - else: - database.commit() - weechat.prnt("", "Trigger added successfully!") + print_help() + weechat.prnt("", "DB Insert error.") + return weechat.WEECHAT_RC_ERROR + + database.commit() + weechat.prnt("", "Trigger added successfully!") elif command[0] == "remove": + if len(argv) == len(command[0]): + print_help() + return weechat.WEECHAT_RC_ERROR + try: - cursor.execute("DELETE FROM triggers WHERE trig = ?", (argv[7:],)) + cursor.execute("DELETE FROM triggers WHERE id = ?", (argv[7:],)) except: - weechat.prnt("", "Could not remove trigger.") - weechat.prnt("", "Usage: /triggerreply remove trigger") - weechat.prnt("", "Example: /triggerreply remove hue") - else: - database.commit() - weechat.prnt("", "Trigger successfully removed.") + print_help() + return weechat.WEECHAT_RC_ERROR + + database.commit() + weechat.prnt("", "Trigger successfully removed.") elif command[0] == "ignore": + if len(argv) == len(command[0]): + print_help() + return weechat.WEECHAT_RC_ERROR + try: cursor.execute("INSERT INTO banchans(ignored) VALUES (?)", (command[1],)) except: - weechat.prnt("", "Could not add channel to ignored list.") - weechat.prnt("", "Usage: /triggerreply ignore server.#channel") - weechat.prnt("", "Example: /triggerreply ignore freenode.#mychan") - else: - database.commit() - weechat.prnt("", "Channel successfully added to ignore list!") + print_help() + return weechat.WEECHAT_RC_ERROR + + database.commit() + weechat.prnt("", "Channel successfully added to ignore list!") elif command[0] == "parse": + if len(argv) == len(command[0]): + print_help() + return weechat.WEECHAT_RC_ERROR + try: cursor.execute("DELETE FROM banchans WHERE ignored = ?", (command[1],)) except: - weechat.prnt("", "Could not remove channel from ignored.") - weechat.prnt("", "Usage: /triggerreply parse server.#channel") - weechat.prnt("", "Example: /triggerreply parse freenode.#mychan") - else: - database.commit() - weechat.prnt("", "Channel successfully removed from ignored.") + print_help() + return weechat.WEECHAT_RC_ERROR + + database.commit() + weechat.prnt("", "Channnel being watched again.") + + elif command[0] == "ignorenick": + if len(argv) == len(command[0]): + print_help() + return weechat.WEECHAT_RC_ERROR + + try: + cursor.execute("INSERT INTO ignorenicks(ignored) VALUES (?)", (command[1],)) + except: + print_help() + return weechat.WEECHAT_RC_ERROR + + database.commit() + weechat.prnt("", "Nick successfully added to ignore list!") + elif command[0] == "watchnick": + if len(argv) == len(command[0]): + print_help() + return weechat.WEECHAT_RC_ERROR + + try: + cursor.execute("DELETE FROM ignorenicks WHERE ignored = ?", (command[1],)) + except: + print_help() + return weechat.WEECHAT_RC_ERROR + + database.commit() + weechat.prnt("", "Nick successfully removed from ignored.") + return weechat.WEECHAT_RC_OK -if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, - "", ""): - if IMPORT_ERR: - weechat.prnt("", "You need sqlite3 to run this plugin.") - DBFILE = "%s/trigge.rs" % weechat.info_get("weechat_dir", "") - if not os.path.isfile(DBFILE): + +if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + options = { + 'directory': 'data', + } + + if weechat.config_get_plugin('database') == "": + weechat.config_set_plugin('database', "%h/trigge.rs") + db_file = weechat.string_eval_path_home("%h/trigge.rs", {}, {}, options) + else: + db_file = weechat.string_eval_path_home(weechat.config_get_plugin('database'), {}, {}, options) + + if weechat.config_get_plugin('debug') == "": + weechat.config_set_plugin('debug', "0") + + random.seed() + + if not os.path.isfile(db_file): create_db() + check_db() weechat.hook_print("", "", "", 1, "search_trig_cb", "") - weechat.hook_command(SCRIPT_NAME, SCRIPT_DESC, "See `/triggerreply' for more information.", "", - "", "command_input_callback", "") + weechat.hook_command(SCRIPT_NAME, SCRIPT_DESC, 'Triggerreply (trigge.rs) script. Automatically replies over specified triggers.\n' +'Usage: /triggerreply [list | add | remove | ignore | parse] ARGUMENTS\n\n' +'Commands:\n' +' list - lists the triggers with replies, and ignored channels\n' +' add - three arguments: "trigger", "reply" and probability\n' +' - adds a trigger with the specified reply and probability\n' +' - probability 1 = 1/1 (100%), 5 = 1/5 (20 %) - optional, default is 1 (100%)\n' +' - negative probability means action, see examples\n' +' - %n in the reply will be replaced by the nick of the matching line\n' +' - %N replaced by "my" nick\n' +' - %m replaced by host and mask *!*@\n' +' - %c replaced by channel name\n\n' +' remove - one argument: "trigger"\n' +' - remove a trigger\n' +' ignore - one argument: "server.#channel"\n' +' - ignores a particular channel from a server\n' +' parse - one argument: "server.#channel"\n' +' - removes a channel from ignored list\n' +'ignorenick - one argument: "server.#channel.Nick"\n' +' - ignores a particular nick from a server.#channel\n' +'watchnick - one argument: "server.#channel.Nick"\n' +' - removes a nick from ignored list\n\n' +'Examples:\n' +' /triggerreply add "^H(i|ello|ey)[ .!]*" "Hey there!|Hi matey|Aloha!" "1"\n' +' /triggerreply add "lol" "not funny tho" "5"\n' +' /triggerreply remove 2\n' +' /triggerreply ignore rizon.#help\n' +' /triggerreply parse rizon.#help\n' +' /triggerreply ignore rizon.#help.\n' +' /triggerreply ignorenick rizon.#help.Bot\n' +' /triggerreply watchnick rizon.#help.Bot\n\n' +'Auto greetings:\n' +'/triggerreply add "(hi|hello|hey|howdy)[,: ]+%N" "Hi, %n.|Hello, %n." "1"\n' +'/triggerreply add "%N[,: ]+(hi|hello|hey|howdy)" "Hi, %n.|Helllo, %n." "1"\n\n\n' +'Kick on adult content. Probability -1 means the strings between | are command executed in order:\n' +'/triggerreply add "https?://(www\.)?pornhub\.com|https?://(www\.)?xhamster\.com" "/msg chanserv op %c %N|/kick %n No adult content here, bye|/ban *!*@%m|/msg chanserv deop %c %N" "-1"', "", "list||add||remove||ignore||parse||ignorenick||watchnick", + "command_input_callback", "") + + """ fire every sec """ + hook = weechat.hook_timer(1000, 0, 0, "cooldown_timer_cb", "") diff --git a/python/tts.py b/python/tts.py index 2656c86a..f599d9dc 100644 --- a/python/tts.py +++ b/python/tts.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- # # Project: weechat-tts -# Homepage: https://git.hashtagueule.fr/raspbeguy/weechat-tts # Description: Text-to-speech script. # Requires a TTS engine such as espeak or picospeaker. # License: MIT (see below) +# Contact: raspbeguy @ chat.freenode.net # # Copyright (c) 2017 by raspbeguy # @@ -47,7 +47,7 @@ SCRIPT_AUTHOR = 'raspbeguy' # Version of the script. -SCRIPT_VERSION = '0.2.0' +SCRIPT_VERSION = '0.2.2' # License under which the script is distributed. SCRIPT_LICENSE = 'MIT' @@ -510,22 +510,27 @@ def tts(text): engine = weechat.config_get_plugin('tts_engine') lang = weechat.config_get_plugin('language') if engine == 'espeak': - command = 'espeak "%s" --stdout ' % text + args = {'arg1':text} if lang: - command += '-v %s ' % lang - command += '| paplay' + args['arg2'] = '-v' + args['arg3'] = lang + hook = weechat.hook_process_hashtable('espeak',args,0,'my_process_cb','') elif engine == 'festival': - command = 'echo "%s" | festival --tts ' % text + args = {'stdin':'1', 'arg1':'festival', 'arg2':'--tts'} if lang: - command += '--language %s' % lang + args['arg3'] = '--language' + args['arg4'] = lang + hook = weechat.hook_process_hashtable('festival',args,0,'my_process_cb','') + weechat.hook_set(hook, "stdin", text) + weechat.hook_set(hook, "stdin_close", "") elif engine == 'picospeaker': - command = 'echo "%s" | picospeaker ' % text + args = {'stdin':'1'} if lang: - command += '-l %s' % lang - hook = weechat.hook_process_hashtable("sh", - {"arg1": "-c", - "arg2": command}, - 0, "my_process_cb", "") + args['arg1'] = '-l' + args['arg2'] = lang + hook = weechat.hook_process_hashtable('picospeaker',args,0,'my_process_cb','') + weechat.hook_set(hook, "stdin", text) + weechat.hook_set(hook, "stdin_close", "") if __name__ == '__main__': # Registration. diff --git a/python/twitch.py b/python/twitch.py index ad43069c..c1100474 100644 --- a/python/twitch.py +++ b/python/twitch.py @@ -1,5 +1,6 @@ -# -*- coding: utf-8 -*- - +# SPDX-FileCopyrightText: 2014-2025 mumixam +# +# SPDX-License-Identifier: GPL-3.0-or-later # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or @@ -26,9 +27,42 @@ # settings: # plugins.var.python.twitch.servers (default: twitch) # plugins.var.python.twitch.prefix_nicks (default: 1) +# plugins.var.python.twitch.debug (default: 0) +# plugins.var.python.twitch.ssl_verify (default: 1) +# plugins.var.python.twitch.notice_notify_block (default: 1) +# plugins.var.python.twitch.client_id (default: awtv6n371jb7uayyc4jaljochyjbfxs) +# plugins.var.python.twitch.token (default: "") # # # History: +# 2025-09-23, +# v1.1: changed hook_modifier from "irc_in_WHISPER" to "irc_in2_WHISPER" -mumixam +# changed hook_process_hashtable("url:") to hook_url -mumixam +# use rawstrings to keep invalid escapes from throwing warnings -zer0def +# +# 2024-06-29, mumixam + stacyharper +# v1.0: eval client_id and token expressions so that /secure can be used # +# 2020-07-27, +# v0.9: added support for Oauth token to support twitch APIs requirement -mumixam +# fix bug for when api returns null for game_id -mas90 +# +# 2019-10-13, mumixam +# v0.8: changed input modifier hooks to use irc_in2_* instead +# added setting 'plugins.var.python.twitch.notice_notify_block' +# added setting 'plugins.var.python.twitch.client_id' +# +# 2019-09-21, mumixam +# v0.7: updated script to use current api +# 2019-03-03, +# v0.6: added support for CLEARMSG -MentalFS +# fixed issue with /whois -mumixam +# 2018-06-03, mumixam +# v0.5: enable curl verbose mode when debug is active, add option to disable ssl/tls verification, +# if stream title contains newline char replace it with space +# 2017-11-02, mumixam +# v0.4: added debug mode for API calls, minor bugfixes +# 2017-06-10, mumixam +# v0.3: fixed whois output of utf8 display names # 2016-11-03, mumixam # v0.2: added detailed /help # 2016-10-30, mumixam @@ -38,24 +72,42 @@ SCRIPT_NAME = "twitch" SCRIPT_AUTHOR = "mumixam" -SCRIPT_VERSION = "0.2" +SCRIPT_VERSION = "1.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "twitch.tv Chat Integration" -OPTIONS={ +OPTIONS={ 'servers': ('twitch','Name of server(s) which script will be active on, space seperated'), - 'prefix_nicks': ('1','Prefix nicks based on ircv3 tags for mods/subs, This can be cpu intensive on very active chats [1 for enabled, 0 for disabled]') + 'prefix_nicks': ('1','Prefix nicks based on ircv3 tags for mods/subs, This can be cpu intensive on very active chats [1 for enabled, 0 for disabled]'), + 'debug': ('0','Debug mode'), + 'ssl_verify': ('1', 'Verify SSL/TLS certs'), + 'notice_notify_block': ('1', 'Changes notify level of NOTICEs to low'), + 'client_id': ('awtv6n371jb7uayyc4jaljochyjbfxs', 'Twitch App ClientID'), + 'token': ('', 'Twitch User Token') } - import weechat import json from calendar import timegm from datetime import datetime, timedelta import time import string +import ast +import re + +curlopt = { + "httpheader": "\n".join([ + "Authorization: Bearer "+OPTIONS['token'][0], + "Client-ID: "+OPTIONS['client_id'][0], + ]), + "timeout": "5", + "verbose": "0", + "ssl_verifypeer": "1", + "ssl_verifyhost": "2" +} + -clientid='awtv6n371jb7uayyc4jaljochyjbfxs' -params = '?client_id='+clientid +gameid_cache = {} +uid_cache = {} def days_hours_minutes(td): age = '' @@ -78,103 +130,26 @@ def twitch_main(data, buffer, args): type = weechat.buffer_get_string(buffer, 'localvar_type') if not (server in OPTIONS['servers'].split() and type == 'channel'): return weechat.WEECHAT_RC_OK - url = 'https://api.twitch.tv/kraken/streams/' + username - url_hook_process = weechat.hook_process( - "url:" + url+params, 7 * 1000, "stream_api", buffer) + url = 'https://api.twitch.tv/helix/streams?user_login=' + username + weechat.hook_url(url, curlopt, 7 * 1000, "stream_api", buffer) return weechat.WEECHAT_RC_OK -gamelist = [ - "Counter-Strike: Global Offensive;CSGO", - "World of Warcraft: Warlords of Draenor;WOW", - "Hearthstone: Heroes of Warcraft;Hearthstone", - "H1Z1: King of the Kill;H1Z1 KotK", - "Tom Clancy\'s The Division;The Division" -] - +def makeutf8(data): + data = data.encode('utf8') + if not isinstance(data, str): + data=str(data,'utf8') + return data -def gameshort(game): - for games in gamelist: - gamelong = games.split(';')[0] - if gamelong.lower() == game.lower(): - return('<' + games.split(';')[-1] + '>') - return '<' + game + '>' - - -def channel_api(data, command, rc, stdout, stderr): - global name +def stream_api(data, url, options, output): try: - jsonDict = json.loads(stdout.strip()) + jsonDict = json.loads(output['output'].strip()) except Exception as e: - weechat.prnt(data, 'TWITCH: Error with twitch API') - return weechat.WEECHAT_RC_OK - currentbuf = weechat.current_buffer() - pcolor = weechat.color('chat_prefix_network') - ccolor = weechat.color('chat') - dcolor = weechat.color('chat_delimiters') - ncolor = weechat.color('chat_nick') - ul = weechat.color("underline") - rul = weechat.color("-underline") - pformat = weechat.config_string( - weechat.config_get("weechat.look.prefix_network")) - if len(jsonDict) == 22: - name = jsonDict['display_name'] - create = jsonDict['created_at'].split('T')[0] - status = jsonDict['status'] - follows = jsonDict['followers'] - partner = str(jsonDict['partner']) - output = '%s%s %s[%s%s%s]%s %sAccount Created%s: %s' % ( - pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, create) - if status: - output += '\n%s%s %s[%s%s%s]%s %sStatus%s: %s' % ( - pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, status) - output += '\n%s%s %s[%s%s%s]%s %sPartnered%s: %s %sFollowers%s: %s' % ( - pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, partner, ul, rul, follows) - output = output.encode('utf8') - if not isinstance(output, str): - output=str(output,'utf8') - weechat.prnt(data, output) - url = 'https://api.twitch.tv/kraken/users/' + \ - name.lower() + '/follows/channels' - urlh = weechat.hook_process( - "url:" + url+params, 7 * 1000, "channel_api", currentbuf) - - if len(jsonDict) == 18: - name = jsonDict['display_name'] - s64id = jsonDict['steam_id'] - if s64id: - sid3 = int(s64id) - 76561197960265728 - highaid = "{0:b}".format(sid3).zfill(32)[:31] - lowaid = "{0:b}".format(sid3).zfill(32)[31:] - id32bit = "STEAM_0:%s:%s" % (lowaid, int(highaid, 2)) - - output = '%s%s %s[%s%s%s]%s %ssteamID64%s: %s %ssteamID3%s: %s %ssteamID%s: %s' % ( - pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, s64id, ul, rul, sid3, ul, rul, id32bit) - weechat.prnt(data, output) - - if len(jsonDict) == 3: - if 'status' in jsonDict.keys(): - if jsonDict['status'] == 404 or jsonDict['status'] == 422: - user = jsonDict['message'].split()[1].replace("'", "") - weechat.prnt(data, '%s%s %s[%s%s%s]%s No such user' % ( - pcolor, pformat, dcolor, ncolor, user, dcolor, ccolor)) - else: - url = 'https://api.twitch.tv/api/channels/' + name.lower() - urlh = weechat.hook_process( - "url:" + url+params, 7 * 1000, "channel_api", currentbuf) - count = jsonDict['_total'] - if count: - output = '%s%s %s[%s%s%s]%s %sFollowing%s: %s' % ( - pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, count) - weechat.prnt(data, output) - return weechat.WEECHAT_RC_OK - - -def stream_api(data, command, rc, stdout, stderr): - try: - jsonDict = json.loads(stdout.strip()) - except Exception as e: - weechat.prnt(data, 'TWITCH: Error with twitch API') + weechat.prnt(data, '%stwitch.py: error communicating with twitch api' % weechat.prefix('error')) + if OPTIONS['debug']: + weechat.prnt(data,'%stwitch.py: response code: %s' % (weechat.prefix('error'),output['response_code'])) + weechat.prnt(data,'%stwitch.py: headers: %s' % (weechat.prefix('error'),output['headers'])) + weechat.prnt(data,'%stwitch.py: output: %s' % (weechat.prefix('error'),output['output'])) return weechat.WEECHAT_RC_OK currentbuf = weechat.current_buffer() title_fg = weechat.color( @@ -191,17 +166,13 @@ def stream_api(data, command, rc, stdout, stderr): r9k = weechat.buffer_get_string(data, 'localvar_r9k') slow = weechat.buffer_get_string(data, 'localvar_slow') emote = weechat.buffer_get_string(data, 'localvar_emote') - if 'status' in jsonDict.keys(): - if jsonDict['status'] == 422: - weechat.prnt(data, 'ERROR: The community has closed this channel due to terms of service violations.') - if jsonDict['status'] == 404: - weechat.prnt(data, 'ERROR: The page could not be found, or has been deleted by its owner.') - return weechat.WEECHAT_RC_OK - if not 'stream' in jsonDict.keys(): - weechat.prnt(data, 'TWITCH: Error with twitch API') + if not 'data' in jsonDict.keys(): + weechat.prnt(data, 'twitch.py: Error with twitch API (data key missing from json)') + if OPTIONS['debug']: + weechat.prnt(data, 'twitch.py: %s' % output['output'].strip()) return weechat.WEECHAT_RC_OK - if not jsonDict['stream']: - line = "STREAM: %sOFFLINE%s %sCHECKED AT: %s" % ( + if not jsonDict['data']: + line = "STREAM: %sOFFLINE%s %sCHECKED AT: (%s)" % ( red, title_fg, blue, ptime) if subs: line += " %s[SUBS]" % title_fg @@ -214,45 +185,43 @@ def stream_api(data, command, rc, stdout, stderr): weechat.buffer_set(data, "title", line) else: currenttime = time.time() + if len(jsonDict['data']) == 1: + jsonDict['data'] = jsonDict['data'][0] output = 'STREAM: %sLIVE%s' % (green, title_fg) - if 'game' in jsonDict['stream']: - if jsonDict['stream']['game']: - game = gameshort(jsonDict['stream']['game']).encode('utf8') - output += ' %s with' % game - if 'viewers' in jsonDict['stream']: - viewers = jsonDict['stream']['viewers'] + if 'game_id' in jsonDict['data']: + if jsonDict['data']['game_id']: + game = jsonDict['data']['game_id'] + game_id = game + if game in gameid_cache: + game = gameid_cache[game] + output += ' <%s> with' % game + else: + game_id = None + else: + game_id = None + if 'viewer_count' in jsonDict['data']: + viewers = jsonDict['data']['viewer_count'] output += ' %s viewers started' % viewers - if 'created_at' in jsonDict['stream']: - createtime = jsonDict['stream']['created_at'].replace('Z', 'GMT') + if 'started_at' in jsonDict['data']: + createtime = jsonDict['data']['started_at'].replace('Z', 'GMT') starttime = timegm( time.strptime(createtime, '%Y-%m-%dT%H:%M:%S%Z')) dur = timedelta(seconds=currenttime - starttime) uptime = days_hours_minutes(dur) output += ' %s ago' % uptime - if 'channel' in jsonDict['stream']: - if 'followers' in jsonDict['stream']['channel']: - followers = jsonDict['stream']['channel']['followers'] - output += ' [%s followers]' % followers - if 'status' in jsonDict['stream']['channel']: - titleutf8=jsonDict['stream']['channel']['status'].encode('utf8') - titleascii=jsonDict['stream']['channel']['status'].encode('ascii','replace') - if not isinstance(titleutf8, str): - titleascii=str(titleascii,'utf8') - titleutf8=str(titleutf8,'utf8') - oldtitle = weechat.buffer_get_string(data, 'localvar_tstatus') - if not oldtitle == titleascii: - weechat.prnt(data, '%s--%s Title is "%s"' % - (pcolor, ccolor, titleutf8)) - weechat.buffer_set(data, 'localvar_set_tstatus', titleascii) - if 'updated_at' in jsonDict['stream']['channel']: - updateat = jsonDict['stream']['channel'][ - 'updated_at'].replace('Z', 'GMT') - updatetime = timegm( - time.strptime(updateat, '%Y-%m-%dT%H:%M:%S%Z')) - udur = timedelta(seconds=currenttime - updatetime) - titleage = days_hours_minutes(udur) - - output += ' %s' % ptime + if 'title' in jsonDict['data']: + titleutf8=jsonDict['data']['title'].replace('\n',' ').encode('utf8') + titleascii=jsonDict['data']['title'].encode('ascii','replace') + if not isinstance(titleutf8, str): + titleascii=str(titleascii,'utf8') + titleutf8=str(titleutf8,'utf8') + oldtitle = weechat.buffer_get_string(data, 'localvar_tstatus') + if not oldtitle == titleascii: + weechat.prnt(data, '%s--%s Title is "%s"' % + (pcolor, ccolor, titleutf8)) + weechat.buffer_set(data, 'localvar_set_tstatus', titleascii) + + output += ' (%s)' % ptime if subs: output += " %s[SUBS]" % title_fg if r9k: @@ -262,6 +231,100 @@ def stream_api(data, command, rc, stdout, stderr): if emote: output += " %s[EMOTE]" % title_fg weechat.buffer_set(data, "title", output) + if game_id is not None and not game_id in gameid_cache: + url = 'https://api.twitch.tv/helix/games?id=' + game_id + weechat.hook_url(url, curlopt, 7 * 1000, "game_api", data) + + return weechat.WEECHAT_RC_OK + + +def game_api(data, url, options, output): + try: + jsonDict = json.loads(output['output'].strip()) + except Exception as e: + weechat.prnt(data, '%stwitch.py: error communicating with twitch api' % weechat.prefix('error')) + if OPTIONS['debug']: + weechat.prnt(data,'%stwitch.py: response code: %s' % (weechat.prefix('error'),output['response_code'])) + weechat.prnt(data,'%stwitch.py: headers: %s' % (weechat.prefix('error'),output['headers'])) + weechat.prnt(data,'%stwitch.py: output: %s' % (weechat.prefix('error'),output['output'])) + return weechat.WEECHAT_RC_OK + + if 'data' in jsonDict.keys(): + if not jsonDict['data']: + return weechat.WEECHAT_RC_OK + if len(jsonDict['data']) == 1: + jsonDict['data'] = jsonDict['data'][0] + old_title = weechat.buffer_get_string(data, "title") + id = jsonDict['data']['id'] + name = makeutf8(jsonDict['data']['name']) + new_title = old_title.replace('<{}>'.format(id),'<{}>'.format(name)) + weechat.buffer_set(data, "title", new_title) + gameid_cache[id] = name + return weechat.WEECHAT_RC_OK + + +def channel_api(data, url, options, output): + try: + jsonDict = json.loads(output['output'].strip()) + except Exception as e: + weechat.prnt(data, '%stwitch.py: error communicating with twitch api' % weechat.prefix('error')) + if OPTIONS['debug']: + weechat.prnt(data,'%stwitch.py: response code: %s' % (weechat.prefix('error'),output['response_code'])) + weechat.prnt(data,'%stwitch.py: headers: %s' % (weechat.prefix('error'),output['headers'])) + weechat.prnt(data,'%stwitch.py: output: %s' % (weechat.prefix('error'),output['output'])) + return weechat.WEECHAT_RC_OK + currentbuf = weechat.current_buffer() + pcolor = weechat.color('chat_prefix_network') + ccolor = weechat.color('chat') + dcolor = weechat.color('chat_delimiters') + ncolor = weechat.color('chat_nick') + ul = weechat.color("underline") + rul = weechat.color("-underline") + pformat = weechat.config_string( + weechat.config_get("weechat.look.prefix_network")) + + if 'total' in jsonDict: + uid = command.split('=')[-1] + name = 'WHOIS' + if 'to_id' in command: + followers = jsonDict['total'] + if uid in uid_cache: + name = uid_cache[uid] + output = '%s%s %s[%s%s%s]%s %sFollowers%s: %s' % ( + pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, followers) + weechat.prnt(data, makeutf8(output)) + url = 'https://api.twitch.tv/helix/users/follows?from_id=' + uid + url_hook = weechat.hook_url(url, curlopt, 7 * 1000, "channel_api", data) + return weechat.WEECHAT_RC_OK + if 'from_id' in command: + following = jsonDict['total'] + if uid in uid_cache: + name = uid_cache[uid] + output = '%s%s %s[%s%s%s]%s %sFollowing%s: %s' % ( + pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, following) + weechat.prnt(data, makeutf8(output)) + return weechat.WEECHAT_RC_OK + if ('users' in jsonDict) and jsonDict['users'] and len(jsonDict['users'][0]) == 8: + dname = jsonDict['users'][0]['display_name'] + name = jsonDict['users'][0]['name'] + create = jsonDict['users'][0]['created_at'].split('T')[0] + status = jsonDict['users'][0]['bio'] + uid = jsonDict['users'][0]['_id'] + uid_cache[uid] = name + output = '%s%s %s[%s%s%s]%s %sDisplay Name%s: %s' % ( + pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, dname) + output += '\n%s%s %s[%s%s%s]%s %sAccount Created%s: %s' % ( + pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, create) + if status: + output += '\n%s%s %s[%s%s%s]%s %sBio%s: %s' % ( + pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, status) + weechat.prnt(data, makeutf8(output)) + url = 'https://api.twitch.tv/helix/users/follows?to_id=' + uid + url_hook = weechat.hook_url(url, curlopt, 7 * 1000, "channel_api", data) + + else: + weechat.prnt(data, 'Error: No Such User') + return weechat.WEECHAT_RC_OK @@ -272,7 +335,7 @@ def twitch_clearchat(data, modifier, modifier_data, string): user = mp['text'] channel = mp['channel'] try: - tags = dict([s.split('=') for s in mp['tags'].split(';')]) + tags = dict([s.split('=',1) for s in mp['tags'].split(';')]) except: tags = '' buffer = weechat.buffer_search("irc", "%s.%s" % (server, channel)) @@ -283,8 +346,8 @@ def twitch_clearchat(data, modifier, modifier_data, string): rul = weechat.color("-underline") if user: if 'ban-duration' in tags: - if tags['ban-reason']: - bn=tags['ban-reason'].replace('\s',' ') + if 'ban-reason' in tags and tags['ban-reason']: + bn=re.sub(r'\s',' ', tags['ban-reason']) weechat.prnt(buffer,"%s--%s %s has been timed out for %s seconds %sReason%s: %s" % (pcolor, ccolor, user, tags['ban-duration'], ul, rul, bn)) else: @@ -292,7 +355,7 @@ def twitch_clearchat(data, modifier, modifier_data, string): (pcolor, ccolor, user, tags['ban-duration'])) elif 'ban-reason' in tags: if tags['ban-reason']: - bn=tags['ban-reason'].replace('\s',' ') + bn=re.sub(r'\s', ' ', tags['ban-reason']) weechat.prnt(buffer,"%s--%s %s has been banned %sReason%s: %s" % (pcolor, ccolor, user, ul, rul,bn)) else: @@ -307,6 +370,26 @@ def twitch_clearchat(data, modifier, modifier_data, string): return "" +def twitch_clearmsg(data, modifier, modifier_data, string): + mp = weechat.info_get_hashtable( + 'irc_message_parse', {"message": string}) + server = modifier_data + channel = mp['channel'] + try: + tags = dict([s.split('=',1) for s in mp['tags'].split(';')]) + except: + tags = '' + buffer = weechat.buffer_search("irc", "%s.%s" % (server, channel)) + if buffer: + pcolor = weechat.color('chat_prefix_network') + ccolor = weechat.color('chat') + if 'login' in tags: + weechat.prnt(buffer,"%s--%s a message from %s was deleted" % (pcolor, ccolor, tags['login'])) + else: + weechat.prnt(buffer, "%s--%s a message was deleted" % (pcolor, ccolor)) + return "" + + def twitch_suppress(data, modifier, modifier_data, string): return "" @@ -368,8 +451,8 @@ def twitch_usernotice(data, modifier, server, string): buffer = weechat.buffer_search( "irc", "%s.%s" % (server, mp['channel'])) if mp['tags']: - tags = dict([s.split('=') for s in mp['tags'].split(';')]) - msg = tags['system-msg'].replace('\s',' ') + tags = dict([s.split('=',1) for s in mp['tags'].split(';')]) + msg = re.sub(r'\\s', ' ', tags['system-msg']) if mp['text']: msg += ' [Comment] '+mp['text'] weechat.prnt(buffer, '%s--%s %s' % (pcolor, ccolor, msg)) @@ -394,7 +477,7 @@ def twitch_privmsg(data, modifier, server_name, string): 'irc_message_parse', {"message": string}) if message['channel'].startswith('#'): return string - newmsg = 'PRIVMSG jtv :.w ' + message['nick'] + ' ' + message['text'] + newmsg = 'PRIVMSG #%s :/w %s %s' % (message['nick'],message['nick'],message['text']) return newmsg @@ -407,10 +490,12 @@ def twitch_in_privmsg(data, modifier, server_name, string, prefix=''): if not mp['tags']: return string + if not '#' in mp['channel']: + return string if '#' + mp['nick'] == mp['channel']: return mp['message_without_tags'].replace(mp['nick'], '~' + mp['nick'], 1) - tags = dict([s.split('=') for s in mp['tags'].split(';')]) + tags = dict([s.split('=',1) for s in mp['tags'].split(';')]) if tags['user-type'] == 'mod': prefix += '@' if tags['subscriber'] == '1': @@ -427,30 +512,95 @@ def twitch_whois(data, modifier, server_name, string): if not server_name in OPTIONS['servers'].split(): return string msg = weechat.info_get_hashtable("irc_message_parse", {"message": string}) - username = msg['nick'] + username = msg['nick'].lower() currentbuf = weechat.current_buffer() - url = 'https://api.twitch.tv/kraken/channels/' + username - url_hook = weechat.hook_process( - "url:" + url+params, 7 * 1000, "channel_api", currentbuf) + url = 'https://api.twitch.tv/kraken/users?login=' + username + params='&api_version=5' + url_hook = weechat.hook_url(url+params, curlopt, 7 * 1000, "channel_api", currentbuf) return "" + +def twitch_notice(data, line): + if not OPTIONS['notice_notify_block']: return string + return {"notify_level": "0"} + + def config_setup(): for option,value in OPTIONS.items(): - weechat.config_set_desc_plugin(option, '%s' % value[1]) if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value[0]) + weechat.config_set_desc_plugin(option, '%s' % value[1]) OPTIONS[option] = value[0] else: - if option == 'prefix_nicks': + if option == 'prefix_nicks' or option == 'debug' or option == 'ssl_verify' or option == 'notice_notify_block': OPTIONS[option] = weechat.config_string_to_boolean( weechat.config_get_plugin(option)) else: OPTIONS[option] = weechat.config_get_plugin(option) + if option == 'debug': + curlopt['verbose'] = weechat.config_get_plugin(option) + if option == 'ssl_verify': + if weechat.config_get_plugin(option) == 0: + curlopt['ssl_verifypeer'] = "0" + curlopt['ssl_verifyhost'] = "0" + else: + curlopt['ssl_verifypeer'] = "1" + curlopt['ssl_verifyhost'] = "2" + if option == 'client_id': + hlist = [] + cidv = weechat.config_get_plugin(option) + tokv = weechat.config_get_plugin('token') + if tokv[:6] == "${sec.": + tokv = weechat.string_eval_expression(tokv, {}, {}, {}) + if cidv: + hlist.append('Client-ID: '+cidv) + if tokv: + hlist.append('Authorization: Bearer '+tokv) + if hlist: + curlopt['httpheader'] = '\n'.join(hlist) + if option == 'token': + hlist = [] + cidv = weechat.config_get_plugin('client_id') + tokv = weechat.config_get_plugin(option) + if tokv[:6] == "${sec.": + tokv = weechat.string_eval_expression(tokv, {}, {}, {}) + if tokv: + hlist.append('Authorization: Bearer '+tokv) + if cidv: + hlist.append('Client-ID: '+cidv) + if hlist: + curlopt['httpheader'] = '\n'.join(hlist) + def config_change(pointer, name, value): option = name.replace('plugins.var.python.'+SCRIPT_NAME+'.','') - if option == 'prefix_nicks': + if option == 'prefix_nicks' or option == 'debug' or option == 'ssl_verify' or option == 'notice_notify_block': value=weechat.config_string_to_boolean(value) + if option == 'debug': + if value == 0: + curlopt['verbose'] = "0" + if value == 1: + curlopt['verbose'] = "1" + if option == 'ssl_verify': + if value == 0: + curlopt['ssl_verifypeer'] = "0" + curlopt['ssl_verifyhost'] = "0" + if value == 1: + curlopt['ssl_verifypeer'] = "1" + curlopt['ssl_verifyhost'] = "2" + if option == 'client_id': + for x in curlopt['httpheader'].split('\n'): + if x.startswith('Authorization: Bearer'): + curlopt['httpheader'] = x + '\n' + "Client-ID: " + value + break + if option == 'token': + if value[:6] == "${sec.": + value = weechat.string_eval_expression(value, {}, {}, {}) + for x in curlopt['httpheader'].split('\n'): + if x.startswith('Client-ID:'): + curlopt['httpheader'] = x + '\n' + "Authorization: Bearer " + value + break + OPTIONS[option] = value return weechat.WEECHAT_RC_OK @@ -461,6 +611,10 @@ def config_change(pointer, name, value): " settings:\n" " plugins.var.python.twitch.servers (default: twitch)\n" " plugins.var.python.twitch.prefix_nicks (default: 1)\n" + " plugins.var.python.twitch.debug (default: 0)\n" + " plugins.var.python.twitch.ssl_verify (default: 0)\n" + " plugins.var.python.twitch.notice_notify_block (default: 1)\n" + " plugins.var.python.twitch.client_id (default: awtv6n371jb7uayyc4jaljochyjbfxs)\n" "\n\n" " This script checks stream status of any channel on any servers listed\n" " in the \"plugins.var.python.twitch.servers\" setting. When you switch\n" @@ -474,33 +628,54 @@ def config_change(pointer, name, value): "\n\n" " This script also will prefix users nicks (@ for mod, % for sub,\n" " and ~ for broadcaster). This will break the traditional function\n" - " of /ignore add nightbot and will require you to prefix nicks if you\n" - " want to ignore someone /ignore add re:[~@%]{0,3}nightbot should ignore\n" + " of `/ignore add nightbot` and will require you to prefix nicks if you\n" + " want to ignore someone `/ignore add re:[~@%]{0,3}nightbot` should ignore\n" " a nick with all or none of the prefixes used by this script.\n" " NOTE: This may cause high cpu usage in very active chat and/or on slower cpus.\n" " This can also be disabled by setting\n /set plugins.var.python.twitch.prefix_nicks off\n" "\n\n" + " If you are experiencing errors you can enable debug mode by setting\n" + " /set plugins.var.python.twitch.debug on\n" + " You can also try disabling SSL/TLS cert verification.\n" + " /set plugins.var.python.twitch.ssl_verify off\n" + "\n\n" " Required server settings:\n" - " /server add twitch irc.twitch.tv\n" + " /server add twitch irc.chat.twitch.tv\n" " /set irc.server.twitch.capabilities \"twitch.tv/membership,twitch.tv/commands,twitch.tv/tags\"\n" " /set irc.server.twitch.nicks \"My Twitch Username\"\n" " /set irc.server.twitch.password \"oauth:My Oauth Key\"\n" "\n" " If you do not have a oauth token one can be generated for your account here\n" - " https://twitchapps.com/tmi/\n" + " https://mumixam.github.io/weechat_twitch\n" + "\n" + " This script now by default limits the level of NOTICEs from twitch server\n" + " What this does is makes it so 'Now hosting' notifications are classes as a low level message\n" + " So they no longer show up in your hotlist like a 'actual' message\n" + " If you would like to disable this set the following\n" + " /set plugins.var.python.twitch.notice_notify_block 0\n" + "\n" + " If would like to use your own Client-ID it can be set with\n" + " /set plugins.var.python.twitch.client_id (clientid)\n" + "\n" + " Twitch Helix API now requires a OAuth token for any API calls. Your token has the match your ClientID\n" + " One can be generated here that matches the default CleintID here:\n" + " https://mumixam.github.io/weechat_twitch\n" + " /set plugins.var.python.twitch.token (token from url)\n" "\n" " This script also has whisper support that works like a standard query. \"/query user\"\n\n", "", "twitch_main", "") weechat.hook_signal('buffer_switch', 'twitch_buffer_switch', '') weechat.hook_config('plugins.var.python.' + SCRIPT_NAME + '.*', 'config_change', '') config_setup() - weechat.hook_modifier("irc_in_CLEARCHAT", "twitch_clearchat", "") - weechat.hook_modifier("irc_in_RECONNECT", "twitch_reconnect", "") - weechat.hook_modifier("irc_in_USERSTATE", "twitch_suppress", "") - weechat.hook_modifier("irc_in_HOSTTARGET", "twitch_suppress", "") - weechat.hook_modifier("irc_in_ROOMSTATE", "twitch_roomstate", "") - weechat.hook_modifier("irc_in_USERNOTICE", "twitch_usernotice", "") - weechat.hook_modifier("irc_in_WHISPER", "twitch_whisper", "") + weechat.hook_line("", "", "irc_notice+nick_tmi.twitch.tv", "twitch_notice", "") + weechat.hook_modifier("irc_in2_CLEARCHAT", "twitch_clearchat", "") + weechat.hook_modifier("irc_in2_CLEARMSG", "twitch_clearmsg", "") + weechat.hook_modifier("irc_in2_RECONNECT", "twitch_reconnect", "") + weechat.hook_modifier("irc_in2_USERSTATE", "twitch_suppress", "") + weechat.hook_modifier("irc_in2_HOSTTARGET", "twitch_suppress", "") + weechat.hook_modifier("irc_in2_ROOMSTATE", "twitch_roomstate", "") + weechat.hook_modifier("irc_in2_USERNOTICE", "twitch_usernotice", "") + weechat.hook_modifier("irc_in2_WHISPER", "twitch_whisper", "") weechat.hook_modifier("irc_out_PRIVMSG", "twitch_privmsg", "") weechat.hook_modifier("irc_out_WHOIS", "twitch_whois", "") - weechat.hook_modifier("irc_in_PRIVMSG", "twitch_in_privmsg", "") + weechat.hook_modifier("irc_in2_PRIVMSG", "twitch_in_privmsg", "") diff --git a/python/typing_counter.py b/python/typing_counter.py index a21a34c3..d6311f3d 100644 --- a/python/typing_counter.py +++ b/python/typing_counter.py @@ -1,4 +1,8 @@ -# Copyright (c) 2010-2013 by fauno +# +# SPDX-FileCopyrightText: 2010-2013 fauno +# SPDX-FileCopyrightText: 2018 Nils Görs +# +# SPDX-License-Identifier: GPL-3.0-or-later # # Bar item showing typing count. Add 'tc' to a bar. # @@ -40,12 +44,16 @@ # fix bug with root bar (reported by fours_) # 0.8 : # fix regex bug with ":" in sms text (reported by ahuemer) -# # 0.9 : # add option 'start_cursor_pos_at_zero' (idea by nesthib) -# -# Note: As of version 0.2 this script requires a version of weechat -# from git 2010-01-25 or newer, or at least 0.3.2 stable. +# 1.0 : +# make script python3 compatible +# add regular expression for format option +# 1.0.1: +# fix warning messages when loading script +# 1.0.2: +# drop Python 2 support, remove commented code, fix linter errors +# add SPDX copyright and license tags # # usage: # add [tc] to your weechat.bar.status.items @@ -69,26 +77,28 @@ # to activate a display beep use. # /set plugins.var.python.typing_counter.warn_command "$bell" # -## TODO: +# TODO: # - buffer whitelist/blacklist # - max chars per buffer (ie, bar item will turn red when count > 140 for identica buffer) SCRIPT_NAME = "typing_counter" SCRIPT_AUTHOR = "fauno " -SCRIPT_VERSION = "0.9" +SCRIPT_VERSION = "1.0.2" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Bar item showing typing count and cursor position. Add 'tc' to a bar." +import_ok = True + try: import weechat as w +except ImportError: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org/") + import_ok = False -except Exception: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" - quit() try: - import os, sys, re - + import os + import re except ImportError as message: print('Missing package(s) for %s: %s' % (SCRIPT_NAME, message)) import_ok = False @@ -99,7 +109,7 @@ count_over = '0' tc_default_options = { - 'format' : ('[%P|%L|<%R|%C>]','item name to add in a bar is "tc". item format is: %P = cursor position, %L = input length, %R = reverse counting from max_chars, %C = displays how many chars are count over max_chars'), + 'format' : ('[%P|%L|<%R|%C>]','item name to add in a bar is "tc". item format is: %P = cursor position, %L = input length, %R = reverse counting from max_chars, %C = displays how many chars are count over max_chars (content is evaluated, eg use colors with format "${color:xxx}", see /help eval)'), 'warn' : ('140','turns indicator to "warn_colour" when position is reached'), 'warn_colour' : ('red','color for warn after specified number of chars'), 'max_chars' : ('200','max number of chars to count reverse'), @@ -110,7 +120,15 @@ } tc_options = {} +# regexp to match ${color} tags +regex_color=re.compile(r'\$\{([^\{\}]+)\}') + +# regexp to match ${optional string} tags +regex_optional_tags=re.compile(r'%\{[^\{\}]+\}') + + def command_run_cb (data, signal, signal_data): + """Callback for /input xxx commands.""" if tc_options['warn_command'] == '': return w.WEECHAT_RC_OK global length, cursor_pos, tc_input_text @@ -121,16 +139,20 @@ def command_run_cb (data, signal, signal_data): tc_action_cb() return w.WEECHAT_RC_OK + def tc_bar_item_update (data=None, signal=None, signal_data=None): - '''Updates bar item''' - '''May be used as a callback or standalone call.''' + """Update bar item. + + May be used as a callback or standalone call. + """ global length, cursor_pos, tc_input_text w.bar_item_update('tc') return w.WEECHAT_RC_OK + def tc_bar_item (data, item, window): - '''Item constructor''' + """Item constructor.""" # window empty? root bar! if not window: window = w.current_window() @@ -159,7 +181,6 @@ def tc_bar_item (data, item, window): name = w.buffer_get_string(ptr_buffer, 'localvar_name') input_line = w.buffer_get_string(ptr_buffer, 'input') mynick = w.info_get('irc_nick', servername) - nick_ptr = w.nicklist_search_nick(ptr_buffer, '', mynick) # check for a sms message if channel_type == 'private' and name in tc_options['sms_buffer'].split(","): @@ -167,10 +188,7 @@ def tc_bar_item (data, item, window): # 'sms:name:text' get_sms_text = re.match(r'(s|sms):(.*?:)(.*)', input_line) if get_sms_text: -# if get_sms_text.group(2): sms_len = len(get_sms_text.group(3)) -# input_length = len(input_line) -# sms_prefix = input_length - sms_len sms = 160-sms_len reverse_chars = sms else: @@ -192,14 +210,12 @@ def tc_bar_item (data, item, window): # get host and length from host elif servername != channelname: infolist = w.infolist_get('irc_nick', '', '%s,%s,%s' % (servername,channelname,mynick)) -# w.prnt("","%s.%s.%s.%s" % (servername,channelname,mynick,nick_ptr)) while w.infolist_next(infolist): host = w.infolist_string(infolist, 'host') w.infolist_free(infolist) if host != '': host = ':%s!%s PRIVMSG %s :' % (mynick,host,channelname) host_length = len(host) -# w.prnt("","%d" % host_length) reverse_chars = (475 - int(host_length) - length -1) # -1 = return else: reverse_chars = (int(tc_options['max_chars']) - length) @@ -211,13 +227,12 @@ def tc_bar_item (data, item, window): if reverse_chars == 0: reverse_chars = "%s" % ("0") + elif reverse_chars < 0: + count_over = "%s%s%s" % (w.color(tc_options['warn_colour']),str(reverse_chars*-1), w.color('default')) + reverse_chars = "%s" % ("0") + tc_action_cb() else: - if reverse_chars < 0: - count_over = "%s%s%s" % (w.color(tc_options['warn_colour']),str(reverse_chars*-1), w.color('default')) - reverse_chars = "%s" % ("0") - tc_action_cb() - else: - reverse_chars = str(reverse_chars) + reverse_chars = str(reverse_chars) out_format = tc_options['format'] if tc_options['warn']: @@ -237,11 +252,17 @@ def tc_bar_item (data, item, window): else: out_format = out_format.replace('%R', reverse_chars) out_format = out_format.replace('%C', count_over) -# out_format = out_format.replace('%T', str(tweet)) -# out_format = out_format.replace('%S', str(sms)) tc_input_text = out_format - return tc_input_text + return substitute_colors(tc_input_text) + + +def substitute_colors(text): + if int(version) >= 0x00040200: + return w.string_eval_expression(text,{},{},{}) + # substitute colors in output + return re.sub(regex_color, lambda match: w.color(match.group(1)), text) + def init_config(): global tc_default_options, tc_options @@ -254,32 +275,34 @@ def init_config(): else: tc_options[option] = w.config_get_plugin(option) + def config_changed(data, option, value): init_config() return w.WEECHAT_RC_OK + def tc_action_cb(): global tc_options if tc_options['warn_command']: if tc_options['warn_command'] == '$bell': - f = open('/dev/tty', 'w') - f.write('\a') - f.close() + with open('/dev/tty', 'w') as f: + f.write('\a') else: os.system(tc_options['warn_command']) return w.WEECHAT_RC_OK -if __name__ == "__main__": - if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, - SCRIPT_LICENSE, SCRIPT_DESC, - "", ""): - init_config() # read configuration - tc_bar_item_update() # update status bar display - - w.hook_signal('input_text_changed', 'tc_bar_item_update', '') - w.hook_signal('input_text_cursor_moved','tc_bar_item_update','') - w.hook_command_run('/input move_previous_char','command_run_cb','') - w.hook_command_run('/input delete_previous_char','command_run_cb','') - w.hook_signal('buffer_switch','tc_bar_item_update','') - w.hook_config('plugins.var.python.' + SCRIPT_NAME + ".*", "config_changed", "") - w.bar_item_new('tc', 'tc_bar_item', '') + +if (__name__ == "__main__" + and import_ok + and w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "")): + version = w.info_get("version_number", "") or 0 + init_config() # read configuration + tc_bar_item_update() # update status bar display + + w.hook_signal('input_text_changed', 'tc_bar_item_update', '') + w.hook_signal('input_text_cursor_moved','tc_bar_item_update','') + w.hook_command_run('/input move_previous_char','command_run_cb','') + w.hook_command_run('/input delete_previous_char','command_run_cb','') + w.hook_signal('buffer_switch','tc_bar_item_update','') + w.hook_config('plugins.var.python.' + SCRIPT_NAME + ".*", "config_changed", "") + w.bar_item_new('tc', 'tc_bar_item', '') diff --git a/python/uname.py b/python/uname.py deleted file mode 100644 index b5245463..00000000 --- a/python/uname.py +++ /dev/null @@ -1,40 +0,0 @@ -# This script sends "uname -a" output to current channel. -# Just type /uname while chatting on some channel ;) -# -# by Stalwart -# -# port to WeeChat 0.3.0 by Benjamin Neff (SuperTux88) -# -# Released under GPL licence. - - -SCRIPT_NAME = "uname" -SCRIPT_AUTHOR = "Stalwart " -SCRIPT_VERSION = "1.1" -SCRIPT_LICENSE = "GPL2" -SCRIPT_DESC = "Sends \"uname -a\" output to current channel" - -import_ok = True - -try: - import weechat -except ImportError: - print "This script must be run under WeeChat." - print "Get WeeChat now at: http://www.weechat.org/" - import_ok = False - -try: - from os import popen -except ImportError, message: - print "Missing package(s) for %s: %s" % (SCRIPT_NAME, message) - import_ok = False - -def senduname(data, buffer, args): - unameout = popen ('uname -a') - uname = unameout.readline() - weechat.command(buffer, "uname -a: " + uname[:-1]) - return weechat.WEECHAT_RC_OK - -if __name__ == "__main__" and import_ok: - if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): - weechat.hook_command (SCRIPT_NAME, SCRIPT_DESC, '','Just type /uname while chatting on some channel ;)','', 'senduname', '') diff --git a/python/undernet_totp.py b/python/undernet_totp.py index 3fc99e48..b5759e75 100644 --- a/python/undernet_totp.py +++ b/python/undernet_totp.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2013 - 2015 Stefan Wold +# Copyright (C) 2013 - 2019 Stefan Wold # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,22 +25,21 @@ # This allows OTP login when using irc.server.*.command to automatically # sign in to the X service when connecting to an undernet server. # -# -# Configuration: -# /set plugins.var.python.undernet-totp.otp_server_names ",,..." -# Set servers for which to automatically enable OTP login -# # Commands: -# /uotp [server] -# Generate an OTP for server, output in core buffer. -# +# /uotp otp [server] +# /uotp list +# /uotp add +# /uotp remove +# /uotp enable +# /uotp disable +from __future__ import print_function SCRIPT_NAME = "undernet_totp" SCRIPT_AUTHOR = "Stefan Wold " -SCRIPT_VERSION = "0.3.1" +SCRIPT_VERSION = "0.4.2" SCRIPT_LICENSE = "GPL3" -SCRIPT_DESC = "Enables automatic OTP (OATH-TOTP) support for UnderNET's X and Login on Connect (LoC) authentication." +SCRIPT_DESC = "Automatic OTP (OATH-TOTP) authentication with UnderNET's channel services (X) and Login on Connect (LoC)." SCRIPT_COMMAND = "uotp" HOOKS = {} @@ -55,10 +54,11 @@ try: import weechat except ImportError: - print "This script must be run under WeeChat." + print("This script must be run under WeeChat.") import_ok = False try: + import sys import hmac import re from base64 import b32decode @@ -67,7 +67,7 @@ from time import time from binascii import unhexlify except ImportError as err: - print "Missing module(s) for %s: %s" % (SCRIPT_NAME, err) + print("Missing module(s) for %s: %s" % (SCRIPT_NAME, err)) import_ok = False @@ -75,6 +75,9 @@ def print_debug(message): if weechat.config_get_plugin('debug') == 'on': weechat.prnt("", "%s DEBUG: %s" % (SCRIPT_NAME, message)) +def sprint(message, buffer=""): + weechat.prnt(buffer, "%s: %s" % (SCRIPT_NAME, message)) + def unhook(hook): global HOOKS @@ -91,6 +94,7 @@ def unhook_all(server): def hook_all(server): + print_debug("hook_all(%s)" % server) global HOOKS notice = server + '.notice' @@ -140,7 +144,6 @@ def get_otp_cb(data, buffer, server): else: server = enabled_servers() - for _server in server: otp = generate_totp(_server) if otp is not None: @@ -149,23 +152,43 @@ def get_otp_cb(data, buffer, server): return weechat.WEECHAT_RC_OK +def get_irc_servers(): + """ Returns a list of configured IRC servers in weechat""" + serverptrlist = weechat.infolist_get('irc_server', '', '') + serverlist = [] + while weechat.infolist_next(serverptrlist): + serverlist.append(weechat.infolist_string(serverptrlist, 'name')) + weechat.infolist_free(serverptrlist) + return serverlist + + def enabled_servers(): - def server_exists(server): - print_debug('enabled_servers(%s)' % server) - if weechat.config_get('irc.server.%s.addresses' % server) is not '': - return True - return False + """ Return a list of TOTP enabled servers. """ + serverlist = get_irc_servers() + return [s for s in get_config_as_list('otp_server_names') if s in serverlist] + + +def disabled_servers(): + """ Return a list of configured TOTP servers that are currently disabled. """ + serverlist = get_irc_servers() + server_seed_list = [server for server in serverlist + if weechat.string_eval_expression("${sec.data.%s_seed}" % server, {}, {}, {}) + and server not in get_config_as_list('otp_server_names')] + return [s for s in server_seed_list if s in serverlist] - servers = weechat.config_get_plugin('otp_server_names') - return [s.strip() for s in servers.split(',') if server_exists(s.strip())] +def configured_servers(): + """ Return a lost of servers with an existing seed. """ + serverlist = get_irc_servers() + return [s for s in serverlist if weechat.string_eval_expression("${sec.data.%s_seed}" % s, {}, {}, {})] -def generate_totp(server, period=30): + +def generate_totp(server, period=30, buffer=""): print_debug('generate_totp(%s)' % server) seed = weechat.string_eval_expression("${sec.data.%s_seed}" % server, {}, {}, {}) - if seed is "": - weechat.prnt("", "No OATH-TOTP secret set, use: /secure set %s_seed " % server) + if not seed: + sprint("No OATH-TOTP secret set, use: /uotp add %s " % server, buffer) return None if len(seed) == 40: # Assume hex format @@ -173,14 +196,142 @@ def generate_totp(server, period=30): else: seed = b32decode(seed.replace(" ", ""), True) + def _ord(b): + if sys.version_info[0] < 3 or type(b) == str: + return ord(b) + return b + t = pack(">Q", int(time() / period)) _hmac = hmac.new(seed, t, sha1).digest() - o = ord(_hmac[19]) & 15 + o = _ord(_hmac[19]) & 15 otp = (unpack(">I", _hmac[o:o+4])[0] & 0x7fffffff) % 1000000 return '%06d' % otp +def config_update_cb(data, option, value): + """ Reload hooks on configuration change. """ + print_debug("config_cb(%s)" % value) + [hook_all(s.strip()) for s in value.split(',')] + return weechat.WEECHAT_RC_OK + + +def options_cb(data, buffer, args): + """ Script configuration callback """ + if not args: + weechat.command("", "/help %s" % SCRIPT_COMMAND) + args = args.strip().split(' ') + opt = args[0] + opt_args = args[1:] + + if opt == 'otp': + if opt_args: + servers = [opt_args[0]] + else: + servers = enabled_servers() + for server in servers: + otp = generate_totp(server, buffer=buffer) + if otp: + sprint("%s = %s" % (server, otp), buffer) + elif opt == 'list': + sprint("List of configured servers", buffer) + for server in enabled_servers(): + weechat.prnt(buffer, " - %s [enabled]" % server) + for server in disabled_servers(): + weechat.prnt(buffer, " - %s [disabled]" % server) + elif opt == 'add': + if len(opt_args) >= 2: + if opt_args[0] not in enabled_servers() and opt_args[0] in get_irc_servers(): + #weechat.command("", "/secure set %s_seed %s" % (opt_args[0], opt_args[1])) + try: + add_server(opt_args[0], opt_args[1:]) + sprint("server '%s' was successfully added" % opt_args[0], buffer) + except Exception as ex: + sprint("invalid TOTP seed provided", buffer) + elif opt_args[0] not in get_irc_servers(): + sprint("No server named '%s' was found, see /help server" % opt_args[0], buffer) + else: + sprint("OTP already configured for '%s', to change remove the existing one first." % opt_args[0], buffer) + else: + sprint("/uotp -- invalid argument, valid command is /uotp add ", buffer) + elif opt == 'remove': + if opt_args[0] in enabled_servers() or opt_args[0] in disabled_servers(): + remove_server(opt_args[0], True) + sprint("server '%s' was successfully removed" % opt_args[0], buffer) + else: + sprint("failed to remove server, '%s' not found" % opt_args[0], buffer) + elif opt == 'enable': + if opt_args and opt_args[0] not in enabled_servers(): + if opt_args[0] in get_irc_servers(): + add_server(opt_args[0]) + sprint("server '%s' was successfully enabled" % opt_args[0], buffer) + else: + sprint("No server named '%s' was found, see /help server" % opt_args[0], buffer) + else: + sprint("OTP is already enabled for the server '%s'." % opt_args[0], buffer) + elif opt == 'disable': + if opt_args and opt_args[0] in enabled_servers(): + remove_server(opt_args[0]) + else: + sprint("OTP does not seem to be enabled for '%s'" % opt_args[0], buffer) + elif opt: + sprint("/uotp: invalid option -- '%s'" % opt, buffer) + weechat.command("", "/help %s" % SCRIPT_COMMAND) + + return weechat.WEECHAT_RC_OK + + +def get_config_as_list(option): + """ Return comma-separated