mirror of
https://github.com/jlu5/PyLink.git
synced 2025-04-21 07:17:54 +02:00
Compare commits
No commits in common. "master" and "2.0.0" have entirely different histories.
@ -1,25 +0,0 @@
|
||||
*.yml
|
||||
*.yaml
|
||||
# Git, CI, etc. config files
|
||||
.*
|
||||
test/
|
||||
|
||||
# Automatically generated by setup.py
|
||||
/__init__.py
|
||||
|
||||
env/
|
||||
build/
|
||||
__pycache__/
|
||||
.idea/
|
||||
*.py[cod]
|
||||
*.bak
|
||||
*~
|
||||
*#
|
||||
*.save*
|
||||
*.db
|
||||
*.pid
|
||||
*.pem
|
||||
.eggs
|
||||
*.egg-info/
|
||||
dist/
|
||||
log/
|
@ -1,30 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Write Docker tags for Drone CI: version-YYMMDD, version, major version, latest
|
||||
|
||||
VERSION="$1"
|
||||
|
||||
if test -z "$VERSION"; then
|
||||
echo "Reading version from VERSION file" >&2
|
||||
VERSION=$(<VERSION)
|
||||
fi
|
||||
|
||||
if [[ "$VERSION" == *"alpha"* || "$VERSION" == *"dev"* ]]; then
|
||||
# This should never trigger if reference based tagging is enabled
|
||||
echo "ERROR: Pushing alpha / dev tags is not supported"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
major_version="$(printf '%s' "$VERSION" | cut -d . -f 1)"
|
||||
|
||||
# Date based tag
|
||||
printf '%s' "$VERSION-$(date +%Y%m%d),"
|
||||
# Program version
|
||||
printf '%s' "$VERSION,"
|
||||
|
||||
if [[ "$VERSION" == *"beta"* ]]; then
|
||||
printf '%s' "$major_version-beta,"
|
||||
printf '%s' "latest-beta"
|
||||
else # Stable or rc build
|
||||
printf '%s' "$major_version,"
|
||||
printf '%s' "latest"
|
||||
fi
|
@ -1,94 +0,0 @@
|
||||
local test(py_version) = {
|
||||
"kind": "pipeline",
|
||||
"type": "docker",
|
||||
"name": "test-" + py_version,
|
||||
"steps": [
|
||||
{
|
||||
"name": "test",
|
||||
"image": "python:" + py_version,
|
||||
"commands": [
|
||||
"git submodule update --recursive --remote --init",
|
||||
"pip install -r requirements-docker.txt",
|
||||
"python3 setup.py install",
|
||||
"python3 -m unittest discover test/ --verbose"
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
local build_docker(py_version) = {
|
||||
"kind": "pipeline",
|
||||
"type": "docker",
|
||||
"name": "build_docker",
|
||||
"steps": [
|
||||
{
|
||||
"name": "set Docker image tags",
|
||||
"image": "bash",
|
||||
"commands": [
|
||||
"bash .drone-write-tags.sh $DRONE_TAG > .tags",
|
||||
"# Will build the following tags:",
|
||||
"cat .tags"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "build Docker image",
|
||||
"image": "plugins/docker",
|
||||
"settings": {
|
||||
"repo": "jlu5/pylink",
|
||||
"username": {
|
||||
"from_secret": "docker_user"
|
||||
},
|
||||
"password": {
|
||||
"from_secret": "docker_token"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"trigger": {
|
||||
"event": [
|
||||
"push"
|
||||
],
|
||||
"branch": ["release"],
|
||||
},
|
||||
"depends_on": ["test-" + py_version]
|
||||
};
|
||||
|
||||
local deploy_pypi(py_version) = {
|
||||
"kind": "pipeline",
|
||||
"type": "docker",
|
||||
"name": "deploy_pypi",
|
||||
"steps": [
|
||||
{
|
||||
"name": "pypi_publish",
|
||||
"image": "plugins/pypi",
|
||||
"settings": {
|
||||
"username": "__token__",
|
||||
"password": {
|
||||
"from_secret": "pypi_token"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"trigger": {
|
||||
"event": [
|
||||
"tag"
|
||||
],
|
||||
"ref": {
|
||||
"exclude": [
|
||||
"refs/tags/*alpha*",
|
||||
"refs/tags/*beta*",
|
||||
"refs/tags/*dev*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"depends_on": ["test-" + py_version]
|
||||
};
|
||||
|
||||
[
|
||||
test("3.7"),
|
||||
test("3.8"),
|
||||
test("3.9"),
|
||||
test("3.10"),
|
||||
deploy_pypi("3.10"),
|
||||
build_docker("3.10"),
|
||||
]
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,9 +3,6 @@
|
||||
!example-*.yml
|
||||
!.*.yml
|
||||
|
||||
# Generated from .drone.jsonnet
|
||||
.drone.yml
|
||||
|
||||
# Automatically generated by setup.py
|
||||
/__init__.py
|
||||
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "test/parser_tests"]
|
||||
path = test/parser-tests
|
||||
url = https://github.com/ircdocs/parser-tests
|
@ -1,2 +0,0 @@
|
||||
[settings]
|
||||
line_length=100
|
31
.travis.yml
Normal file
31
.travis.yml
Normal file
@ -0,0 +1,31 @@
|
||||
dist: xenial
|
||||
sudo: false
|
||||
|
||||
language: python
|
||||
python:
|
||||
- '3.4'
|
||||
- '3.5'
|
||||
- '3.6'
|
||||
|
||||
install: python3 setup.py install
|
||||
script:
|
||||
- python3 -m compileall .
|
||||
- cd test/ && python3 -m unittest *.py
|
||||
|
||||
deploy:
|
||||
provider: pypi
|
||||
# Enable this to use test mode
|
||||
# server: https://testpypi.python.org/pypi
|
||||
user:
|
||||
secure: Ql6ihu5MDgWuAvT9NYfriGUYGhHpsqwXfZHWDQT+DfRjOqHo9QT7PnfexeBoe6L6cYUkEnIrnAXKtBXGy6UmyvfrnvBl68877dLVuoC8PfQ4J0ej7TVnCJmT/LwRqFvzZXkeg4CIlJsVJ6pvrPHXQBDPH1rj/rWCucchrofmps8=
|
||||
password:
|
||||
secure: JOHSaZDPCImV/TlQ7hqKLzEvxY4/gpYGlZlOvxgFEd/k/sGk13sva1MfQkOh7Fgjblhk/CHt59wVKXa0VaylRugFQnXb+NYNrxYON0IRVsKON20XaLXg7qsyKCS4ml+7cd2KvM8a6LVO9078yLWAhTZkZ69nLIRZwFbmL5+mep4=
|
||||
|
||||
on:
|
||||
tags: true
|
||||
# Only deploy on tags that don't have -alpha, -beta, etc. suffixes attached
|
||||
condition: $(python3 -c 'import re,os; print(bool(re.match(r"^(\d+\.){2,}\d+$", os.environ.get("TRAVIS_TAG", ""))))') == "True"
|
||||
python: '3.6'
|
||||
|
||||
notifications:
|
||||
email: false
|
17
Dockerfile
17
Dockerfile
@ -1,17 +0,0 @@
|
||||
FROM python:3-alpine
|
||||
|
||||
RUN adduser -D -H -u 10000 pylink
|
||||
|
||||
VOLUME /pylink
|
||||
|
||||
COPY . /pylink-src
|
||||
|
||||
RUN cd /pylink-src && pip3 install --no-cache-dir -r requirements-docker.txt
|
||||
RUN cd /pylink-src && python3 setup.py install
|
||||
RUN rm -r /pylink-src
|
||||
|
||||
USER pylink
|
||||
WORKDIR /pylink
|
||||
|
||||
# Run in no-PID file mode by default
|
||||
CMD ["pylink", "-n"]
|
105
README.md
105
README.md
@ -1,34 +1,37 @@
|
||||
# PyLink IRC Services
|
||||
|
||||
## END OF LIFE NOTICE: This project is no longer maintained. So long and thanks for all the fish.
|
||||
[webchatlink]: https://webchat.overdrivenetworks.com/?channels=PyLink
|
||||
|
||||
<!--
|
||||
[](https://github.com/PyLink/PyLink/tree/master)
|
||||
[](https://pypi.python.org/pypi/pylinkirc/)
|
||||
[](https://hub.docker.com/r/jlu5/pylink)
|
||||
[](https://www.python.org/downloads/)
|
||||
-->
|
||||
[](https://www.python.org/downloads/)
|
||||
[](LICENSE.MPL2)
|
||||
[][webchatlink]
|
||||
|
||||
PyLink is an extensible, plugin-based IRC services framework written in Python. It aims to be:
|
||||
|
||||
1) a transparent server-side relayer between IRC networks.
|
||||
1) a replacement for the now-defunct Janus.
|
||||
|
||||
2) a versatile framework for developing IRC services.
|
||||
|
||||
PyLink is licensed under the Mozilla Public License, version 2.0 ([LICENSE.MPL2](LICENSE.MPL2)). The [corresponding documentation](docs/) is licensed under the Creative Attribution-ShareAlike 4.0 International License. ([LICENSE.CC-BY-SA-4.0](LICENSE.CC-BY-SA-4.0))
|
||||
PyLink and any bundled software are licensed under the Mozilla Public License, version 2.0 ([LICENSE.MPL2](LICENSE.MPL2)). The corresponding documentation in the [docs/](docs/) folder is licensed under the Creative Attribution-ShareAlike 4.0 International License. ([LICENSE.CC-BY-SA-4.0](LICENSE.CC-BY-SA-4.0))
|
||||
|
||||
## Getting help
|
||||
## Support the project
|
||||
[](https://www.paypal.me/jlucode)
|
||||
|
||||
## Obtaining support
|
||||
|
||||
**First, MAKE SURE you've read the [FAQ](docs/faq.md)!**
|
||||
|
||||
**When upgrading between major versions, remember to read the [release notes](RELNOTES.md) for any breaking changes!**
|
||||
|
||||
Please report any bugs you find to the [issue tracker](https://github.com/PyLink/PyLink/issues). Pull requests are likewise welcome.
|
||||
Please report any bugs you find to the [issue tracker](https://github.com/jlu5/PyLink/issues). Pull requests are open if you'd like to contribute, though new stuff generally goes to the **devel** branch.
|
||||
|
||||
You can also find support via our IRC channel at `#PyLink @ irc.overdrivenetworks.com `([webchat][webchatlink]). Ask your questions and be patient for a response.
|
||||
|
||||
## Installation
|
||||
|
||||
### Pre-requisites
|
||||
* Python 3.7 or above - prefer the newest Python 3.x when available
|
||||
* CPython 3.4 or above (other intepreters are untested and unsupported)
|
||||
* A Unix-like operating system: PyLink is actively developed on Linux only, so we cannot guarantee that things will work properly on other systems.
|
||||
|
||||
If you are a developer and want to help make PyLink more portable, patches are welcome.
|
||||
@ -39,45 +42,31 @@ If you are a developer and want to help make PyLink more portable, patches are w
|
||||
|
||||
* Setuptools (`pip3 install setuptools`)
|
||||
* PyYAML (`pip3 install pyyaml`)
|
||||
* cachetools (`pip3 install cachetools`)
|
||||
* *For hashed password support*: Passlib >= 1.7.0 (`pip3 install passlib`)
|
||||
* *For Unicode support in Relay*: unidecode (`pip3 install Unidecode`)
|
||||
* *For extended PID file tracking (i.e. removing stale PID files after a crash)*: psutil (`pip3 install psutil`)
|
||||
* ircmatch (`pip3 install ircmatch`)
|
||||
* *For password encryption*: Passlib (`pip3 install passlib`)
|
||||
* *For better PID file tracking (i.e. removing stale PID files in the case of a crash)*: psutil (`pip3 install psutil`)
|
||||
* *For the servprotect plugin*: expiringdict (`pip3 install expiringdict`)
|
||||
|
||||
2) Clone the repository: `git clone https://github.com/PyLink/PyLink && cd PyLink`
|
||||
- Previously there was a *devel* branch for testing versions of PyLink - this practice has since been discontinued.
|
||||
2) Clone the repository: `git clone https://github.com/jlu5/PyLink && cd PyLink`
|
||||
|
||||
3) Install PyLink using `python3 setup.py install` (global install) or `python3 setup.py install --user` (local install)
|
||||
3) Pick your branch.
|
||||
* By default you'll be on the **master** branch, which contains the latest stable code. This branch is recommended for production networks that don't require new features or intensive bug fixes as they are developed.
|
||||
* The **devel** branch is where active development goes, and it can be accessed by running `git checkout devel` in your Git tree.
|
||||
|
||||
4) Install PyLink using `python3 setup.py install` (global install) or `python3 setup.py install --user` (local install)
|
||||
* Note: `--user` is a *literal* string; *do not* replace it with your username.
|
||||
* **Whenever you switch branches or update PyLink's sources via `git pull`, you will need to re-run this command for changes to apply!**
|
||||
|
||||
### Installing via Docker
|
||||
|
||||
As of PyLink 3.0 there is a Docker image available on Docker Hub: [jlu5/pylink](https://hub.docker.com/r/jlu5/pylink)
|
||||
|
||||
It supports the following tags:
|
||||
|
||||
- Rolling tags: **`latest`** (latest stable/RC release), **`latest-beta`** (latest beta snapshot)
|
||||
- Pinned to a major branch: e.g. **`3`** (latest 3.x stable release), **`3-beta`** (latest 3.x beta snapshot)
|
||||
- Pinned to a specific version: e.g. **`3.0.0`**
|
||||
|
||||
To use this image you should mount your configuration/DB folder into `/pylink`. **Make sure this directory is writable by UID 10000.**
|
||||
|
||||
```bash
|
||||
$ docker run -v $HOME/pylink:/pylink jlu5/pylink
|
||||
```
|
||||
|
||||
### Installing via PyPI (stable branch only)
|
||||
|
||||
1) Make sure you're running the right pip command: on most distros, pip for Python 3 uses the command `pip3`.
|
||||
|
||||
2) Run `pip3 install pylinkirc` to download and install PyLink. pip will automatically resolve dependencies.
|
||||
|
||||
3) Download or copy https://github.com/PyLink/PyLink/blob/master/example-conf.yml for an example configuration.
|
||||
3) Download or copy https://github.com/jlu5/PyLink/blob/master/example-conf.yml for an example configuration.
|
||||
|
||||
## Configuration
|
||||
|
||||
1) Rename `example-conf.yml` to `pylink.yml` (or a similarly named `.yml` file) and configure your instance there.
|
||||
1) Rename `example-conf.yml` to `pylink.yml` (or a similarly named `.yml` file) and configure your instance there. Note that the configuration format isn't finalized yet - this means that your configuration may break in an update!
|
||||
|
||||
2) Run `pylink` from the command line. PyLink will load its configuration from `pylink.yml` by default, but you can override this by running `pylink` with a config argument (e.g. `pylink mynet.yml`).
|
||||
|
||||
@ -87,39 +76,31 @@ $ docker run -v $HOME/pylink:/pylink jlu5/pylink
|
||||
|
||||
These IRCds (in alphabetical order) are frequently tested and well supported. If any issues occur, please file a bug on the issue tracker.
|
||||
|
||||
* [InspIRCd](http://www.inspircd.org/) (2.0 - 3.x) - module `inspircd`
|
||||
- Set the `target_version` option to `insp3` to target InspIRCd 3.x (default), or `insp20` to target InspIRCd 2.0 (legacy).
|
||||
* [charybdis](https://github.com/charybdis-ircd/charybdis) (3.5+) - module `ts6`
|
||||
- For KLINE support to work, a `shared{}` block should be added for PyLink on all servers.
|
||||
* [InspIRCd](http://www.inspircd.org/) 2.0.x - module `inspircd`
|
||||
- For vHost setting to work, `m_chghost.so` must be loaded. For ident and realname changing support, `m_chgident.so` and `m_chgname.so` must be loaded respectively.
|
||||
- Supported channel, user, and prefix modes are negotiated on connect, but hotloading modules that change these is not supported. After changing module configuration, it is recommended to SQUIT PyLink to force a protocol renegotiation.
|
||||
* [Nefarious IRCu](https://github.com/evilnet/nefarious2) (2.0.0+) - module `p10`
|
||||
- Note: Both account cloaks (user and oper) and hashed IP cloaks are optionally supported (`HOST_HIDING_STYLE` settings 0 to 3). Make sure you configure PyLink to match your IRCd settings.
|
||||
* [UnrealIRCd](https://www.unrealircd.org/) (4.2.x - 5.0.x) - module `unreal`
|
||||
- Supported channel, user, and prefix modes are negotiated on connect, but hotloading modules that change these is not supported. After changing module configuration, it is recommended to SQUIT PyLink to force a protocol renegotiation.
|
||||
* [UnrealIRCd](https://www.unrealircd.org/) 4.x (4.0.12+) - module `unreal`
|
||||
- UnrealIRCd 4.x before version 4.0.12 suffers from [bug #4890](https://bugs.unrealircd.org/view.php?id=4890) which causes hostname desyncs on servers not directly linked to PyLink (e.g. `pylink<->serverA<->serverB` creates desynced hostnames on server B). This problem is fixed by upgrading your IRCds.
|
||||
- Linking to UnrealIRCd 3.2 servers is only possible when using an UnrealIRCd 4.x server as a hub, with topology such as `pylink<->unreal4<->unreal3.2`. We nevertheless encourage you to upgrade so all your IRCds are running the same version.
|
||||
|
||||
### Extended support
|
||||
|
||||
Support for these IRCds exist, but are not tested as frequently and thoroughly. Bugs should be filed if there are any issues, though they may not always be fixed in a timely fashion.
|
||||
|
||||
* [charybdis](https://github.com/charybdis-ircd/charybdis) (3.5+) - module `ts6`
|
||||
- For KLINE support to work, a `shared{}` block should be added for PyLink on all servers.
|
||||
* [ChatIRCd](http://www.chatlounge.net/software) (1.2.x / git master) - module `ts6`
|
||||
- For KLINE support to work, a `shared{}` block should be added for PyLink on all servers.
|
||||
* [juno-ircd](https://github.com/cooper/juno) (13.x / ava) - module `ts6` (see [configuration example](https://github.com/cooper/juno/blob/master/doc/ts6.md#pylink))
|
||||
* [ngIRCd](https://ngircd.barton.de/) (24+) - module `ngircd`
|
||||
- For GLINEs to propagate, the `AllowRemoteOper` option must be enabled in ngIRCd.
|
||||
- `+` (modeless) channels are not supported, and should be disabled for PyLink to function correctly.
|
||||
- For use with Relay, the `CloakHostModeX` setting will work fine but `CloakHost` and `CloakUserToNick` are *not* supported.
|
||||
|
||||
### Legacy extended support
|
||||
|
||||
Support for these IRCds was added at some point but is no longer actively maintained, either due to inactive upstream development or a perceived lack of interest. We recommend migrating to an IRCd in the above two sections.
|
||||
|
||||
* [beware-ircd](http://ircd.bircd.org/) (1.6.3) - module `p10`
|
||||
- Because bircd disallows BURST after ENDBURST for regular servers, U-lines are required for all PyLink servers. Fortunately, wildcards are supported in U-lines, so you can add something along the lines of `U:<your pylink server>:` and `U:*.relay:` (adjust accordingly for your relay server suffix).
|
||||
- Use `ircd: snircd` as the target IRCd.
|
||||
- Halfops, `sethost` (`+h`), and account-based cloaking (`VHostStyle=1`) are supported. Crypted IPs and static hosts (`VHostStyle` 2 and 3) are NOT.
|
||||
* [ChatIRCd](http://www.chatlounge.net/software) (1.2.x / git master) - module `ts6`
|
||||
- For KLINE support to work, a `shared{}` block should be added for PyLink on all servers.
|
||||
* [Elemental-IRCd](https://github.com/Elemental-IRCd/elemental-ircd) (6.6.x / git master) - module `ts6`
|
||||
- For KLINE support to work, a `shared{}` block should be added for PyLink on all servers.
|
||||
* [InspIRCd](http://www.inspircd.org/) 3.0.x (git master) - module `inspircd`
|
||||
- The same notes for InspIRCd 2.x apply here as well.
|
||||
* [IRCd-Hybrid](http://www.ircd-hybrid.org/) (8.2.x / svn trunk) - module `hybrid`
|
||||
- For host changing support and optimal functionality, a `service{}` block / U-line should be added for PyLink on every IRCd across your network.
|
||||
- For KLINE support to work, a `shared{}` block should also be added for PyLink on all servers.
|
||||
@ -128,14 +109,18 @@ Support for these IRCds was added at some point but is no longer actively mainta
|
||||
- On ircd-ratbox, all known IPs of users will be shown in `/whois`, even if the client is e.g. a cloaked relay client. If you're paranoid about this, turn off Relay IP forwarding on the ratbox network(s).
|
||||
- For KLINE support to work, a `shared{}` block should be added for PyLink on all servers.
|
||||
* [IRCu](http://coder-com.undernet.org/) (u2.10.12.16+) - module `p10`
|
||||
- Host changing (changehost, relay) is not supported.
|
||||
- Host changing is not supported.
|
||||
* [juno-ircd](https://github.com/cooper/juno) (13.x / ava) - module `ts6` (see [configuration example](https://github.com/cooper/juno/blob/master/doc/ts6.md#pylink))
|
||||
* [ngIRCd](https://ngircd.barton.de/) (24+) - module `ngircd`
|
||||
- For GLINEs to propagate, the `AllowRemoteOper` option must be enabled in ngIRCd.
|
||||
- `+` (modeless) channels are not supported, and should be disabled for PyLink to function correctly.
|
||||
* [snircd](https://development.quakenet.org/) (1.3.x+) - module `p10`
|
||||
- Outbound host changing (i.e. for the `changehost` plugin) is not supported.
|
||||
- Outbound host changing (i.e. for the `changehost` plugin) is not supported on P10 variants other than Nefarious.
|
||||
|
||||
Other TS6 and P10 variations may work, but are not officially supported.
|
||||
|
||||
### Clientbot
|
||||
|
||||
PyLink supports connecting to IRCds as a relay bot and forwarding users back as virtual clients, similar to Janus' Clientbot. This can be useful if the IRCd a network used isn't supported, or if you want to relay certain channels without fully linking with a network.
|
||||
Since v1.0, PyLink supports connecting to IRCds as a relay bot and forwarding users back, similar to Janus' Clientbot. This can be useful if the IRCd a network used isn't supported, or if you want to relay certain channels without fully linking with a network.
|
||||
|
||||
For Relay to work properly with Clientbot, be sure to load the `relay_clientbot` plugin in conjunction with `relay`.
|
||||
|
||||
Note: **Clientbot links can only be used as a leaf for Relay links - they CANNOT be used to host channels!** This means that Relay does not support having all your networks be Clientbot - in those cases you are better off using a classic relay bot, like [RelayNext for Limnoria](https://github.com/jlu5/SupyPlugins/tree/master/RelayNext).
|
||||
|
532
RELNOTES.md
532
RELNOTES.md
@ -1,271 +1,3 @@
|
||||
# PyLink 3.1.0 (2023-01-03)
|
||||
|
||||
This will be my (**@jlu5**'s) last release.
|
||||
|
||||
Changes since 3.1-beta1:
|
||||
|
||||
### Feature changes
|
||||
|
||||
- Allow loading a custom CA certificate via a ssl_cafile option (#677). Thanks to **@paigeadelethompson** for contributing
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- relay: strip slashes (/) from idents
|
||||
- raw: fix permission check logic
|
||||
|
||||
# PyLink 3.1-beta1 (2021-12-30)
|
||||
|
||||
### Feature changes
|
||||
|
||||
- **PyLink now requires Python >= 3.7**
|
||||
- **protocols/inspircd now defaults to InspIRCd 3.x mode** (`target_version=insp3`)
|
||||
- **Default to system IPv4/IPv6 preference when resolving hostnames.** For existing users, this means that PyLink will most likely **default to IPv6** when resolving hostnames if your server supports it!
|
||||
- You can override this by setting `ipv6: false` in your server config block or using an explicit `bindhost`. Connections made to IPs directly are not affected.
|
||||
- **[SECURITY]** exec and raw plugins are now locked behind config options, to disable running arbitrary code by admins
|
||||
- Implement path configuration for PyLink data files (#659)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Various fixes as detected by pylint, thanks to **@Celelibi** for reporting
|
||||
- ircs2s_common: fix parsing clash if the sender's nick matches an IRC command (e.g. on UnrealIRCd)
|
||||
- ircs2s_common: gracefully handle QUIT messages without a reason (seen on Anope / InspIRCd)
|
||||
- Properly handle EAGAIN in non-blocking sockets
|
||||
- relay: fix "channel not found" errors on LINK when the remote casemapping differs. This mainly affects channels with "|" and other RFC1459 special cases in their name
|
||||
- unreal: bounce attempts to CHGIDENT/HOST/NAME services clients
|
||||
- unreal: fix formatting of outgoing `/kill` (#671)
|
||||
- opercmds: remove double "Killed by" prefixes in the `kill` command
|
||||
|
||||
### Internal improvements
|
||||
|
||||
- Added best-effort tracking of user SSL/TLS status (#169)
|
||||
- Add support for oper notices (GLOBOPS/OPERWALL) (#511)
|
||||
- relay: better ident sanitizing for IRCd-Hybrid
|
||||
- Refactored UID generators to be more concise
|
||||
|
||||
# PyLink 3.0.0 (2020-04-11)
|
||||
|
||||
Changes since 3.0-rc1:
|
||||
|
||||
- Added new install instructions via Docker
|
||||
- global: fix type-safety errors when connected to pylink-discord
|
||||
- Various code cleanup
|
||||
|
||||
For a broader summary of changes since 2.0.x, consult the below release notes for PyLink 3.0-rc1.
|
||||
|
||||
# PyLink 3.0-rc1 (2020-02-22)
|
||||
|
||||
PyLink 3.0 brings PyLink up to date with the latest IRCds (InspIRCd 3, UnrealIRCd 5), and introduces Discord integration via the [pylink-discord](https://github.com/PyLink/pylink-discord) contrib module. It also improves support for Unicode nicks in Relay and Clientbot.
|
||||
|
||||
## Major changes since PyLink 2.0.x
|
||||
|
||||
- **Added support for InspIRCd 3 and UnrealIRCd 5.x**:
|
||||
- To enable InspIRCd 3 link mode (highly recommended) on InspIRCd 3 networks, set the `target_version: insp3` option in the relevant network's `server` block.
|
||||
- For UnrealIRCd 5 support, no changes in PyLink are necessary.
|
||||
- **Updated list of required dependencies: added cachetools, removed ircmatch and expiringdict**
|
||||
- Relay: added an optional dependency on unidecode, to translate Unicode nicks to ASCII on networks not supporting it.
|
||||
- Also, nick normalization is now skipped on protocols where it is not necessary (Clientbot, Discord)
|
||||
- Relay now tracks kill/mode/topic clashes and will stop relaying when it detects such a conflict ([issue#23](https://github.com/jlu5/PyLink/issues/23))
|
||||
- Changehost now supports network-specific configuration ([issue#611](https://github.com/jlu5/PyLink/issues/611)) and listening for services account changes - this allows for consistent account based hostmasks for SASL gateways, etc.
|
||||
- Clientbot: added an option to continuously rejoin channels the bot is not in. [issue#647](https://github.com/jlu5/PyLink/issues/647)
|
||||
- Antispam: added optional quit/part message filtering ([issue#617](https://github.com/jlu5/PyLink/issues/617))
|
||||
|
||||
### API changes
|
||||
- **API Break:** Channel, Server, and User keys may now be type `int` - previously, they were always strings.
|
||||
- **API Break:** PyLink now supports having multiple UIDs mapping to the same nick, as this is common on platforms like Discord.
|
||||
- `nick_to_uid()` has been reworked to optionally return multiple nicks when `multi=True`
|
||||
- Using `nick_to_uid()` without this option will raise a warning if duplicate nicks are present, and may be deprecated in the future.
|
||||
- Added `utils.match_text()`, a general (regex-based) glob matcher to replace `ircmatch` calls ([issue#636](https://github.com/jlu5/PyLink/issues/636)).
|
||||
- Editing hook payloads is now officially supported in plugin hook handlers ([issue#452](https://github.com/jlu5/PyLink/issues/452))
|
||||
- Added new protocol module capabilities:
|
||||
- `can-manage-bot-channels`
|
||||
- `freeform-nicks`
|
||||
- `has-irc-modes`
|
||||
- `virtual-server`
|
||||
- SQUIT hooks now track a list of affected servers (SIDs) in the `affected_servers` field
|
||||
|
||||
This branch was previously known as PyLink 2.1. For more detailed changes between 3.0-rc1 and individual 2.1 snapshots, see their separate changelogs below.
|
||||
|
||||
|
||||
## Changes since PyLink 2.1-beta1
|
||||
|
||||
### Feature changes
|
||||
- Added a Dockerfile for PyLink, thanks to @jameswritescode. Official images will be available on Docker Hub before the final 2.1 release.
|
||||
- unreal: declare support for usermodes +G (censor) and +Z (secureonlymsg)
|
||||
- The `version` command now prints the Python interpreter version to assist with debugging
|
||||
|
||||
### Bug fixes
|
||||
- Fix desync when removing multiple ban modes in one MODE command (regression from 2.1-beta1)
|
||||
- p10: properly ignore ACCOUNT subcommands other than R, M, and U
|
||||
- Fix extraneous lowercasing of the network name in the `$account` exttarget, causing per-network matches to fail if the network name had capital letters
|
||||
- inspircd: negotiate casemapping setting on link for InspIRCd 3. [issue#654](https://github.com/jlu5/PyLink/issues/654)
|
||||
- ircs2s_common: fix crash when failing to extract KILL reason
|
||||
|
||||
### Documentation changes
|
||||
- `relay-quickstart`: describe delinking another network from channels owned by the caller's network
|
||||
- Refreshed mode list documentation
|
||||
|
||||
### Internal improvements
|
||||
- Debug logs for mode parsers and other miscellaneous internals are now less noisy
|
||||
- inspircd: warn when using InspIRCd 2 compat mode on an InspIRCd 3 uplink - some commands like KICK are not translated correctly in this mode
|
||||
- classes: fix `SyntaxWarning: "is" with a literal` on Python 3.8
|
||||
|
||||
|
||||
# PyLink 2.1-beta1 (2019-12-08)
|
||||
|
||||
### Feature changes
|
||||
- **Declare support for UnrealIRCd 5.0.0-rc2+**. Since the S2S protocol has not changed significantly, no protocol changes are needed for this to work.
|
||||
- UnrealIRCd 5.0.0-rc1 suffers from a [message routing bug](https://bugs.unrealircd.org/view.php?id=5469) and is not supported.
|
||||
- clientbot: added an option to continuously rejoin channels the bot is not in. [issue#647](https://github.com/jlu5/PyLink/issues/647)
|
||||
- changehost: added support for network-specific options. [issue#611](https://github.com/jlu5/PyLink/issues/611)
|
||||
- changehost: listen for services account changes - this allows for consistent account based hostmasks for SASL gateways, etc.
|
||||
- relay_clientbot: `rpm` now deals with duplicate nicks and nicks including spaces (e.g. on Discord)
|
||||
- Relay now merges together network specific `clientbot_styles` options with global settings, instead of ignoring the latter if the network specific options are set. [issue#642](https://github.com/jlu5/PyLink/issues/642)
|
||||
- ts6: add support for hiding PyLink servers (`(H)` in server description)
|
||||
- commands: various improvements to the `showuser` command
|
||||
|
||||
### Bug fixes
|
||||
- More fixes for InspIRCd 3 support:
|
||||
- Fix crash on receiving SVSTOPIC on InspIRCd 3
|
||||
- Fix incorrect handling of PING to PyLink subservers - this caused harmless but confusing "high latency" warnings
|
||||
- inspircd: revert change disallowing `_` in hosts; this has been enabled by default for quite some time
|
||||
- Fixed various edge cases in mode handling (+b-b ban cycles, casefolding mode arguments, etc.)
|
||||
- automode: add better handling for protocols where setting IRC modes isn't possible
|
||||
- automode: disable on networks where IRC modes aren't supported. [issue#638](https://github.com/jlu5/PyLink/issues/638)
|
||||
|
||||
### Internal improvements
|
||||
- Added test cases for shared protocol code (mode parsers, state checks, etc.)
|
||||
- Added more IRC parsing tests based off [ircdocs/parser-tests](https://github.com/ircdocs/parser-tests)
|
||||
- Add `get_service_options` method to merge together global & local network options. [issue#642](https://github.com/jlu5/PyLink/issues/642)
|
||||
- Enhancements to UnrealIRCd protocol module:
|
||||
- unreal: read supported user modes on link when available (UnrealIRCd 4.2.3 and later)
|
||||
- unreal: stop sending NETINFO on link; this suppresses protocol version/network name mismatch warnings
|
||||
- unreal: declare support for msgbypass and timedban extbans
|
||||
- Added new protocol capabilities: `has-irc-modes`, `can-manage-bot-channels`
|
||||
- relay: handle acting extbans for ban exceptions `+e`, which is supported by InspIRCd and UnrealIRCd
|
||||
|
||||
|
||||
# PyLink 2.1-alpha2 (2019-07-14)
|
||||
|
||||
**PyLink now requires Python 3.5 or later!**
|
||||
|
||||
This release includes all changes from PyLink 2.0.3, plus the following:
|
||||
|
||||
### Feature changes
|
||||
- **Added cachetools as a core runtime dependency** (relay and expiringdict)
|
||||
- **Added beta support for InspIRCd 3** ([issue#644](https://github.com/jlu5/PyLink/issues/644)):
|
||||
- The target IRCd version can be configured via a `target_version` server option, which supports `insp20` (InspIRCd 2.0, default) and `insp3`.
|
||||
- **Removed dependencies on ircmatch** ([issue#636](https://github.com/jlu5/PyLink/issues/636)) **and expiringdict** ([issue#445](https://github.com/jlu5/PyLink/issues/445))
|
||||
- Increased Passlib requirement to 1.7.0+ to remove deprecated calls
|
||||
- pylink-mkpasswd: use `hash()` instead of `encrypt()`
|
||||
- Relay updates:
|
||||
- Relay now tracks kill/mode/topic clashes and will stop relaying when it detects such a conflict ([issue#23](https://github.com/jlu5/PyLink/issues/23))
|
||||
- Skip nick normalization on protocols where they aren't actually required (Clientbot, Discord)
|
||||
- Support `@servicenick` as a fantasy trigger prefix (useful on Discord)
|
||||
- commands: add a `shownet` command to show server info ([issue#578](https://github.com/jlu5/PyLink/issues/578))
|
||||
- antispam: added optional quit/part message filtering ([issue#617](https://github.com/jlu5/PyLink/issues/617))
|
||||
|
||||
### Bug fixes
|
||||
- SECURITY: only whitelist permissions for the defined `login:user` if a legacy account is enabled
|
||||
- clientbot: fix crash when `MODES` is defined in ISUPPORT without a value (affects connections to Oragono)
|
||||
- relay: fix KILL message formatting (regression from [issue#520](https://github.com/jlu5/PyLink/issues/520))
|
||||
- relay: consistency fixes when handling hideoper mode changes ([issue#629](https://github.com/jlu5/PyLink/issues/629))
|
||||
- exttargets: coerse services_account to string before matching ([issue#639](https://github.com/jlu5/PyLink/issues/639))
|
||||
|
||||
### Internal improvements
|
||||
- **API Break:** Reworked `PyLinkNetworkCore.nick_to_uid()` specification to support duplicate nicks and user filtering
|
||||
- Reworked most plugins (`commands`, `bots`, `opercmds`) to deal with duplicate nicks more robustly
|
||||
- Revised handling of KILL and QUIT hooks: the `userdata` argument is now always defined
|
||||
- If we receive both a KILL and a QUIT for any client, only the one received first will be sent as a hook.
|
||||
- Added new protocol module capabilities: `freeform-nicks`, `virtual-server`
|
||||
- Added `utils.match_text()`, a general glob matching function to replace ircmatch calls ([issue#636](https://github.com/jlu5/PyLink/issues/636))
|
||||
- Editing hook payloads is now officially supported in plugin hook handlers ([issue#452](https://github.com/jlu5/PyLink/issues/452))
|
||||
- ClientbotWrapperProtocol: override `_get_UID()` to only return non-virtual clients, ensuring separate namespaces between internal and external clients.
|
||||
- ClientbotBaseProtocol: disallow `part()` from the main pseudoclient by default, as this may cause desyncs if not supported
|
||||
- Moved IRCv3 message tags parser from `clientbot` to `ircs2s_common`
|
||||
- Merged relay's `showchan`, `showuser` commands into the `commands` plugin, for better tracking of errors and duplicate nicks
|
||||
|
||||
|
||||
# PyLink 2.1-alpha1 (2019-05-02)
|
||||
|
||||
This release focuses on internal improvements to better integrate with [pylink-discord](https://github.com/PyLink/pylink-discord). It includes all fixes from 2.0.2, plus the following:
|
||||
|
||||
#### Feature changes
|
||||
- Various Relay improvements:
|
||||
- Relay now sanitizes UTF-8 nicks and idents where not supported, optionally using the [unidecode](https://github.com/avian2/unidecode) module to decode Unicode names more cleanly to ASCII
|
||||
- This introduces a new option `relay::use_unidecode` which is enabled by default when the module is installed
|
||||
- The fallback character to replace invalid nick characters is now `-` instead of `|`
|
||||
- Add protocol-level support for hiding users in Relay - this is used by pylink-discord to optionally hide invisible/offline users
|
||||
- Decrease default log file size from 50 MiB to 20 MiB
|
||||
|
||||
#### Bug fixes
|
||||
- changehost: only send a host change if new host != original
|
||||
- This prevents duplicate host changes on InspIRCd, since it echoes back successful host changes
|
||||
- clientbot: fix /names parsing errors on networks supporting colors in hosts. [issue#641](https://github.com/jlu5/PyLink/issues/641)
|
||||
- inspircd: disallow `_` in hosts since CHGHOST does not treat it as valid
|
||||
- relay: allow trailing .'s in subserver names (e.g. `relay.` is now an accepted server suffix)
|
||||
- stats: hide login blocks in `/stats O` when not relevant to the caller's network
|
||||
- unreal: work around a potential race when sending kills on join
|
||||
|
||||
#### Internal improvements
|
||||
- log: use pylinkirc as logger name; this prevents other libraries' debug output from making it to the PyLink log by default
|
||||
- clientbot: properly bounce kicks on networks not implementing them
|
||||
- classes: remove channels, modes substitutions from `User.get_fields()`
|
||||
- various: type-safety fixes to support numeric channel, server, and user IDs (they were previously always strings)
|
||||
- SQUIT hooks now track a list of affected servers (SIDs) in the `affected_servers` field
|
||||
- relay: minor optimizations
|
||||
|
||||
# PyLink 2.0.3 (2019-10-11)
|
||||
|
||||
Changes since 2.0.2:
|
||||
|
||||
#### Feature changes
|
||||
- Switch to more secure password hashing defaults, using pbkdf2-sha256 as the default hash method
|
||||
- Introduce a `login::cryptcontext_settings` option to further tweak passlib settings if desired
|
||||
|
||||
#### Bug fixes
|
||||
- **SECURITY**: Only allow the defined `login:user` to take all permissions when legacy accounts are enabled
|
||||
- clientbot: fix /names handling on networks with colours in hostnames
|
||||
- clientbot: fix crash when MODES is defined in ISUPPORT but given no value (affects connections to Oragono)
|
||||
- changehost: only send a host change if new host != original
|
||||
- relay: fix inconsistent handling of the hideoper setting. [issue#629](https://github.com/jlu5/PyLink/issues/629)
|
||||
- unreal: work around a potential race when sending kills on join
|
||||
|
||||
# PyLink 2.0.2 (2019-03-31)
|
||||
|
||||
Changes since 2.0.1:
|
||||
|
||||
#### Feature changes
|
||||
- Antispam now supports filtering away Unicode lookalike characters when processing text
|
||||
- Allow disabling dynamic channels via a new "join_empty_channels" option
|
||||
- relay: add an explicit `forcetag` command, since IRC kills are now relayed between networks
|
||||
|
||||
#### Bug fixes
|
||||
- launcher: fix crash when --no-pid is set
|
||||
- relay: fix DB corruption when editing modedelta modes
|
||||
- automode: fix sending joins to the wrong network when editing remote channels
|
||||
|
||||
#### Internal improvements
|
||||
- relay: minor optimizations and cleanup
|
||||
- Disable throttling on S2S links by default, since it usually isn't necessary there
|
||||
|
||||
# PyLink 2.0.1 (2018-10-06)
|
||||
|
||||
Changes since 2.0.0:
|
||||
|
||||
#### Feature changes
|
||||
- Slashes (`/`) in hosts is now supported on UnrealIRCd.
|
||||
- Added an `ignore_ts_errors` server option to suppress bogus TS warnings.
|
||||
|
||||
#### Bug fixes
|
||||
- clientbot: fix desync when the bot is told to kick itself ([issue#377](https://github.com/jlu5/PyLink/issues/377))
|
||||
- launcher: fix PID files not being read if psutil isn't installed
|
||||
- relay_clientbot no longer relays list modes set during a server burst ([issue#627](https://github.com/jlu5/PyLink/issues/627))
|
||||
- Fixed stray "bogus TS 0" warnings on some UnrealIRCd mode commands
|
||||
|
||||
#### Internal improvements
|
||||
- unreal: bump protocol version to 4200 (UnrealIRCd 4.2.0)
|
||||
- unreal: use SJOIN in `join()` to work around non-deterministic TS when forwarding to other servers
|
||||
|
||||
# PyLink 2.0.0 (2018-07-31)
|
||||
|
||||
Changes since 2.0-rc1:
|
||||
@ -345,10 +77,10 @@ This release does *not* preserve compatibility with third-party plugins written
|
||||
|
||||
#### Internal improvements
|
||||
- Reading from sockets now uses a select-based backend instead of one thread per network.
|
||||
- Major optimizations to user tracking that lets PyLink handle Relay networks of 500+ users.
|
||||
- Major optimizations to to user tracking that lets PyLink handle Relay networks of 500+ users.
|
||||
- Service bot handling was completely redone to minimize desyncs when mixing Relay and services. [issue#265](https://github.com/jlu5/PyLink/issues/265)
|
||||
- This is done via a new `UserMapping` class in `pylinkirc.classes`, which stores User objects by UID and provides a `bynick` attribute mapping case-normalized nicks to lists of UIDs.
|
||||
- `classes.User.nick` is now a property, where the setter implicitly updates the `bynick` index with a pre-computed case-normalized version of the nick (also stored to `User.lower_nick`)
|
||||
- Service bot handling was completely redone to minimize desyncs when mixing Relay and services. [issue#265](https://github.com/jlu5/PyLink/issues/265)
|
||||
|
||||
### Changes in this RC build
|
||||
|
||||
@ -437,58 +169,58 @@ This release contains all changes from 2.0-alpha3 as well as the following:
|
||||
This release contains all changes from 1.3.0, as well as the following:
|
||||
|
||||
#### New features
|
||||
- **Experimental daemonization support via `pylink -d`**. [issue#187](https://github.com/jlu5/PyLink/issues/187)
|
||||
- New (alpha-quality) `antispam` plugin targeting mass-highlight spam: it supports any combination of kick, ban, quiet (mute), and kill as punishment. [issue#359](https://github.com/jlu5/PyLink/issues/359)
|
||||
- **Experimental daemonization support via `pylink -d`**. [issue#187](https://github.com/GLolol/PyLink/issues/187)
|
||||
- New (alpha-quality) `antispam` plugin targeting mass-highlight spam: it supports any combination of kick, ban, quiet (mute), and kill as punishment. [issue#359](https://github.com/GLolol/PyLink/issues/359)
|
||||
- Clientbot now supports expansions such as `$nick` in autoperform.
|
||||
- Relay now translates STATUSMSG messages (e.g. `@#channel` messages) for target networks instead of passing them on as-is. [issue#570](https://github.com/jlu5/PyLink/issues/570)
|
||||
- Relay now translates STATUSMSG messages (e.g. `@#channel` messages) for target networks instead of passing them on as-is. [issue#570](https://github.com/GLolol/PyLink/issues/570)
|
||||
- Relay endburst delay on InspIRCd networks is now configurable via the `servers::NETNAME::relay_endburst_delay` option.
|
||||
- The servermaps plugin now shows the uplink server name for Clientbot links
|
||||
- Added `--trace / -t` options to the launcher for integration with Python's `trace` module.
|
||||
|
||||
#### Feature changes
|
||||
- **Reverted the commit making SIGHUP shutdown the PyLink daemon**. Now, SIGUSR1 and SIGHUP both trigger a rehash, while SIGTERM triggers a shutdown.
|
||||
- The `raw` command has been split into a new plugin (`plugins/raw.py`) with two permissions: `raw.raw` for Clientbot networks, and `raw.raw.unsupported_network` for other protocols. Using raw commands outside Clientbot is not supported. [issue#565](https://github.com/jlu5/PyLink/issues/565)
|
||||
- The `raw` command has been split into a new plugin (`plugins/raw.py`) with two permissions: `raw.raw` for Clientbot networks, and `raw.raw.unsupported_network` for other protocols. Using raw commands outside Clientbot is not supported. [issue#565](https://github.com/GLolol/PyLink/issues/565)
|
||||
- The servermaps plugin now uses two permissions for `map` and `localmap`: `servermaps.map` and `servermaps.localmap` respectively
|
||||
- `showuser` and `showchan` now consistently report times in UTC
|
||||
|
||||
#### Bug fixes
|
||||
- protocols/clientbot: fix errors when connecting to networks with mixed-case server names (e.g. AfterNET)
|
||||
- relay: fix KeyError when a local client is kicked from a claimed channel. [issue#572](https://github.com/jlu5/PyLink/issues/572)
|
||||
- Fix `irc.parse_modes()` incorrectly mangling modes changes like `+b-b *!*@test.host *!*@test.host` into `+b *!*@test.host`. [issue#573](https://github.com/jlu5/PyLink/issues/573)
|
||||
- relay: fix KeyError when a local client is kicked from a claimed channel. [issue#572](https://github.com/GLolol/PyLink/issues/572)
|
||||
- Fix `irc.parse_modes()` incorrectly mangling modes changes like `+b-b *!*@test.host *!*@test.host` into `+b *!*@test.host`. [issue#573](https://github.com/GLolol/PyLink/issues/573)
|
||||
- automode: fix handling of channels with multiple \#'s in them
|
||||
- launcher: prevent protocol module loading errors (e.g. non-existent protocol module) from blocking the setup of other networks.
|
||||
- This fixes a side-effect which can cause relay to stop functioning (`world.started` is never set)
|
||||
- relay_clientbot: fix `STATUSMSG` (`@#channel`) notices from being relayed to channels that it shouldn't
|
||||
- Fixed various 2.0-alpha2 regressions:
|
||||
- Relay now relays service client messages as PRIVMSG and P10 WALL\* commands as NOTICE
|
||||
- protocols/inspircd: fix supported modules list being corrupted when an indirectly linked server shuts down. [issue#567](https://github.com/jlu5/PyLink/issues/567)
|
||||
- networks: `remote` now properly errors if the target service is not available on a network. [issue#554](https://github.com/jlu5/PyLink/issues/554)
|
||||
- protocols/inspircd: fix supported modules list being corrupted when an indirectly linked server shuts down. [issue#567](https://github.com/GLolol/PyLink/issues/567)
|
||||
- networks: `remote` now properly errors if the target service is not available on a network. [issue#554](https://github.com/GLolol/PyLink/issues/554)
|
||||
- commands: fix `showchan` displaying status prefixes in reverse
|
||||
- stats: route permission error replies to notice instead of PRIVMSG
|
||||
- This prevents "Unknown command" flood loops with services which poll `/stats` on link.
|
||||
- clientbot: fixed sending duplicate JOIN hooks and AWAY status updates. [issue#551](https://github.com/jlu5/PyLink/issues/551)
|
||||
- clientbot: fixed sending duplicate JOIN hooks and AWAY status updates. [issue#551](https://github.com/GLolol/PyLink/issues/551)
|
||||
|
||||
#### Internal improvements
|
||||
- **Reading from sockets now uses select instead of one thread per network.**
|
||||
- This new code uses the Python selectors module, which automatically chooses the fastest polling backend available ([`epoll|kqueue|devpoll > poll > select`](https://github.com/python/cpython/blob/v3.6.5/Lib/selectors.py#L599-L601)).
|
||||
- **API Break: significantly reworked channel handling for service bots**. [issue#265](https://github.com/jlu5/PyLink/issues/265)
|
||||
- **API Break: significantly reworked channel handling for service bots**. [issue#265](https://github.com/GLolol/PyLink/issues/265)
|
||||
- The `ServiceBot.extra_channels` attribute in previous versions is replaced with `ServiceBot.dynamic_channels`, which is accessed indirectly via new functions `ServiceBot.add_persistent_channel()`, `ServiceBot.remove_persistent_channel()`, `ServiceBot.get_persistent_channels()`. This API also replaces `ServiceBot.join()` for most plugins, which now joins channels *non-persistently*.
|
||||
- This API change provides plugins with a way of registering dynamic persistent channels, which are consistently rejoined on kick or kill.
|
||||
- Persistent channels are also "dynamic" in the sense that PyLink service bots will now part channels marked persistent when they become empty, and rejoin when it is recreated.
|
||||
- This new implementation is also plugin specific, as plugins must provide a namespace (usually the plugin name) when managing persistent channels using `ServiceBot.(add|remove)_persistent_channel()`.
|
||||
- New abstraction: `ServiceBot.get_persistent_channels()` which fetches the list of all persistent channels on a network (i.e. *both* the config defined channels and what's registered in `dynamic_channels`).
|
||||
- New abstraction: `ServiceBot.part()` sends a part request to channels and only succeeds if it is not marked persistent by any plugin. This effectively works around the long-standing issue of relay-services conflicts. [issue#265](https://github.com/jlu5/PyLink/issues/265)
|
||||
- New abstraction: `ServiceBot.part()` sends a part request to channels and only succeeds if it is not marked persistent by any plugin. This effectively works around the long-standing issue of relay-services conflicts. [issue#265](https://github.com/GLolol/PyLink/issues/265)
|
||||
- Major optimizations to `irc.nick_to_uid`: `PyLinkNetworkCore.users` and `classes.User` now transparently maintain an index mapping nicks to UIDs instead of doing reverse lookup on every call.
|
||||
- This is done via a new `UserMapping` class in `pylinkirc.classes`, which stores User objects by UID and provides a `bynick` attribute mapping case-normalized nicks to lists of UIDs.
|
||||
- `classes.User.nick` is now a property, where the setter implicitly updates the `bynick` index with a pre-computed case-normalized version of the nick (also stored to `User.lower_nick`)
|
||||
- Various relay optimizations: reuse target SID when bursting joins, and only look up nick once in `normalize_nick`
|
||||
- Rewritten CTCP plugin, now extending to all service bots. [issue#468](https://github.com/jlu5/PyLink/issues/468), [issue#407](https://github.com/jlu5/PyLink/issues/407)
|
||||
- Rewritten CTCP plugin, now extending to all service bots. [issue#468](https://github.com/GLolol/PyLink/issues/468), [issue#407](https://github.com/GLolol/PyLink/issues/407)
|
||||
- Relay no longer spams configured U-lines with "message dropped because you aren't in a common channel" errors
|
||||
- The `endburst_delay` option to `spawn_server()` was removed from the protocol spec, and replaced by a private API used by protocols/inspircd and relay.
|
||||
- New API: hook handlers can now filter messages from lower-priority handlers by returning `False`. [issue#547](https://github.com/jlu5/PyLink/issues/547)
|
||||
- New API: added `irc.get_server_option()` to fetch server-specific config variables and global settings as a fallback. [issue#574](https://github.com/jlu5/PyLink/issues/574)
|
||||
- New API: hook handlers can now filter messages from lower-priority handlers by returning `False`. [issue#547](https://github.com/GLolol/PyLink/issues/547)
|
||||
- New API: added `irc.get_server_option()` to fetch server-specific config variables and global settings as a fallback. [issue#574](https://github.com/GLolol/PyLink/issues/574)
|
||||
- automode: replace assert checks with proper exceptions
|
||||
- Renamed methods in log, utils, conf to snake case. [issue#523](https://github.com/jlu5/PyLink/issues/523)
|
||||
- Renamed methods in log, utils, conf to snake case. [issue#523](https://github.com/GLolol/PyLink/issues/523)
|
||||
- Remove `structures.DeprecatedAttributesObject`; it's vastly inefficient for what it accomplishes
|
||||
- clientbot: removed unreliable pre-/WHO join bursting with `userhost-in-names`
|
||||
- API change: `kick` and `kill` command funcitons now raise `NotImplementedError` when not supported by a protocol
|
||||
@ -498,45 +230,45 @@ This release contains all changes from 1.3.0, as well as the following:
|
||||
This release includes all changes from 1.2.2-dev, plus the following:
|
||||
|
||||
#### New features
|
||||
- relay_clientbot: add support for showing prefix modes in relay text, via a new `$mode_prefix` expansion. [issue#540](https://github.com/jlu5/PyLink/issues/540)
|
||||
- relay_clientbot: add support for showing prefix modes in relay text, via a new `$mode_prefix` expansion. [issue#540](https://github.com/GLolol/PyLink/issues/540)
|
||||
- Added new modedelta feature to Relay:
|
||||
- Modedelta allows specifying a list of (named) modes to only apply on leaf channels, which can be helpful to fight spam if leaf networks don't have adequate spam protection.
|
||||
- relay: added new option `server::<networkname>:relay_forcetag_nicks`, a per-network list of nick globs to always tag when introducing users onto a network. [issue#564](https://github.com/jlu5/PyLink/issues/564)
|
||||
- relay: added new option `server::<networkname>:relay_forcetag_nicks`, a per-network list of nick globs to always tag when introducing users onto a network. [issue#564](https://github.com/GLolol/PyLink/issues/564)
|
||||
- Added support for more channel modes in Relay:
|
||||
* blockcaps: inspircd +B, elemental-ircd +G
|
||||
* exemptchanops: inspircd +X
|
||||
* filter: inspircd +g, unreal extban ~T:block ([issue#557](https://github.com/jlu5/PyLink/issues/557))
|
||||
* filter: inspircd +g, unreal extban ~T:block ([issue#557](https://github.com/GLolol/PyLink/issues/557))
|
||||
* hidequits: nefarious +Q, snircd +u
|
||||
* history: inspircd +H
|
||||
* largebanlist: ts6 +L
|
||||
* noamsg: snircd/nefarious +T
|
||||
* blockhighlight: inspircd +V (extras module)
|
||||
* kicknorejoin: elemental-ircd +J ([issue#559](https://github.com/jlu5/PyLink/issues/559))
|
||||
* kicknorejoin_insp: inspircd +J (with argument; [issue#559](https://github.com/jlu5/PyLink/issues/559))
|
||||
* repeat: elemental-ircd +E ([issue#559](https://github.com/jlu5/PyLink/issues/559))
|
||||
* repeat_insp: inspircd +K (with argument; [issue#559](https://github.com/jlu5/PyLink/issues/559))
|
||||
- Added support for UnrealIRCd extban `~T` in Relay. [issue#557](https://github.com/jlu5/PyLink/issues/557)
|
||||
* kicknorejoin: elemental-ircd +J ([issue#559](https://github.com/GLolol/PyLink/issues/559))
|
||||
* kicknorejoin_insp: inspircd +J (with argument; [issue#559](https://github.com/GLolol/PyLink/issues/559))
|
||||
* repeat: elemental-ircd +E ([issue#559](https://github.com/GLolol/PyLink/issues/559))
|
||||
* repeat_insp: inspircd +K (with argument; [issue#559](https://github.com/GLolol/PyLink/issues/559))
|
||||
- Added support for UnrealIRCd extban `~T` in Relay. [issue#557](https://github.com/GLolol/PyLink/issues/557)
|
||||
- p10: added proper support for STATUSMSG notices (i.e. messages to `@#channel` and the like) via WALLCHOPS/WALLHOPS/WALLVOICES
|
||||
- p10: added outgoing /knock support by sending it as a notice
|
||||
- ts6: added incoming /knock handling
|
||||
- relay: added support for relaying /knock
|
||||
|
||||
#### Backwards incompatible changes
|
||||
- **The ratbox protocol module has been merged into ts6**, with a new `ircd: ratbox` option introduced to declare Ratbox as the target IRCd. [issue#543](https://github.com/jlu5/PyLink/issues/543)
|
||||
- **The ratbox protocol module has been merged into ts6**, with a new `ircd: ratbox` option introduced to declare Ratbox as the target IRCd. [issue#543](https://github.com/GLolol/PyLink/issues/543)
|
||||
|
||||
#### Bug fixes
|
||||
- Fix default permissions not applying on startup (2.0-alpha1 regression). [issue#542](https://github.com/jlu5/PyLink/issues/542)
|
||||
- Fix rejoin-on-kill for the main PyLink bot not working (2.0-alpha1/[94e05a6](https://github.com/jlu5/PyLink/commit/94e05a623314e9b0607de4eb01fab28be2e0c7e1) regression).
|
||||
- Fix default permissions not applying on startup (2.0-alpha1 regression). [issue#542](https://github.com/GLolol/PyLink/issues/542)
|
||||
- Fix rejoin-on-kill for the main PyLink bot not working (2.0-alpha1/[94e05a6](https://github.com/GLolol/PyLink/commit/94e05a623314e9b0607de4eb01fab28be2e0c7e1) regression).
|
||||
- Clientbot fixes:
|
||||
- Fix desyncs caused by incomplete nick collision checking when a user on a Clientbot link changes their nick to match an existing virtual client. [issue#535](https://github.com/jlu5/PyLink/issues/535)
|
||||
- Fix desync involving ghost users when a person leaves a channel, changes their nick, and rejoins. [issue#536](https://github.com/jlu5/PyLink/issues/536)
|
||||
- Fix desyncs caused by incomplete nick collision checking when a user on a Clientbot link changes their nick to match an existing virtual client. [issue#535](https://github.com/GLolol/PyLink/issues/535)
|
||||
- Fix desync involving ghost users when a person leaves a channel, changes their nick, and rejoins. [issue#536](https://github.com/GLolol/PyLink/issues/536)
|
||||
- Treat 0 as "no account" when parsing WHOX responses; this fixes incorrect "X is logged in as 0" output on WHOIS.
|
||||
- protocols/p10: fix the `use_hashed_cloaks` server option not being effective.
|
||||
- Fix long standing issues where relay would sometimes burst users multiple times on connect. [issue#529](https://github.com/jlu5/PyLink/issues/529)
|
||||
- Also fix a regression from 2.0-alpha1 where users would not be joined if the hub link is down ([issue#548](https://github.com/jlu5/PyLink/issues/548))
|
||||
- Fix `$a:account` extbans being dropped by relay (they were being confused with `$a`). [issue#560](https://github.com/jlu5/PyLink/issues/560)
|
||||
- Fix corrupt arguments when mixing the `remote` and `mode` commands. [issue#538](https://github.com/jlu5/PyLink/issues/538)
|
||||
- Fix lingering queue threads when networks disconnect. [issue#558](https://github.com/jlu5/PyLink/issues/558)
|
||||
- Fix long standing issues where relay would sometimes burst users multiple times on connect. [issue#529](https://github.com/GLolol/PyLink/issues/529)
|
||||
- Also fix a regression from 2.0-alpha1 where users would not be joined if the hub link is down ([issue#548](https://github.com/GLolol/PyLink/issues/548))
|
||||
- Fix `$a:account` extbans being dropped by relay (they were being confused with `$a`). [issue#560](https://github.com/GLolol/PyLink/issues/560)
|
||||
- Fix corrupt arguments when mixing the `remote` and `mode` commands. [issue#538](https://github.com/GLolol/PyLink/issues/538)
|
||||
- Fix lingering queue threads when networks disconnect. [issue#558](https://github.com/GLolol/PyLink/issues/558)
|
||||
- The relay and global plugins now better handle empty / poorly formed config blocks.
|
||||
- bots: don't allow `spawnclient` on protocol modules with virtual clients (e.g. clientbot)
|
||||
- bots: fix KeyError when trying to join previously nonexistent channels
|
||||
@ -545,10 +277,10 @@ This release includes all changes from 1.2.2-dev, plus the following:
|
||||
- `Channel.sort_prefixes()` now consistently sorts modes from highest to lowest (i.e. from owner to voice). Also removed workaround code added to deal with the wonkiness of this function.
|
||||
- ircs2s_common: add handling for `nick@servername` messages.
|
||||
- `IRCNetwork` should no longer send multiple disconnect hooks for one disconnection.
|
||||
- protocols/ts6 no longer requires `SAVE` support from the uplink. [issue#545](https://github.com/jlu5/PyLink/issues/545)
|
||||
- protocols/ts6 no longer requires `SAVE` support from the uplink. [issue#545](https://github.com/GLolol/PyLink/issues/545)
|
||||
- ts6, hybrid: miscellaneous cleanup
|
||||
- protocols/inspircd now tracks module (un)loads for `m_chghost.so` and friends. [issue#555](https://github.com/jlu5/PyLink/issues/555)
|
||||
- Clientbot now logs failed attempts in joining channels. [issue#533](https://github.com/jlu5/PyLink/issues/533)
|
||||
- protocols/inspircd now tracks module (un)loads for `m_chghost.so` and friends. [issue#555](https://github.com/GLolol/PyLink/issues/555)
|
||||
- Clientbot now logs failed attempts in joining channels. [issue#533](https://github.com/GLolol/PyLink/issues/533)
|
||||
|
||||
# PyLink 2.0-alpha1 (2017-10-07)
|
||||
The "Eclectic" release. This release includes all changes from 1.2.1, plus the following:
|
||||
@ -593,7 +325,7 @@ The "Eclectic" release. This release includes all changes from 1.2.1, plus the f
|
||||
- Fixed a long standing bug where fantasy responses would relay before a user's original command if the `fantasy` plugin was loaded before `relay`. (Bug #123)
|
||||
|
||||
#### Internal changes
|
||||
- **API Break**: The protocol module layer is completely rewritten, with the `Irc` and `Protocol`-derived classes combining into one. Porting **will** be needed for old protocol modules and plugins targetting 1.x; see the [new (WIP) protocol specification](https://github.com/jlu5/PyLink/blob/devel/docs/technical/pmodule-spec.md) for details.
|
||||
- **API Break**: The protocol module layer is completely rewritten, with the `Irc` and `Protocol`-derived classes combining into one. Porting **will** be needed for old protocol modules and plugins targetting 1.x; see the [new (WIP) protocol specification](https://github.com/GLolol/PyLink/blob/devel/docs/technical/pmodule-spec.md) for details.
|
||||
- **API Break**: Channels are now stored in two linked dictionaries per IRC object: once in `irc._channels`, and again in `irc.channels`. The main difference is that `irc._channels` implicitly creates new channels when accessing them if they didn't previously exist (prefer this for protocol modules), while `irc.channels` does not raises and raises KeyError instead (prefer this for plugins).
|
||||
- **API Break**: Most methods in `utils` and `classes` were renamed from camel case to snake case. `log`, `conf`, and others will be ported too before the final 2.0 release.
|
||||
- **API Break**: IRC protocol modules' server introductions must now use **`post_connect()`** instead of **`connect()`** to prevent name collisions with the base connection handling code.
|
||||
@ -610,7 +342,7 @@ The "Eclectic" release. This release includes all changes from 1.2.1, plus the f
|
||||
# PyLink 1.3.0 (2018-05-08)
|
||||
The 1.3 update focuses on backporting some commonly requested and useful features from the WIP 2.0 branch. This release includes all changes from 1.3-beta1, plus the following:
|
||||
|
||||
- Errors due to missing permissions now log to warning. [issue#593](https://github.com/jlu5/PyLink/issues/593)
|
||||
- Errors due to missing permissions now log to warning. [issue#593](https://github.com/GLolol/PyLink/issues/593)
|
||||
- Documentation updates to advanced-relay-config.md and the FAQ
|
||||
|
||||
# PyLink 1.3-beta1 (2018-04-07)
|
||||
@ -618,14 +350,14 @@ The 1.3 update focuses on backporting some commonly requested and useful feature
|
||||
|
||||
#### New features
|
||||
- **Backported the launcher from 2.0-alpha2**:
|
||||
- Added support for daemonization via the `--daemon/-d` option. [issue#187](https://github.com/jlu5/PyLink/issues/187)
|
||||
- Added support for shutdown/restart/rehash via the command line. [issue#244](https://github.com/jlu5/PyLink/issues/244)
|
||||
- The launcher now detects and removes stale PID files when `psutil` (an optional dependency) is installed, making restarting from crashes a more streamlined process. [issue#512](https://github.com/jlu5/PyLink/issues/512)
|
||||
- Added support for daemonization via the `--daemon/-d` option. [issue#187](https://github.com/GLolol/PyLink/issues/187)
|
||||
- Added support for shutdown/restart/rehash via the command line. [issue#244](https://github.com/GLolol/PyLink/issues/244)
|
||||
- The launcher now detects and removes stale PID files when `psutil` (an optional dependency) is installed, making restarting from crashes a more streamlined process. [issue#512](https://github.com/GLolol/PyLink/issues/512)
|
||||
- PID file checking is now enabled by default, with the `--check-pid/-c` option retained as a no-op option for compatibility with PyLink <= 1.2
|
||||
- Following 2.0 changes, sending SIGUSR1 to the PyLink daemon now triggers a rehash (along with SIGHUP).
|
||||
- Service bot idents, hosts, and realnames can now be configured globally and on a per-network basis. [issue#281](https://github.com/jlu5/PyLink/issues/281)
|
||||
- Relay server suffix is now configurable by network (`servers::<netname>::relay_server_suffix` option). [issue#462](https://github.com/jlu5/PyLink/issues/462)
|
||||
- Login blocks can now be restricted to specific networks, opered users, and hostmasks. [issue#502](https://github.com/jlu5/PyLink/issues/502)
|
||||
- Service bot idents, hosts, and realnames can now be configured globally and on a per-network basis. [issue#281](https://github.com/GLolol/PyLink/issues/281)
|
||||
- Relay server suffix is now configurable by network (`servers::<netname>::relay_server_suffix` option). [issue#462](https://github.com/GLolol/PyLink/issues/462)
|
||||
- Login blocks can now be restricted to specific networks, opered users, and hostmasks. [issue#502](https://github.com/GLolol/PyLink/issues/502)
|
||||
- Relay now supports relaying more channel modes, including inspircd blockhighlight +V and exemptchanops +X (the whitelist was synced with 2.0-alpha3)
|
||||
|
||||
#### Bug fixes
|
||||
@ -637,7 +369,7 @@ The 1.3 update focuses on backporting some commonly requested and useful feature
|
||||
- global: ignore empty `global:` configuration blocks
|
||||
|
||||
#### Misc changes
|
||||
- Config loading now uses `yaml.safe_load()` instead of `yaml.load()` so that arbitrary code cannot be executed. [issue#589](https://github.com/jlu5/PyLink/issues/589)
|
||||
- Config loading now uses `yaml.safe_load()` instead of `yaml.load()` so that arbitrary code cannot be executed. [issue#589](https://github.com/GLolol/PyLink/issues/589)
|
||||
- Significantly revised example-conf for wording and consistency.
|
||||
- protocols/unreal: bumped protocol version to 4017 (no changes needed)
|
||||
|
||||
@ -649,7 +381,7 @@ The "Dancer" release. Changes from 1.2.0:
|
||||
- Fix wrong database and PID filenames if the config file name includes a period (".")
|
||||
- automode: don't send empty mode lines if no users match the ACL
|
||||
- networks: check in "remote" that the remote network is actually connected
|
||||
- Fix commonly reported crashes on `logging:` config syntax errors ([49136d5](https://github.com/jlu5/PyLink/commit/49136d5abd609fd5e3ba2ec2e42a0443118e62ab))
|
||||
- Fix commonly reported crashes on `logging:` config syntax errors ([49136d5](https://github.com/GLolol/PyLink/commit/49136d5abd609fd5e3ba2ec2e42a0443118e62ab))
|
||||
- Backported fixes from 2.0-dev:
|
||||
- p10: fix wrong hook name for user introduction
|
||||
- clientbot: warn when an outgoing message is blocked (e.g. due to bans) (#497)
|
||||
@ -719,21 +451,21 @@ For a full list of changes since 1.1.x, consult the changelogs for the 1.2.x bet
|
||||
The "Dynamo" release. This release includes all fixes from 1.1.2, plus the following:
|
||||
|
||||
#### Feature changes
|
||||
- Added configurable encoding support via the `encoding` option in server config blocks ([#467](https://github.com/jlu5/PyLink/pull/467)).
|
||||
- Added configurable encoding support via the `encoding` option in server config blocks ([#467](https://github.com/GLolol/PyLink/pull/467)).
|
||||
- **Certain configuration options were renamed / deprecated:**
|
||||
- The `bot:` configuration block was renamed to `pylink:`, with the old name now deprecated.
|
||||
- `logging:stdout` is now `logging:console` (the previous name was a misnomer since text actually went to `stderr`).
|
||||
- The `bot:prefix` option is deprecated: you should instead define the `prefixes` setting in a separate config block for each service you wish to customize (e.g. set `automode:prefix` and `games:prefix`)
|
||||
- Added new `$and` and `$network` exttargets - see the new [exttargets documentation page](https://github.com/jlu5/PyLink/blob/1.2-beta1/docs/exttargets.md) for how to use them.
|
||||
- Added new `$and` and `$network` exttargets - see the new [exttargets documentation page](https://github.com/GLolol/PyLink/blob/1.2-beta1/docs/exttargets.md) for how to use them.
|
||||
- Hostmasks can now be negated in ban matching: e.g. `!*!*@localhost` now works. Previously, this negation was limited to exttargets only.
|
||||
- `relay_clientbot` no longer colours network names by default. It is still possible to restore the old behaviour by defining [custom clientbot styles](https://github.com/jlu5/PyLink/blob/1.2-beta1/docs/advanced-relay-config.md#custom-clientbot-styles).
|
||||
- `relay_clientbot` no longer colours network names by default. It is still possible to restore the old behaviour by defining [custom clientbot styles](https://github.com/GLolol/PyLink/blob/1.2-beta1/docs/advanced-relay-config.md#custom-clientbot-styles).
|
||||
- `relay_clientbot` no longer uses dark blue as a random colour choice, as it is difficult to read on clients with dark backgrounds.
|
||||
|
||||
#### Bug fixes
|
||||
- Fix service respawn on KILL not working at all - this was likely broken for a while but never noticed...
|
||||
- Fix kick-on-rejoin not working on P10 IRCds when `joinmodes` is set. (a.k.a. acknowledge incoming KICKs with a PART per [the P10 specification](https://github.com/evilnet/nefarious2/blob/ed12d64/doc/p10.txt#L611-L618))
|
||||
- servprotect: only track kills and saves to PyLink clients, not all kills on a network!
|
||||
- Fix `~#channel` prefix messages not working over relay on RFC1459-casemapping networks ([#464](https://github.com/jlu5/PyLink/issues/464)).
|
||||
- Fix `~#channel` prefix messages not working over relay on RFC1459-casemapping networks ([#464](https://github.com/GLolol/PyLink/issues/464)).
|
||||
- Show errors when trying to use `showchan` on a secret channel in the same way as actually non-existent channels. Previously this error response forced replies as a private notice, potentially leaking the existence of secret/private channels.
|
||||
- example-conf: fix reversed description for the password encryption setting.
|
||||
|
||||
@ -913,7 +645,7 @@ The "Crunchy" release. This release includes all bug fixes from PyLink 1.0.4, al
|
||||
- Documentation updates: add a permissions reference, document advanced relay config, etc.
|
||||
|
||||
# PyLink 1.0.4
|
||||
Tagged as **1.0.4** by [jlu5](https://github.com/jlu5)
|
||||
Tagged as **1.0.4** by [GLolol](https://github.com/GLolol)
|
||||
|
||||
The "Bonfire" release.
|
||||
|
||||
@ -987,8 +719,8 @@ The "Candescent" release.
|
||||
- exec: Drop `raw` text logging to DEBUG to prevent information leakage (e.g. passwords on Clientbot)
|
||||
- Removed `update.sh` (my convenience script for locally building + running PyLink)
|
||||
|
||||
# [PyLink 1.0.3](https://github.com/jlu5/PyLink/releases/tag/1.0.3)
|
||||
Tagged as **1.0.3** by [jlu5](https://github.com/jlu5) on 2016-11-20T04:51:11Z
|
||||
# [PyLink 1.0.3](https://github.com/GLolol/PyLink/releases/tag/1.0.3)
|
||||
Tagged as **1.0.3** by [GLolol](https://github.com/GLolol) on 2016-11-20T04:51:11Z
|
||||
|
||||
The "Buoyant" release.
|
||||
|
||||
@ -1006,8 +738,8 @@ The "Buoyant" release.
|
||||
#### Misc. changes
|
||||
- Various spelling/grammar fixes in the example config.
|
||||
|
||||
# [PyLink 1.0.2](https://github.com/jlu5/PyLink/releases/tag/1.0.2)
|
||||
Tagged as **1.0.2** by [jlu5](https://github.com/jlu5) on 2016-10-15T05:52:37Z
|
||||
# [PyLink 1.0.2](https://github.com/GLolol/PyLink/releases/tag/1.0.2)
|
||||
Tagged as **1.0.2** by [GLolol](https://github.com/GLolol) on 2016-10-15T05:52:37Z
|
||||
|
||||
The "Baluga" release.
|
||||
|
||||
@ -1023,8 +755,8 @@ The "Baluga" release.
|
||||
#### Internal fixes / improvements
|
||||
- setup.py: reworded warnings if `git describe --tags` fails / fallback version is used. Also, the internal VCS version for non-Git builds is now `-nogit` instead of `-dirty`.
|
||||
|
||||
# [PyLink 1.0.1](https://github.com/jlu5/PyLink/releases/tag/1.0.1)
|
||||
Tagged as **1.0.1** by [jlu5](https://github.com/jlu5) on 2016-10-06T02:13:42Z
|
||||
# [PyLink 1.0.1](https://github.com/GLolol/PyLink/releases/tag/1.0.1)
|
||||
Tagged as **1.0.1** by [GLolol](https://github.com/GLolol) on 2016-10-06T02:13:42Z
|
||||
|
||||
The "Beam" release.
|
||||
|
||||
@ -1037,8 +769,8 @@ The "Beam" release.
|
||||
- relay: clobber colour codes in hosts
|
||||
- bots: allow JOIN/NICK/QUIT on ServiceBot clients
|
||||
|
||||
# [PyLink 1.0.0](https://github.com/jlu5/PyLink/releases/tag/1.0.0)
|
||||
Tagged as **1.0.0** by [jlu5](https://github.com/jlu5) on 2016-09-17T05:25:51Z
|
||||
# [PyLink 1.0.0](https://github.com/GLolol/PyLink/releases/tag/1.0.0)
|
||||
Tagged as **1.0.0** by [GLolol](https://github.com/GLolol) on 2016-09-17T05:25:51Z
|
||||
|
||||
The "Benevolence" release.
|
||||
|
||||
@ -1066,8 +798,8 @@ The "Benevolence" release.
|
||||
- Added a debug log example <sup><sup><sup>because nobody knew how to turn it on</sup></sup></sup>
|
||||
- Fix inverted option description for Relay's `show_netsplits` option.
|
||||
|
||||
# [PyLink 1.0-beta1](https://github.com/jlu5/PyLink/releases/tag/1.0-beta1)
|
||||
Tagged as **1.0-beta1** by [jlu5](https://github.com/jlu5) on 2016-09-03T07:49:12Z
|
||||
# [PyLink 1.0-beta1](https://github.com/GLolol/PyLink/releases/tag/1.0-beta1)
|
||||
Tagged as **1.0-beta1** by [GLolol](https://github.com/GLolol) on 2016-09-03T07:49:12Z
|
||||
|
||||
The "Badgers" release. Note: This is an **beta** build and may not be completely stable!
|
||||
|
||||
@ -1082,7 +814,7 @@ The "Badgers" release. Note: This is an **beta** build and may not be completely
|
||||
|
||||
#### Feature changes
|
||||
- Irc: implement basic message queueing (1 message sent per X seconds, where X defaults to 0.01 for servers) .
|
||||
- This appears to also workaround sporadic SSL errors causing disconnects (https://github.com/jlu5/PyLink/issues/246)
|
||||
- This appears to also workaround sporadic SSL errors causing disconnects (https://github.com/GLolol/PyLink/issues/246)
|
||||
- relay: CLAIM is now more resistant to things like `/OJOIN` abuse<sup><sup><sup>Seriously people, show some respect for your linked networks ;)</sup></sup></sup>.
|
||||
- core: New permissions system, used exclusively by Automode at this time. See `example-permissions.yml` in the Git tree for configuration options.
|
||||
- relay_clientbot now optionally supports PMs with users linked via Clientbot. This can be enabled via the `relay::allow_clientbot_pms` option, and provides the following behaviour:
|
||||
@ -1104,8 +836,8 @@ The "Badgers" release. Note: This is an **beta** build and may not be completely
|
||||
#### Misc. changes
|
||||
- Various to documentation update and installation instruction improvements.
|
||||
|
||||
# [PyLink 0.10-alpha1](https://github.com/jlu5/PyLink/releases/tag/0.10-alpha1)
|
||||
Tagged as **0.10-alpha1** by [jlu5](https://github.com/jlu5) on 2016-08-22T00:04:34Z
|
||||
# [PyLink 0.10-alpha1](https://github.com/GLolol/PyLink/releases/tag/0.10-alpha1)
|
||||
Tagged as **0.10-alpha1** by [GLolol](https://github.com/GLolol) on 2016-08-22T00:04:34Z
|
||||
|
||||
The "Balloons" release. Note: This is an **alpha** build and may not be completely stable! This version includes all fixes from PyLink 0.9.2, with the following additions:
|
||||
|
||||
@ -1149,8 +881,8 @@ The "Balloons" release. Note: This is an **alpha** build and may not be complete
|
||||
#### Misc. changes
|
||||
- `FakeIRC` and `FakeProto` are removed (unused and not updated for 0.10 internal APIs)
|
||||
|
||||
# [PyLink 0.9.2](https://github.com/jlu5/PyLink/releases/tag/0.9.2)
|
||||
Tagged as **0.9.2** by [jlu5](https://github.com/jlu5) on 2016-08-21T23:59:23Z
|
||||
# [PyLink 0.9.2](https://github.com/GLolol/PyLink/releases/tag/0.9.2)
|
||||
Tagged as **0.9.2** by [GLolol](https://github.com/GLolol) on 2016-08-21T23:59:23Z
|
||||
|
||||
The "Acorn" release.
|
||||
|
||||
@ -1163,8 +895,8 @@ The "Acorn" release.
|
||||
- Relay now normalizes `/` to `.` in hostnames on IRCd-Hybrid.
|
||||
- Cloaked hosts for UnrealIRCd 3.2 users are now applied instead of the real host being visible.
|
||||
|
||||
# [PyLink 0.9.1](https://github.com/jlu5/PyLink/releases/tag/0.9.1)
|
||||
Tagged as **0.9.1** by [jlu5](https://github.com/jlu5) on 2016-08-07T03:05:01Z
|
||||
# [PyLink 0.9.1](https://github.com/GLolol/PyLink/releases/tag/0.9.1)
|
||||
Tagged as **0.9.1** by [GLolol](https://github.com/GLolol) on 2016-08-07T03:05:01Z
|
||||
|
||||
### *Important*, backwards incompatible changes for those upgrading from 0.8.x!
|
||||
- The configuration file is now **pylink.yml** by default, instead of **config.yml**.
|
||||
@ -1194,8 +926,8 @@ Tagged as **0.9.1** by [jlu5](https://github.com/jlu5) on 2016-08-07T03:05:01Z
|
||||
#### Misc. changes
|
||||
- Minor example configuration updates, including a mention of passwordless UnrealIRCd links by setting recvpass and sendpass to `*`.
|
||||
|
||||
# [PyLink 0.9.0](https://github.com/jlu5/PyLink/releases/tag/0.9.0)
|
||||
Tagged as **0.9.0** by [jlu5](https://github.com/jlu5) on 2016-07-25T05:49:55Z
|
||||
# [PyLink 0.9.0](https://github.com/GLolol/PyLink/releases/tag/0.9.0)
|
||||
Tagged as **0.9.0** by [GLolol](https://github.com/GLolol) on 2016-07-25T05:49:55Z
|
||||
|
||||
### *Important*, backwards incompatible changes for those upgrading from 0.8.x!
|
||||
- The configuration file is now **pylink.yml** by default, instead of **config.yml**.
|
||||
@ -1226,8 +958,8 @@ Tagged as **0.9.0** by [jlu5](https://github.com/jlu5) on 2016-07-25T05:49:55Z
|
||||
- Redone version handling so `__init__.py` isn't committed anymore.
|
||||
- `update.sh` now passes arguments to the `pylink` launcher.
|
||||
|
||||
# [PyLink 0.9-beta1](https://github.com/jlu5/PyLink/releases/tag/0.9-beta1)
|
||||
Tagged as **0.9-beta1** by [jlu5](https://github.com/jlu5) on 2016-07-14T02:11:07Z
|
||||
# [PyLink 0.9-beta1](https://github.com/GLolol/PyLink/releases/tag/0.9-beta1)
|
||||
Tagged as **0.9-beta1** by [GLolol](https://github.com/GLolol) on 2016-07-14T02:11:07Z
|
||||
|
||||
### *Important*, backwards incompatible changes for those upgrading from 0.8.x
|
||||
- The configuration file is now **pylink.yml** by default, instead of **config.yml**.
|
||||
@ -1265,8 +997,8 @@ Tagged as **0.9-beta1** by [jlu5](https://github.com/jlu5) on 2016-07-14T02:11:0
|
||||
- Relay now creates relay clones with the current time as nick TS, instead of the origin user's TS.
|
||||
- This has the effect of purposely losing nick collisions against local users, so that it's easier to reclaim nicks.
|
||||
|
||||
# [PyLink 0.9-alpha1](https://github.com/jlu5/PyLink/releases/tag/0.9-alpha1)
|
||||
Tagged as **0.9-alpha1** by [jlu5](https://github.com/jlu5) on 2016-07-09T07:27:47Z
|
||||
# [PyLink 0.9-alpha1](https://github.com/GLolol/PyLink/releases/tag/0.9-alpha1)
|
||||
Tagged as **0.9-alpha1** by [GLolol](https://github.com/GLolol) on 2016-07-09T07:27:47Z
|
||||
|
||||
### Summary of changes from 0.8.x
|
||||
|
||||
@ -1277,7 +1009,7 @@ Tagged as **0.9-alpha1** by [jlu5](https://github.com/jlu5) on 2016-07-09T07:27:
|
||||
##### Added / changed / removed features
|
||||
- New **`ctcp`** plugin, handling CTCP VERSION and PING ~~(and perhaps an easter egg?!)~~
|
||||
- New **`automode`** plugin, implementing basic channel ACL by assigning prefix modes like `+o` to hostmasks and exttargets.
|
||||
- New exttarget support: see https://github.com/jlu5/PyLink/blob/0.9-alpha1/coremods/exttargets.py#L15 for a list of supported ones.
|
||||
- New exttarget support: see https://github.com/GLolol/PyLink/blob/0.9-alpha1/coremods/exttargets.py#L15 for a list of supported ones.
|
||||
- Relay can now handle messages sent by users not in a target channel (e.g. for channels marked `-n`)
|
||||
- Relay subserver spawning is now always on - the `spawn_servers` option is removed
|
||||
- Relay can now optionally show netsplits from remote networks, using a `show_netsplits` option in the `relay:` block
|
||||
@ -1315,8 +1047,8 @@ Tagged as **0.9-alpha1** by [jlu5](https://github.com/jlu5) on 2016-07-09T07:27:
|
||||
- protocols/nefarious,ts6,unreal: KILL handling (inbound & outbound) now supports kill paths and formats kill reasons properly
|
||||
- protocols: encapsulated (ENCAP) commands are now implicitly expanded, so protocol modules no longer need to bother with IF statement chains in a `handle_encap()`
|
||||
|
||||
# [PyLink 0.8-alpha4](https://github.com/jlu5/PyLink/releases/tag/0.8-alpha4)
|
||||
Tagged as **0.8-alpha4** by [jlu5](https://github.com/jlu5) on 2016-06-30T18:56:42Z
|
||||
# [PyLink 0.8-alpha4](https://github.com/GLolol/PyLink/releases/tag/0.8-alpha4)
|
||||
Tagged as **0.8-alpha4** by [GLolol](https://github.com/GLolol) on 2016-06-30T18:56:42Z
|
||||
|
||||
Major changes in this snapshot release:
|
||||
|
||||
@ -1331,10 +1063,10 @@ Major changes in this snapshot release:
|
||||
- Example conf: fix various typos (0edb516, cd4bf55) and be more clear about link blocks only being examples
|
||||
- Various freezes and crash bugs fixed (dd08c01, 1ad8b2e, 504a9be, 5f2da1c)
|
||||
|
||||
Full diff: https://github.com/jlu5/PyLink/compare/0.8-alpha3...0.8-alpha4
|
||||
Full diff: https://github.com/GLolol/PyLink/compare/0.8-alpha3...0.8-alpha4
|
||||
|
||||
# [PyLink 0.8-alpha3](https://github.com/jlu5/PyLink/releases/tag/0.8-alpha3)
|
||||
Tagged as **0.8-alpha3** by [jlu5](https://github.com/jlu5) on 2016-06-01T02:58:49Z
|
||||
# [PyLink 0.8-alpha3](https://github.com/GLolol/PyLink/releases/tag/0.8-alpha3)
|
||||
Tagged as **0.8-alpha3** by [GLolol](https://github.com/GLolol) on 2016-06-01T02:58:49Z
|
||||
|
||||
- relay: support relaying a few more channel modes (flood, joinflood, freetarget, noforwards, and noinvite)
|
||||
- Introduce a new (WIP) API to create simple service bots (#216).
|
||||
@ -1345,8 +1077,8 @@ Tagged as **0.8-alpha3** by [jlu5](https://github.com/jlu5) on 2016-06-01T02:58:
|
||||
- New `games` plugin, currently implementing eightball, dice, and fml.
|
||||
- Various fixes to the Nefarious protocol module (89ed92b46a4376abf69698b76955fec010a230b4...c82cc9d822ad46f441de3f2f820d5203b6e70516, #209, #210).
|
||||
|
||||
# [PyLink 0.8-alpha2](https://github.com/jlu5/PyLink/releases/tag/0.8-alpha2)
|
||||
Tagged as **0.8-alpha2** by [jlu5](https://github.com/jlu5) on 2016-05-08T04:40:17Z
|
||||
# [PyLink 0.8-alpha2](https://github.com/GLolol/PyLink/releases/tag/0.8-alpha2)
|
||||
Tagged as **0.8-alpha2** by [GLolol](https://github.com/GLolol) on 2016-05-08T04:40:17Z
|
||||
|
||||
- protocols/nefarious: fix incorrect decoding of IPv6 addresses (0e0d96e)
|
||||
- protocols/(hybrid|nefarious): add missing BURST/SJOIN->JOIN hook mappings, fixing problems with relay missing users after a netjoin
|
||||
@ -1356,10 +1088,10 @@ Tagged as **0.8-alpha2** by [jlu5](https://github.com/jlu5) on 2016-05-08T04:40:
|
||||
- relay: Fix various race conditions, especially when multiple networks happen to lose connection simultaneously
|
||||
- API changes: many commands from `utils` were split into either `Irc()` or a new `structures` module (#199)
|
||||
|
||||
[Full diff](https://github.com/jlu5/PyLink/compare/0.8-alpha1...0.8-alpha2)
|
||||
[Full diff](https://github.com/GLolol/PyLink/compare/0.8-alpha1...0.8-alpha2)
|
||||
|
||||
# [PyLink 0.8-alpha1](https://github.com/jlu5/PyLink/releases/tag/0.8-alpha1)
|
||||
Tagged as **0.8-alpha1** by [jlu5](https://github.com/jlu5) on 2016-04-23T03:14:21Z
|
||||
# [PyLink 0.8-alpha1](https://github.com/GLolol/PyLink/releases/tag/0.8-alpha1)
|
||||
Tagged as **0.8-alpha1** by [GLolol](https://github.com/GLolol) on 2016-04-23T03:14:21Z
|
||||
|
||||
- New protocol support: IRCd-Hybrid 8.x and Nefarious IRCu
|
||||
- Track user IPs of UnrealIRCd 3.2 users (#196)
|
||||
@ -1367,10 +1099,10 @@ Tagged as **0.8-alpha1** by [jlu5](https://github.com/jlu5) on 2016-04-23T03:14:
|
||||
- Improved mode support for Charybdis (#203)
|
||||
- Fix disconnect logic during ping timeouts
|
||||
|
||||
[Full diff](https://github.com/jlu5/PyLink/compare/0.7.2-dev...0.8-alpha1)
|
||||
[Full diff](https://github.com/GLolol/PyLink/compare/0.7.2-dev...0.8-alpha1)
|
||||
|
||||
# [PyLink 0.7.2-dev](https://github.com/jlu5/PyLink/releases/tag/0.7.2-dev)
|
||||
Tagged as **0.7.2-dev** by [jlu5](https://github.com/jlu5) on 2016-04-19T14:03:50Z
|
||||
# [PyLink 0.7.2-dev](https://github.com/GLolol/PyLink/releases/tag/0.7.2-dev)
|
||||
Tagged as **0.7.2-dev** by [GLolol](https://github.com/GLolol) on 2016-04-19T14:03:50Z
|
||||
|
||||
Bug fix release:
|
||||
|
||||
@ -1464,8 +1196,8 @@ Bug fix release:
|
||||
- 1d4350c4fd00e7f8012781992ab73a1b73f396d2 classes: provide IrcChannel objects with their own name using KeyedDefaultdict
|
||||
- 544d6e10418165415c8ffe2b5fbe59fcffd65b0f utils: add KeyedDefaultdict
|
||||
|
||||
# [PyLink 0.7.1-dev](https://github.com/jlu5/PyLink/releases/tag/0.7.1-dev)
|
||||
Tagged as **0.7.1-dev** by [jlu5](https://github.com/jlu5) on 2016-03-31T01:42:41Z
|
||||
# [PyLink 0.7.1-dev](https://github.com/GLolol/PyLink/releases/tag/0.7.1-dev)
|
||||
Tagged as **0.7.1-dev** by [GLolol](https://github.com/GLolol) on 2016-03-31T01:42:41Z
|
||||
|
||||
Bugfix release. Lingering errata which you may still encounter: #183.
|
||||
|
||||
@ -1478,8 +1210,8 @@ Bugfix release. Lingering errata which you may still encounter: #183.
|
||||
- 9cd1635f68dafee47f147de43b258014d14da6e2 unreal: fix wrong variable name in handle_umode2
|
||||
- 2169a9be28331c6207865d50912cd671ff3c34a2 utils: actually abort when mode target is invalid
|
||||
|
||||
# [PyLink 0.7.0-dev](https://github.com/jlu5/PyLink/releases/tag/0.7.0-dev)
|
||||
Tagged as **0.7.0-dev** by [jlu5](https://github.com/jlu5) on 2016-03-21T19:09:12Z
|
||||
# [PyLink 0.7.0-dev](https://github.com/GLolol/PyLink/releases/tag/0.7.0-dev)
|
||||
Tagged as **0.7.0-dev** by [GLolol](https://github.com/GLolol) on 2016-03-21T19:09:12Z
|
||||
|
||||
### Changes from 0.6.1-dev:
|
||||
|
||||
@ -1532,8 +1264,8 @@ Tagged as **0.7.0-dev** by [jlu5](https://github.com/jlu5) on 2016-03-21T19:09:1
|
||||
- 4b939ea641284aa9bbb796adc58d273f080e59ee ts6: rewrite end-of-burst code (EOB is literally just a PING in ts6)
|
||||
- 5a68dc1bc5f880d1117ca81e729f90fb5e1fce38 Irc: don't call initVars() on IRC object initialization
|
||||
|
||||
# [PyLink 0.6.1-dev](https://github.com/jlu5/PyLink/releases/tag/0.6.1-dev)
|
||||
Tagged as **0.6.1-dev** by [jlu5](https://github.com/jlu5) on 2016-03-02T05:15:22Z
|
||||
# [PyLink 0.6.1-dev](https://github.com/GLolol/PyLink/releases/tag/0.6.1-dev)
|
||||
Tagged as **0.6.1-dev** by [GLolol](https://github.com/GLolol) on 2016-03-02T05:15:22Z
|
||||
|
||||
* Bug fix release.
|
||||
- unreal: fix handing of users connecting via IPv4 3c3ae10
|
||||
@ -1541,8 +1273,8 @@ Tagged as **0.6.1-dev** by [jlu5](https://github.com/jlu5) on 2016-03-02T05:15:2
|
||||
- inspircd, ts6: don't crash when receiving an unrecognized UID 341c208
|
||||
- inspircd: format kill reasons like `Killed (sourcenick (reason))` properly.
|
||||
|
||||
# [PyLink 0.6.0-dev](https://github.com/jlu5/PyLink/releases/tag/0.6.0-dev)
|
||||
Tagged as **0.6.0-dev** by [jlu5](https://github.com/jlu5) on 2016-01-23T18:24:10Z
|
||||
# [PyLink 0.6.0-dev](https://github.com/GLolol/PyLink/releases/tag/0.6.0-dev)
|
||||
Tagged as **0.6.0-dev** by [GLolol](https://github.com/GLolol) on 2016-01-23T18:24:10Z
|
||||
|
||||
Notable changes in this release:
|
||||
|
||||
@ -1562,10 +1294,10 @@ Notable changes in this release:
|
||||
- protocols: allow changing remote users' hosts in updateClient (741fed9).
|
||||
- Speed up and clean up shutdown sequence, fixing hangs due to sockets not shutting down cleanly (#152).
|
||||
- protocols/unreal: Support cloaking with user mode `+x` (#136).
|
||||
- Various bug fixes - see https://github.com/jlu5/PyLink/compare/0.5-dev...0.6.0-dev for a full diff.
|
||||
- Various bug fixes - see https://github.com/GLolol/PyLink/compare/0.5-dev...0.6.0-dev for a full diff.
|
||||
|
||||
# [PyLink 0.5-dev](https://github.com/jlu5/PyLink/releases/tag/0.5-dev)
|
||||
Tagged as **0.5-dev** by [jlu5](https://github.com/jlu5) on 2015-12-06T17:54:02Z
|
||||
# [PyLink 0.5-dev](https://github.com/GLolol/PyLink/releases/tag/0.5-dev)
|
||||
Tagged as **0.5-dev** by [GLolol](https://github.com/GLolol) on 2015-12-06T17:54:02Z
|
||||
|
||||
The "We're getting somewhere..." release.
|
||||
|
||||
@ -1591,27 +1323,27 @@ The "We're getting somewhere..." release.
|
||||
- protocols/unreal: **Add (experimental) support for UnrealIRCd 4.0.x!**
|
||||
- plugins: More complete INFO logging: plugin loading/unloading, unknown commands called, successful operups
|
||||
|
||||
Full diff:https://github.com/jlu5/PyLink/compare/0.4.6-dev...0.5-dev
|
||||
Full diff:https://github.com/GLolol/PyLink/compare/0.4.6-dev...0.5-dev
|
||||
|
||||
# [PyLink 0.4.6-dev](https://github.com/jlu5/PyLink/releases/tag/0.4.6-dev)
|
||||
Tagged as **0.4.6-dev** by [jlu5](https://github.com/jlu5) on 2015-10-01T23:44:20Z
|
||||
# [PyLink 0.4.6-dev](https://github.com/GLolol/PyLink/releases/tag/0.4.6-dev)
|
||||
Tagged as **0.4.6-dev** by [GLolol](https://github.com/GLolol) on 2015-10-01T23:44:20Z
|
||||
|
||||
Bugfix release:
|
||||
|
||||
- f20e6775770b7a118a697c8ae08364d850cdf116 relay: fix PMs across the relay (7d919e6 regression)
|
||||
- 55d9eb240f037a3378a92ab7661b31011398f565 classes.Irc: prettier __repr__
|
||||
|
||||
# [PyLink 0.4.5-dev](https://github.com/jlu5/PyLink/releases/tag/0.4.5-dev)
|
||||
Tagged as **0.4.5-dev** by [jlu5](https://github.com/jlu5) on 2015-09-30T04:14:22Z
|
||||
# [PyLink 0.4.5-dev](https://github.com/GLolol/PyLink/releases/tag/0.4.5-dev)
|
||||
Tagged as **0.4.5-dev** by [GLolol](https://github.com/GLolol) on 2015-09-30T04:14:22Z
|
||||
|
||||
The "fancy stuff!" release.
|
||||
|
||||
New features including in-place config reloading (rehashing) (#89), FANTASY support (#111), and plugin (re/un)loading without a restart.
|
||||
|
||||
Full diff since 0.4.0-dev: https://github.com/jlu5/PyLink/compare/0.4.0-dev...0.4.5-dev
|
||||
Full diff since 0.4.0-dev: https://github.com/GLolol/PyLink/compare/0.4.0-dev...0.4.5-dev
|
||||
|
||||
# [PyLink 0.3.50-dev](https://github.com/jlu5/PyLink/releases/tag/0.3.50-dev)
|
||||
Tagged as **0.3.50-dev** by [jlu5](https://github.com/jlu5) on 2015-09-19T18:28:24Z
|
||||
# [PyLink 0.3.50-dev](https://github.com/GLolol/PyLink/releases/tag/0.3.50-dev)
|
||||
Tagged as **0.3.50-dev** by [GLolol](https://github.com/GLolol) on 2015-09-19T18:28:24Z
|
||||
|
||||
Many updates to core, preparing for an (eventual) 0.4.x release. Commits:
|
||||
|
||||
@ -1658,10 +1390,10 @@ Many updates to core, preparing for an (eventual) 0.4.x release. Commits:
|
||||
- 3d621b0 Move checkAuthenticated() to utils, and give it and isOper() toggles for allowing oper/PyLink logins
|
||||
- 090fa85 Move Irc() from main.py to classes.py
|
||||
|
||||
# [PyLink 0.3.1-dev](https://github.com/jlu5/PyLink/releases/tag/0.3.1-dev)
|
||||
Tagged as **0.3.1-dev** by [jlu5](https://github.com/jlu5) on 2015-09-03T06:56:48Z
|
||||
# [PyLink 0.3.1-dev](https://github.com/GLolol/PyLink/releases/tag/0.3.1-dev)
|
||||
Tagged as **0.3.1-dev** by [GLolol](https://github.com/GLolol) on 2015-09-03T06:56:48Z
|
||||
|
||||
Bugfix release + LINKACL support for relay. [Commits since 0.3.0-dev](https://github.com/jlu5/PyLink/compare/0.3.0-dev...0.3.1-dev):
|
||||
Bugfix release + LINKACL support for relay. [Commits since 0.3.0-dev](https://github.com/GLolol/PyLink/compare/0.3.0-dev...0.3.1-dev):
|
||||
|
||||
- 043fccf4470bfbc8041056f5dbb694be079a45a5 Fix previous commit (Closes #100)
|
||||
- 708d94916477f53ddc79a90c4ff321f636c01348 relay: join remote users before sending ours
|
||||
@ -1673,13 +1405,13 @@ Bugfix release + LINKACL support for relay. [Commits since 0.3.0-dev](https://gi
|
||||
- 3523f8f7663e618829dccfbec6eccfaf0ec87cc5 LINKACL support
|
||||
- 51389b96e26224aab262b7b090032d0b745e9590 relay: LINKACL command (Closes #88)
|
||||
|
||||
# [PyLink 0.2.5-dev](https://github.com/jlu5/PyLink/releases/tag/0.2.5-dev)
|
||||
Tagged as **0.2.5-dev** by [jlu5](https://github.com/jlu5) on 2015-08-16T05:39:34Z
|
||||
# [PyLink 0.2.5-dev](https://github.com/GLolol/PyLink/releases/tag/0.2.5-dev)
|
||||
Tagged as **0.2.5-dev** by [GLolol](https://github.com/GLolol) on 2015-08-16T05:39:34Z
|
||||
|
||||
See the diff for this development build: https://github.com/jlu5/PyLink/compare/0.2.3-dev...0.2.5-dev
|
||||
See the diff for this development build: https://github.com/GLolol/PyLink/compare/0.2.3-dev...0.2.5-dev
|
||||
|
||||
# [PyLink 0.2.3-dev](https://github.com/jlu5/PyLink/releases/tag/0.2.3-dev)
|
||||
Tagged as **0.2.3-dev** by [jlu5](https://github.com/jlu5) on 2015-07-26T06:11:20Z
|
||||
# [PyLink 0.2.3-dev](https://github.com/GLolol/PyLink/releases/tag/0.2.3-dev)
|
||||
Tagged as **0.2.3-dev** by [GLolol](https://github.com/GLolol) on 2015-07-26T06:11:20Z
|
||||
|
||||
The "prevent PyLink from wrecking my server's CPU" release.
|
||||
|
||||
@ -1703,10 +1435,10 @@ Mostly bug fixes here, with a couple of scripts added (`start-cpulimit.sh` and `
|
||||
- ts6: fix `JOIN` handling and `parse_as` key handling in hooks (ddefd38)
|
||||
- relay: only wait for `irc.connected` once per network (4d7d7ce)
|
||||
|
||||
Full diff: https://github.com/jlu5/PyLink/compare/0.2.2-dev...0.2.3-dev
|
||||
Full diff: https://github.com/GLolol/PyLink/compare/0.2.2-dev...0.2.3-dev
|
||||
|
||||
# [PyLink 0.2.2-dev](https://github.com/jlu5/PyLink/releases/tag/0.2.2-dev)
|
||||
Tagged as **0.2.2-dev** by [jlu5](https://github.com/jlu5) on 2015-07-24T18:09:44Z
|
||||
# [PyLink 0.2.2-dev](https://github.com/GLolol/PyLink/releases/tag/0.2.2-dev)
|
||||
Tagged as **0.2.2-dev** by [GLolol](https://github.com/GLolol) on 2015-07-24T18:09:44Z
|
||||
|
||||
The "please don't break again :( " release.
|
||||
|
||||
@ -1717,10 +1449,10 @@ The "please don't break again :( " release.
|
||||
|
||||
...And of course, lots and lots of bug fixes; I won't bother to list them all.
|
||||
|
||||
Full diff: https://github.com/jlu5/PyLink/compare/0.2.0-dev...0.2.2-dev
|
||||
Full diff: https://github.com/GLolol/PyLink/compare/0.2.0-dev...0.2.2-dev
|
||||
|
||||
# [PyLink 0.2.0-dev](https://github.com/jlu5/PyLink/releases/tag/0.2.0-dev)
|
||||
Tagged as **0.2.0-dev** by [jlu5](https://github.com/jlu5) on 2015-07-23T04:44:17Z
|
||||
# [PyLink 0.2.0-dev](https://github.com/GLolol/PyLink/releases/tag/0.2.0-dev)
|
||||
Tagged as **0.2.0-dev** by [GLolol](https://github.com/GLolol) on 2015-07-23T04:44:17Z
|
||||
|
||||
Many changes in this development release, including:
|
||||
|
||||
@ -1736,10 +1468,10 @@ Many changes in this development release, including:
|
||||
|
||||
And of course, many, many bug fixes! (relay should now work properly with more than 2 networks, for example...)
|
||||
|
||||
Full diff: https://github.com/jlu5/PyLink/compare/0.1.6-dev...0.2.0-dev
|
||||
Full diff: https://github.com/GLolol/PyLink/compare/0.1.6-dev...0.2.0-dev
|
||||
|
||||
# [PyLink 0.1.6-dev](https://github.com/jlu5/PyLink/releases/tag/0.1.6-dev)
|
||||
Tagged as **0.1.6-dev** by [jlu5](https://github.com/jlu5) on 2015-07-20T06:09:40Z
|
||||
# [PyLink 0.1.6-dev](https://github.com/GLolol/PyLink/releases/tag/0.1.6-dev)
|
||||
Tagged as **0.1.6-dev** by [GLolol](https://github.com/GLolol) on 2015-07-20T06:09:40Z
|
||||
|
||||
### Bug fixes and improvements from 0.1.5-dev
|
||||
|
||||
@ -1751,10 +1483,10 @@ Tagged as **0.1.6-dev** by [jlu5](https://github.com/jlu5) on 2015-07-20T06:09:4
|
||||
- utils: add `getHostmask()` (1b09a00)
|
||||
- various: Log command usage, `exec` usage, successful logins, and access denied errors in `admin.py`'s commands (57e9bf6)
|
||||
|
||||
Full diff: https://github.com/jlu5/PyLink/compare/0.1.5-dev...0.1.6-dev
|
||||
Full diff: https://github.com/GLolol/PyLink/compare/0.1.5-dev...0.1.6-dev
|
||||
|
||||
# [PyLink 0.1.5-dev](https://github.com/jlu5/PyLink/releases/tag/0.1.5-dev)
|
||||
Tagged as **0.1.5-dev** by [jlu5](https://github.com/jlu5) on 2015-07-18T20:01:39Z
|
||||
# [PyLink 0.1.5-dev](https://github.com/GLolol/PyLink/releases/tag/0.1.5-dev)
|
||||
Tagged as **0.1.5-dev** by [GLolol](https://github.com/GLolol) on 2015-07-18T20:01:39Z
|
||||
|
||||
### New features
|
||||
|
||||
@ -1785,10 +1517,10 @@ Tagged as **0.1.5-dev** by [jlu5](https://github.com/jlu5) on 2015-07-18T20:01:3
|
||||
- commands: remove `debug` command; it's useless now that `exec`, `showchan`, and `showuser` exist (50665ec)
|
||||
- admin: `tell` command has been removed. Rationale: limited usefulness; doesn't wrap long messages properly. (4553eda)
|
||||
|
||||
You can view the full diff here: https://github.com/jlu5/PyLink/compare/0.1.0-dev...0.1.5-dev
|
||||
You can view the full diff here: https://github.com/GLolol/PyLink/compare/0.1.0-dev...0.1.5-dev
|
||||
|
||||
# [PyLink 0.1.0-dev](https://github.com/jlu5/PyLink/releases/tag/0.1.0-dev)
|
||||
Tagged as **0.1.0-dev** by [jlu5](https://github.com/jlu5) on 2015-07-16T06:27:12Z
|
||||
# [PyLink 0.1.0-dev](https://github.com/GLolol/PyLink/releases/tag/0.1.0-dev)
|
||||
Tagged as **0.1.0-dev** by [GLolol](https://github.com/GLolol) on 2015-07-16T06:27:12Z
|
||||
|
||||
PyLink's first pre-alpha development snapshot.
|
||||
|
||||
|
601
classes.py
601
classes.py
File diff suppressed because it is too large
Load Diff
12
conf.py
12
conf.py
@ -10,17 +10,13 @@ try:
|
||||
except ImportError:
|
||||
raise ImportError("PyLink requires PyYAML to function; please install it and try again.")
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
import sys
|
||||
import os.path
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from . import world
|
||||
|
||||
__all__ = ['ConfigurationError', 'conf', 'confname', 'validate', 'load_conf',
|
||||
'get_database_name']
|
||||
|
||||
|
||||
class ConfigurationError(RuntimeError):
|
||||
"""Error when config conditions aren't met."""
|
||||
|
||||
@ -33,7 +29,7 @@ conf = {'bot':
|
||||
},
|
||||
'logging':
|
||||
{
|
||||
'console': 'INFO'
|
||||
'stdout': 'INFO'
|
||||
},
|
||||
'servers':
|
||||
# Wildcard defaultdict! This means that
|
||||
@ -145,7 +141,7 @@ def get_database_name(dbname):
|
||||
|
||||
This returns '<dbname>.db' if the running config name is PyLink's default
|
||||
(pylink.yml), and '<dbname>-<config name>.db' for anything else. For example,
|
||||
if this is called from an instance running as 'pylink testing.yml', it
|
||||
if this is called from an instance running as './pylink testing.yml', it
|
||||
would return '<dbname>-testing.db'."""
|
||||
if confname != 'pylink':
|
||||
dbname += '-%s' % confname
|
||||
|
@ -1,18 +1,15 @@
|
||||
"""
|
||||
control.py - Implements SHUTDOWN and REHASH functionality.
|
||||
"""
|
||||
import atexit
|
||||
import os
|
||||
import signal
|
||||
import os
|
||||
import threading
|
||||
import sys
|
||||
import atexit
|
||||
|
||||
from pylinkirc import conf, utils, world # Do not import classes, it'll import loop
|
||||
from pylinkirc.log import _get_console_log_level, _make_file_logger, _stop_file_loggers, log
|
||||
|
||||
from . import login
|
||||
|
||||
__all__ = ['remove_network', 'shutdown', 'rehash']
|
||||
|
||||
from pylinkirc import world, utils, conf # Do not import classes, it'll import loop
|
||||
from pylinkirc.log import log, _make_file_logger, _stop_file_loggers, _get_console_log_level
|
||||
from . import permissions
|
||||
|
||||
def remove_network(ircobj):
|
||||
"""Removes a network object from the pool."""
|
||||
@ -74,10 +71,7 @@ def shutdown(irc=None):
|
||||
|
||||
for ircobj in world.networkobjects.copy().values():
|
||||
# Disconnect all our networks.
|
||||
try:
|
||||
remove_network(ircobj)
|
||||
except NotImplementedError:
|
||||
continue
|
||||
|
||||
log.info("Waiting for remaining threads to stop; this may take a few seconds. If PyLink freezes "
|
||||
"at this stage, press Ctrl-C to force a shutdown.")
|
||||
@ -110,15 +104,10 @@ def rehash():
|
||||
|
||||
log.debug('rehash: updating console log level')
|
||||
world.console_handler.setLevel(_get_console_log_level())
|
||||
login._make_cryptcontext() # refresh password hashing settings
|
||||
|
||||
for network, ircobj in world.networkobjects.copy().items():
|
||||
# Server was removed from the config file, disconnect them.
|
||||
log.debug('rehash: checking if %r is still in new conf.', network)
|
||||
if ircobj.has_cap('virtual-server') or hasattr(ircobj, 'virtual_parent'):
|
||||
log.debug('rehash: not removing network %r since it is a virtual server.', network)
|
||||
continue
|
||||
|
||||
log.debug('rehash: checking if %r is in new conf still.', network)
|
||||
if network not in new_conf['servers']:
|
||||
log.debug('rehash: removing connection to %r (removed from config).', network)
|
||||
remove_network(ircobj)
|
||||
|
@ -4,14 +4,12 @@ corecommands.py - Implements core PyLink commands.
|
||||
|
||||
import gc
|
||||
import sys
|
||||
import importlib
|
||||
|
||||
from pylinkirc import utils, world
|
||||
from . import control, login, permissions
|
||||
from pylinkirc import utils, world, conf
|
||||
from pylinkirc.log import log
|
||||
|
||||
from . import control, permissions
|
||||
|
||||
__all__ = []
|
||||
|
||||
# Essential, core commands go here so that the "commands" plugin with less-important,
|
||||
# but still generic functions can be reloaded.
|
||||
|
||||
|
@ -5,9 +5,6 @@ exttargets.py - Implements extended targets like $account:xyz, $oper, etc.
|
||||
from pylinkirc import world
|
||||
from pylinkirc.log import log
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
def bind(func):
|
||||
"""
|
||||
Binds an exttarget with the given name.
|
||||
@ -44,10 +41,10 @@ def account(irc, host, uid):
|
||||
homenet, realuid)
|
||||
return False
|
||||
|
||||
slogin = irc.to_lower(str(userobj.services_account))
|
||||
slogin = irc.to_lower(userobj.services_account)
|
||||
|
||||
# Split the given exttarget host into parts, so we know how many to look for.
|
||||
groups = host.split(':')
|
||||
groups = list(map(irc.to_lower, host.split(':')))
|
||||
log.debug('(%s) exttargets.account: groups to match: %s', irc.name, groups)
|
||||
|
||||
if len(groups) == 1:
|
||||
@ -55,12 +52,12 @@ def account(irc, host, uid):
|
||||
return bool(slogin)
|
||||
elif len(groups) == 2:
|
||||
# Second scenario. Return True if the user's account matches the one given.
|
||||
return slogin == irc.to_lower(groups[1]) and homenet == irc.name
|
||||
return slogin == groups[1] and homenet == irc.name
|
||||
else:
|
||||
# Third or fourth scenario. If there are more than 3 groups, the rest are ignored.
|
||||
# In other words: Return True if the user is logged in, the query matches either '*' or the
|
||||
# user's login, and the user is connected on the network requested.
|
||||
return slogin and (irc.to_lower(groups[1]) in ('*', slogin)) and (homenet == groups[2])
|
||||
return slogin and (groups[1] in ('*', slogin)) and (homenet == groups[2])
|
||||
|
||||
@bind
|
||||
def ircop(irc, host, uid):
|
||||
@ -79,8 +76,8 @@ def ircop(irc, host, uid):
|
||||
# 1st scenario.
|
||||
return irc.is_oper(uid)
|
||||
else:
|
||||
# 2nd scenario. Match the opertype glob to the opertype.
|
||||
return irc.match_text(groups[1], irc.users[uid].opertype)
|
||||
# 2nd scenario. Use match_host (ircmatch) to match the opertype glob to the opertype.
|
||||
return irc.match_host(groups[1], irc.users[uid].opertype)
|
||||
|
||||
@bind
|
||||
def server(irc, host, uid):
|
||||
@ -99,7 +96,7 @@ def server(irc, host, uid):
|
||||
sid = irc.get_server(uid)
|
||||
query = groups[1]
|
||||
# Return True if the SID matches the query or the server's name glob matches it.
|
||||
return sid == query or irc.match_text(query, irc.get_friendly_name(sid))
|
||||
return sid == query or irc.match_host(query, irc.get_friendly_name(sid))
|
||||
# $server alone is invalid. Don't match anything.
|
||||
return False
|
||||
|
||||
@ -208,7 +205,7 @@ def realname(irc, host, uid):
|
||||
"""
|
||||
groups = host.split(':')
|
||||
if len(groups) >= 2:
|
||||
return irc.match_text(groups[1], irc.users[uid].realname)
|
||||
return irc.match_host(groups[1], irc.users[uid].realname)
|
||||
|
||||
@bind
|
||||
def service(irc, host, uid):
|
||||
@ -225,5 +222,5 @@ def service(irc, host, uid):
|
||||
groups = host.split(':')
|
||||
|
||||
if len(groups) >= 2:
|
||||
return irc.match_text(groups[1], irc.users[uid].service)
|
||||
return irc.match_host(groups[1], irc.users[uid].service)
|
||||
return True # It *is* a service bot because of the check at the top.
|
||||
|
@ -3,12 +3,9 @@ handlers.py - Implements miscellaneous IRC command handlers (WHOIS, services log
|
||||
"""
|
||||
import time
|
||||
|
||||
from pylinkirc import conf, utils
|
||||
from pylinkirc import utils, conf
|
||||
from pylinkirc.log import log
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
def handle_whois(irc, source, command, args):
|
||||
"""Handle WHOIS queries."""
|
||||
target = args['target']
|
||||
@ -20,7 +17,7 @@ def handle_whois(irc, source, command, args):
|
||||
server = irc.get_server(target)
|
||||
|
||||
if user is None: # User doesn't exist
|
||||
# <- :42X 401 7PYAAAAAB jlu5- :No such nick/channel
|
||||
# <- :42X 401 7PYAAAAAB GL- :No such nick/channel
|
||||
nick = target
|
||||
f(401, source, "%s :No such nick/channel" % nick)
|
||||
else:
|
||||
@ -86,7 +83,7 @@ def handle_whois(irc, source, command, args):
|
||||
n = 'n' if opertype[0].lower() in 'aeiou' else ''
|
||||
|
||||
# Remove the "(on $network)" bit in relay oper types if the target network is the
|
||||
# same - this prevents duplicate text such as "jlu5/ovd is a Network Administrator
|
||||
# same - this prevents duplicate text such as "GL/ovd is a Network Administrator
|
||||
# (on OVERdrive-IRC) on OVERdrive-IRC" from showing.
|
||||
# XXX: does this post-processing really belong here?
|
||||
opertype = opertype.replace(' (on %s)' % irc.get_full_network_name(), '')
|
||||
@ -109,7 +106,7 @@ def handle_whois(irc, source, command, args):
|
||||
# Show botmode info in WHOIS.
|
||||
f(335, source, "%s :is a bot" % nick)
|
||||
|
||||
# :charybdis.midnight.vpn 317 jlu5 jlu5 1946 1499867833 :seconds idle, signon time
|
||||
# :charybdis.midnight.vpn 317 GL GL 1946 1499867833 :seconds idle, signon time
|
||||
if irc.get_service_bot(target) and conf.conf['pylink'].get('whois_show_startup_time', True):
|
||||
f(317, source, "%s 0 %s :seconds idle (placeholder), signon time" % (nick, irc.start_ts))
|
||||
|
||||
|
@ -2,36 +2,21 @@
|
||||
login.py - Implement core login abstraction.
|
||||
"""
|
||||
|
||||
from pylinkirc import conf, utils
|
||||
from pylinkirc import conf, utils, world
|
||||
from pylinkirc.log import log
|
||||
|
||||
__all__ = ['pwd_context', 'check_login', 'verify_hash']
|
||||
|
||||
|
||||
# PyLink's global password context
|
||||
pwd_context = None
|
||||
|
||||
_DEFAULT_CRYPTCONTEXT_SETTINGS = {
|
||||
'schemes': ["pbkdf2_sha256", "sha512_crypt"]
|
||||
}
|
||||
def _make_cryptcontext():
|
||||
try:
|
||||
from passlib.context import CryptContext
|
||||
except ImportError:
|
||||
CryptContext = None
|
||||
log.warning("Hashed passwords are disabled because passlib is not installed. Please install "
|
||||
"it (pip3 install passlib) and rehash for this feature to work.")
|
||||
return
|
||||
"it (pip3 install passlib) and restart for this feature to work.")
|
||||
|
||||
context_settings = conf.conf.get('login', {}).get('cryptcontext_settings') or _DEFAULT_CRYPTCONTEXT_SETTINGS
|
||||
global pwd_context
|
||||
if pwd_context is None:
|
||||
log.debug("Initialized new CryptContext with settings: %s", context_settings)
|
||||
pwd_context = CryptContext(**context_settings)
|
||||
else:
|
||||
log.debug("Updated CryptContext with settings: %s", context_settings)
|
||||
pwd_context.update(**context_settings)
|
||||
|
||||
_make_cryptcontext() # This runs at startup and in rehash (control.py)
|
||||
pwd_context = None
|
||||
if CryptContext:
|
||||
pwd_context = CryptContext(["sha512_crypt", "sha256_crypt"],
|
||||
sha256_crypt__default_rounds=180000,
|
||||
sha512_crypt__default_rounds=90000)
|
||||
|
||||
def _get_account(accountname):
|
||||
"""
|
||||
|
@ -3,16 +3,13 @@ permissions.py - Permissions Abstraction for PyLink IRC Services.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from pylinkirc import conf, utils
|
||||
from pylinkirc.log import log
|
||||
|
||||
__all__ = ['default_permissions', 'add_default_permissions',
|
||||
'remove_default_permissions', 'check_permissions']
|
||||
import threading
|
||||
|
||||
# Global variables: these store mappings of hostmasks/exttargets to lists of permissions each target has.
|
||||
default_permissions = defaultdict(set)
|
||||
|
||||
from pylinkirc import conf, utils
|
||||
from pylinkirc.log import log
|
||||
|
||||
def add_default_permissions(perms):
|
||||
"""Adds default permissions to the index."""
|
||||
@ -35,8 +32,7 @@ def check_permissions(irc, uid, perms, also_show=[]):
|
||||
"""
|
||||
# For old (< 1.1 login blocks):
|
||||
# If the user is logged in, they automatically have all permissions.
|
||||
olduser = conf.conf['login'].get('user')
|
||||
if olduser and irc.match_host('$pylinkacc:%s' % olduser, uid):
|
||||
if irc.match_host('$pylinkacc', uid) and conf.conf['login'].get('user'):
|
||||
log.debug('permissions: overriding permissions check for old-style admin user %s',
|
||||
irc.get_hostmask(uid))
|
||||
return True
|
||||
|
@ -2,12 +2,9 @@
|
||||
service_support.py - Implements handlers for the PyLink ServiceBot API.
|
||||
"""
|
||||
|
||||
from pylinkirc import conf, utils, world
|
||||
from pylinkirc import utils, world, conf
|
||||
from pylinkirc.log import log
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
def spawn_service(irc, source, command, args):
|
||||
"""Handles new service bot introductions."""
|
||||
|
||||
@ -144,8 +141,6 @@ def _services_dynamic_part(irc, channel):
|
||||
if irc.has_cap('visible-state-only'):
|
||||
# No-op on bot-only servers.
|
||||
return
|
||||
if irc.serverdata.get('join_empty_channels', conf.conf['pylink'].get('join_empty_channels', False)):
|
||||
return
|
||||
|
||||
# If all remaining users in the channel are service bots, make them all part.
|
||||
if all(irc.get_service_bot(u) for u in irc.channels[channel].users):
|
||||
|
@ -14,9 +14,9 @@ a:
|
||||
|
||||
### Custom Clientbot Styles
|
||||
|
||||
Custom Clientbot styles can be applied for any of Clientbot's supported events, by defining keys in the format `relay::clientbot_styles::<event name>`. As of PyLink 2.1, you can also override options per network by defining them in the form `servers::<network name>::relay_clientbot_styles::<event names>`
|
||||
Custom Clientbot styles can be applied for any of Clientbot's supported events, by defining keys in the format `relay::clientbot_styles::<event name>`. As of 2.0-beta1, you can also set this per-network by defining options in the form `servers::<network name>::relay_clientbot_styles::<event names>` (Note: defining Clientbot styles locally will override the global `clientbot_styles` block and cause all values under it to be ignored for that network).
|
||||
|
||||
See below for a list of supported events and their default values (as of PyLink 2.1).
|
||||
See below for a list of supported events and their default values (as of 2.0-beta1).
|
||||
|
||||
A common use case for this feature is to turn off or adjust colors/formatting; this is explicitly documented [below](#disabling-colorscontrol-codes).
|
||||
|
||||
@ -44,17 +44,16 @@ To disable relaying for any specific event, set the template string to an empty
|
||||
|
||||
|Event name|Default value|
|
||||
| :---: | :--- |
|
||||
MESSAGE | \x02[$netname]\x02 <$colored\_sender> $text
|
||||
KICK | \x02[$netname]\x02 - $colored_sender$sender\_identhost has kicked $target_nick from $channel ($text)
|
||||
PART | \x02[$netname]\x02 - $colored_sender$sender\_identhost has left $channel ($text)
|
||||
JOIN | \x02[$netname]\x02 - $colored_sender$sender\_identhost has joined $channel
|
||||
NICK | \x02[$netname]\x02 - $colored_sender$sender\_identhost is now known as $newnick
|
||||
QUIT | \x02[$netname]\x02 - $colored_sender$sender\_identhost has quit ($text)
|
||||
ACTION | \x02[$netname]\x02 * $colored\_sender $text
|
||||
NOTICE | \x02[$netname]\x02 - Notice from $colored\_sender: $text
|
||||
SQUIT | \x02[$netname]\x02 - Netsplit lost users: $colored\_nicks
|
||||
SJOIN | \x02[$netname]\x02 - Netjoin gained users: $colored\_nicks
|
||||
MODE | \x02[$netname]\x02 - $colored_sender$sender_identhost sets mode $modes on $channel
|
||||
MESSAGE | \x02[$colored\_netname]\x02 <$colored\_sender> $text
|
||||
KICK | \x02[$colored\_netname]\x02 - $colored_sender$sender\_identhost has kicked $target_nick from $channel ($text)
|
||||
PART | \x02[$colored\_netname]\x02 - $colored_sender$sender\_identhost has left $channel ($text)
|
||||
JOIN | \x02[$colored\_netname]\x02 - $colored_sender$sender\_identhost has joined $channel
|
||||
NICK | \x02[$colored\_netname]\x02 - $colored_sender$sender\_identhost is now known as $newnick
|
||||
QUIT | \x02[$colored\_netname]\x02 - $colored_sender$sender\_identhost has quit ($text)
|
||||
ACTION | \x02[$colored\_netname]\x02 * $colored\_sender $text
|
||||
NOTICE | \x02[$colored\_netname]\x02 - Notice from $colored\_sender: $text
|
||||
SQUIT | \x02[$colored\_netname]\x02 - Netsplit lost users: $colored\_nicks
|
||||
SJOIN | \x02[$colored\_netname]\x02 - Netjoin gained users: $colored\_nicks
|
||||
PM | PM from $sender on $netname: $text
|
||||
PNOTICE | <$sender> $text
|
||||
|
||||
@ -76,7 +75,6 @@ This is a example clientbot_styles config block, which you can copy *into* your
|
||||
JOIN: "[$netname] - $sender$sender_identhost has joined $channel"
|
||||
KICK: "[$netname] - $sender$sender_identhost has kicked $target_nick from $channel ($text)"
|
||||
MESSAGE: "[$netname] <$sender> $text"
|
||||
MODE: "[$netname] - $sender$sender_identhost sets mode $modes on $channel"
|
||||
NICK: "[$netname] - $sender$sender_identhost is now known as $newnick"
|
||||
NOTICE: "[$netname] - Notice from $sender: $text"
|
||||
PART: "[$netname] - $sender$sender_identhost has left $channel ($text)"
|
||||
|
@ -1,5 +1,5 @@
|
||||
This folder contains tables of named modes defined by PyLink modules. The following are HTML versions of the raw .csv data:
|
||||
|
||||
- [Supported named channel modes](https://raw.githack.com/jlu5/PyLink/devel/docs/modelists/channel-modes.html)
|
||||
- [Supported named user modes](https://raw.githack.com/jlu5/PyLink/devel/docs/modelists/user-modes.html)
|
||||
- [Supported extbans](https://raw.githack.com/jlu5/PyLink/devel/docs/modelists/extbans.html)
|
||||
- [Supported named channel modes](https://raw.githack.com/GLolol/PyLink/devel/docs/modelists/channel-modes.html)
|
||||
- [Supported named user modes](https://raw.githack.com/GLolol/PyLink/devel/docs/modelists/user-modes.html)
|
||||
- [Supported extbans](https://raw.githack.com/GLolol/PyLink/devel/docs/modelists/extbans.html)
|
||||
|
@ -1,71 +1,68 @@
|
||||
Channel Mode / IRCd,rfc1459,hybrid,inspircd/insp20,inspircd/insp3,ngircd,p10/ircu,p10/nefarious,p10/snircd,ts6/charybdis,ts6/chatircd,ts6/elemental,ts6/ratbox,unreal
|
||||
admin,,,"a (m_customprefix, m_chanprotect)",a (customprefix),a,,,,,a (when enabled),a (when enabled),,a
|
||||
adminonly,,,,,,,a,,A (ext/chm_adminonly),A (ext/chm_adminonly),A (ext/chm_adminonly.so),,
|
||||
allowinvite,,,A (m_allowinvite),A (allowinvite),,,,,g,g,g,,
|
||||
auditorium,,,u (m_auditorium),u (auditorium),,,,,,,,,
|
||||
autoop,,,w (m_autoop),w (autoop),,,,,,,,,
|
||||
ban,b,b,b,b,b,b,b,b,b,b,b,b,b
|
||||
banexception,,e,e (m_banexception),e (banexception),e,,e,,e,e,e,e,e
|
||||
blockcaps,,,B (m_blockcaps),"B (anticaps, blockcaps)",,,,,,,G (ext/chm_nocaps.so),,
|
||||
blockcolor,,c,c (m_blockcolor),c (blockcolor),,c,c,c,,,,,c (chanmodes/nocolor)
|
||||
blockhighlight,,,V (contrib/m_blockhighlight),V (contrib/blockhighlight),,,,,,,,,
|
||||
censor,,,G (m_censor),G (censor),,,,,,,,,G (chanmodes/censor)
|
||||
delayjoin,,,D (m_delayjoin),D (delayjoin),,D,D,D,,,,,D (chanmodes/delayjoin)
|
||||
delaymsg,,,d (m_delaymsg),d (delaymsg),,,,,,,,,
|
||||
exemptchanops,,,X (m_exemptchanops),X (exemptchanops),,,,,,,,,
|
||||
filter,,,g (m_filter),g (filter),,,,,,,,,(via extban ~T:block:)
|
||||
flood,,,f (m_messageflood),f (messageflood),,,,,,,,,
|
||||
flood_unreal,,,,,,,,,,,,,f (chanmodes/floodprot)
|
||||
freetarget,,,,,,,,,F,F,F,,
|
||||
had_delayjoin,,,,,,d,d,d,,,,,
|
||||
halfop,,h,"h (m_customprefix, m_halfop)",h (customprefix),h,,,,,h (when enabled),h (when enabled),,h
|
||||
hiddenbans,,,,,,,,,,,u,,
|
||||
hidequits,,,,,,,Q,u,,,,,
|
||||
history,,,H (m_chanhistory),H (chanhistory),,,,,,,,,
|
||||
invex,,I,I (m_inviteexception),I (inviteexception),I,,,,I,I,I,I,I
|
||||
inviteonly,i,i,i,i,i,i,i,i,i,i,i,i,i
|
||||
issecure,,,,,,,,,,,,,Z (chanmodes/issecure)
|
||||
joinflood,,,j (m_joinflood),j (joinflood),,,,,j,j,j,,
|
||||
key,k,k,k,k,k,k,k,k,k,k,k,k,k
|
||||
kicknorejoin,,,,,,,,,,,J,,
|
||||
kicknorejoin_insp,,,J (m_kicknorejoin),J (kicknorejoin),,,,,,,,,
|
||||
largebanlist,,,,,,,,,L,L,L,,
|
||||
limit,l,l,l,l,l,l,l,l,l,l,l,l,l
|
||||
moderated,m,m,m,m,m,m,m,m,m,m,m,m,m
|
||||
netadminonly,,,,,,,,,,N (ext/chm_netadminonly),,,
|
||||
nickflood,,,F (m_nickflood),F (nickflood),,,,,,,,,
|
||||
noamsg,,,,,,,T,T,,,,,
|
||||
noctcp,,C,C (m_noctcp),C (noctcp),,C,C,C,C,C,C,,C (chanmodes/noctcp)
|
||||
noextmsg,n,n,n,n,n,n,n,n,n,n,n,n,n
|
||||
noforwards,,,,,,,,,Q,Q,Q,,
|
||||
noinvite,,,,,V,,,,,,,,V (chanmodes/noinvite)
|
||||
nokick,,,Q (m_nokicks),Q (nokicks),Q,,,,,,E,,Q (chanmodes/nokick)
|
||||
noknock,,p*,K (m_knock),K (knock),,,,,p*,p*,p*,p*,K (chanmodes/noknock)
|
||||
nonick,,,N (m_nonicks),N (nonicks),N,,,,,,d,,N (chanmodes/nonickchange)
|
||||
nonotice,,,T (m_nonotice),T (nonotice),,,N,N,T (ext/chm_nonotice),T (ext/chm_nonotice),T,,T (chanmodes/nonotice)
|
||||
official-join,,,Y (m_ojoin),Y (ojoin),,,,,,,,,
|
||||
op,o,o,o,o,o,o,o,o,o,o,o,o,o
|
||||
operonly,,O,O (m_operchans),O (operchans),O,,O,,O (ext/chm_operonly),O (ext/chm_operonly),O (ext/chm_operonly.so),,O (chanmodes/operonly)
|
||||
oplevel_apass,,,,,,A,A,A,,,,,
|
||||
oplevel_upass,,,,,,U,U,U,,,,,
|
||||
opmoderated,,,U (contrib/m_opmoderated),,,,,,z,z,z,,
|
||||
owner,,,"q (m_customprefix, m_chanprotect)",q (customprefix),q,,,,,y (when enabled),y (when enabled),,q
|
||||
paranoia,,p*,,,,,,,,,,,
|
||||
permanent,,,P (m_permchannels),P (permchannels),P,,z,,P,P,P,,P (chanmodes/permanent)
|
||||
private,p,p*,p,p,p,p,p,p,p*,p*,p*,p*,p
|
||||
quiet,,,(via extban m:),(via extban m:),,,(via extban ~q:),,q,q,q,,(via extban ~q:)
|
||||
redirect,,,L (m_redirect),L (redirect),,,L,,f,f,f,,L (chanmodes/link)
|
||||
registered,,r,r (m_services_account),r (services_account),r,R,R,R,,,,,r
|
||||
regmoderated,,M,M (m_services_account),M (services_account),M,,M,M,,,,,M (chanmodes/regonlyspeak)
|
||||
regonly,,R,R (m_services_account),R (services_account),R,r,r,r,r,r,r,r,R (chanmodes/regonly)
|
||||
repeat,,,,,,,,,,,K (ext/chm_norepeat.c),,
|
||||
repeat_insp,,,,E (repeat),,,,,,,,,
|
||||
secret,s,s,s,s,s,s,s,s,s,s,s,s,s
|
||||
sslonly,,S,z (m_sslmodes),z (sslmodes),z,,,,S (ext/chm_sslonly),S (ext/chm_sslonly),S (ext/chm_sslonly.c),S,z (chanmodes/secureonly)
|
||||
stripcolor,,,S (m_stripcolor),S (stripcolor),,,S,,c,c,c,,S (chanmodes/stripcolor)
|
||||
topiclock,t,t,t,t,t,t,t,t,t,t,t,t,t
|
||||
voice,v,v,v,v,v,v,v,v,v,v,v,v,v
|
||||
,,,,,,,,,,,,,
|
||||
----,,,,,,,,,,,,,
|
||||
<b>Note</b>: Channel modes for InspIRCd and UnrealIRCd are automatically negotiated on connect; this may not be a complete list.,,,,,,,,,,,,,
|
||||
"* Mode +p corresponds to both “noknock” and “private” on TS6 IRCds, as well as “paranoia” on hybrid.",,,,,,,,,,,,,
|
||||
Channel Mode / IRCd,rfc1459,hybrid,inspircd,ngircd,p10/ircu,p10/nefarious,p10/snircd,ts6/charybdis,ts6/chatircd,ts6/elemental,ts6/ratbox,unreal
|
||||
admin,,,"a (m_customprefix, m_chanprotect)",a,,,,,a (when enabled),a (when enabled),,a
|
||||
adminonly,,,,,,a,,A (ext/chm_adminonly),A (ext/chm_adminonly),A (ext/chm_adminonly.so),,
|
||||
allowinvite,,,A (m_allowinvite),,,,,g,g,g,,
|
||||
auditorium,,,u (m_auditorium),,,,,,,,,
|
||||
autoop,,,w (m_autoop),,,,,,,,,
|
||||
ban,b,b,b,b,b,b,b,b,b,b,b,b
|
||||
banexception,,e,e (m_banexception),e,,e,,e,e,e,e,e
|
||||
blockcaps,,,B (m_blockcaps),,,,,,,G (ext/chm_nocaps.so),,
|
||||
blockcolor,,c,c (m_blockcolor),,c,c,c,,,,,c
|
||||
blockhighlight,,,V (extras/m_blockhighlight),,,,,,,,,
|
||||
delayjoin,,,D (m_delayjoin),,D,D,D,,,,,D
|
||||
exemptchanops,,,X (m_exemptchanops),,,,,,,,,
|
||||
filter,,,g (m_filter),,,,,,,,,
|
||||
flood,,,f (m_messageflood),,,,,,,,,
|
||||
flood_unreal,,,,,,,,,,,,f
|
||||
freetarget,,,,,,,,F,F,F,,
|
||||
had_delayjoin,,,,,d,d,d,,,,,
|
||||
halfop,,h,"h (m_customprefix, m_halfop)",h,,,,,h (when enabled),h (when enabled),,h
|
||||
hiddenbans,,,,,,,,,,u,,
|
||||
hidequits,,,,,,Q,u,,,,,
|
||||
history,,,H (m_chanhistory),,,,,,,,,
|
||||
invex,,I,I (m_inviteexception),I,,,,I,I,I,I,I
|
||||
inviteonly,i,i,i,i,i,i,i,i,i,i,i,i
|
||||
issecure,,,,,,,,,,,,Z
|
||||
joinflood,,,j (m_joinflood),,,,,j,j,j,,
|
||||
key,k,k,k,k,k,k,k,k,k,k,k,k
|
||||
kicknorejoin,,,,,,,,,,J,,
|
||||
kicknorejoin_insp,,,J (m_kicknorejoin),,,,,,,,,
|
||||
largebanlist,,,,,,,,L,L,L,,
|
||||
limit,l,l,l,l,l,l,l,l,l,l,l,l
|
||||
moderated,m,m,m,m,m,m,m,m,m,m,m,m
|
||||
netadminonly,,,,,,,,,N (ext/chm_netadminonly),,,
|
||||
nickflood,,,F (m_nickflood),,,,,,,,,
|
||||
noamsg,,,,,,T,T,,,,,
|
||||
noctcp,,C,C (m_noctcp),,C,C,C,C,C,C,,C
|
||||
noextmsg,n,n,n,n,,n,,n,n,n,n,n
|
||||
noforwards,,,,,,,,Q,Q,Q,,
|
||||
noinvite,,,,V,,,,,,,,V
|
||||
nokick,,,Q (m_nokicks),Q,,,,,,E,,Q
|
||||
noknock,,p*,K (m_knock),,,,,p*,p*,p*,p*,K
|
||||
nonick,,,N (m_nonicks),N,,,,,,d,,N
|
||||
nonotice,,,T (m_nonotice),,,N,N,T (ext/chm_nonotice),T (ext/chm_nonotice),T,,T
|
||||
official-join,,,Y (m_ojoin),,,,,,,,,
|
||||
op,o,o,o,o,o,o,o,o,o,o,o,o
|
||||
operonly,,O,O (m_operchans),O,,O,,O (ext/chm_operonly),O (ext/chm_operonly),O (ext/chm_operonly.so),,O
|
||||
oplevel_apass,,,,,A,A,A,,,,,
|
||||
oplevel_upass,,,,,U,U,U,,,,,
|
||||
opmoderated,,,U (extras/m_opmoderated),,,,,z,z,z,,
|
||||
owner,,,"q (m_customprefix, m_chanprotect)",q,,,,,y (when enabled),y (when enabled),,q
|
||||
paranoia,,p*,,,,,,,,,,
|
||||
permanent,,,P (m_permchannels),P,,z,,P,P,P,,P
|
||||
private,p,p*,p,p,p,p,p,p*,p*,p*,p*,p
|
||||
quiet,,,(via extban m:),,,(via extban ~q:),,q,q,q,,(via extban ~q:)
|
||||
redirect,,,L (m_redirect),,,L,,f,f,f,,L
|
||||
registered,,r,r (m_services_account),r,R,R,R,,,,,r
|
||||
regmoderated,,M,M (m_services_account),M,,M,M,,,,,M
|
||||
regonly,,R,R (m_services_account),R,r,r,r,r,r,r,r,R
|
||||
repeat,,,,,,,,,,K (ext/chm_norepeat.c),,
|
||||
repeat_insp,,,E (m_repeat),,,,,,,,,
|
||||
secret,s,s,s,s,s,s,s,s,s,s,s,s
|
||||
sslonly,,S,z (m_sslmodes),z,,,,S (ext/chm_sslonly),S (ext/chm_sslonly),S (ext/chm_sslonly.c),S,z
|
||||
stripcolor,,,S (m_stripcolor),,,S,,c,c,c,,S
|
||||
topiclock,t,t,t,t,t,t,t,t,t,t,t,t
|
||||
voice,v,v,v,v,v,v,v,v,v,v,v,v
|
||||
,,,,,,,,,,,,
|
||||
----,,,,,,,,,,,,
|
||||
"* Mode +p corresponds to both “noknock” and “private” on TS6 IRCds, as well as “paranoia” on hybrid.",,,,,,,,,,,,
|
||||
|
|
@ -44,7 +44,7 @@ td:first-child, th[scope="row"] {
|
||||
}
|
||||
|
||||
.tablecell-planned, .tablecell-yes2 {
|
||||
background-color: #B1FCDE
|
||||
background-color: #92E8DF
|
||||
}
|
||||
|
||||
.tablecell-partial {
|
||||
@ -55,6 +55,17 @@ td:first-child, th[scope="row"] {
|
||||
background-color: #DCB1FC
|
||||
}
|
||||
|
||||
.tablecell-caveats {
|
||||
background-color: #F0C884
|
||||
}
|
||||
|
||||
.tablecell-caveats2 {
|
||||
background-color: #ED9A80
|
||||
}
|
||||
|
||||
.tablecell-no-padding {
|
||||
padding: initial;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
@ -64,8 +75,7 @@ td:first-child, th[scope="row"] {
|
||||
<th scope="col">Channel Mode / IRCd</th>
|
||||
<th scope="col">rfc1459</th>
|
||||
<th scope="col">hybrid</th>
|
||||
<th scope="col">inspircd/insp20</th>
|
||||
<th scope="col">inspircd/insp3</th>
|
||||
<th scope="col">inspircd</th>
|
||||
<th scope="col">ngircd</th>
|
||||
<th scope="col">p10/ircu</th>
|
||||
<th scope="col">p10/nefarious</th>
|
||||
@ -78,203 +88,197 @@ td:first-child, th[scope="row"] {
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">admin</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+a<br><span class="note">(m_customprefix, m_chanprotect)</span></td><td class="tablecell-yes2">+a<br><span class="note">(customprefix)</span></td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+a<br><span class="note">(when enabled)</span></td><td class="tablecell-yes2">+a<br><span class="note">(when enabled)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+a<br><span class="note">(m_customprefix, m_chanprotect)</span></td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+a<br><span class="note">(when enabled)</span></td><td class="tablecell-special">+a<br><span class="note">(when enabled)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">adminonly</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+A<br><span class="note">(ext/chm_adminonly)</span></td><td class="tablecell-yes2">+A<br><span class="note">(ext/chm_adminonly)</span></td><td class="tablecell-yes2">+A<br><span class="note">(ext/chm_adminonly.so)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+A<br><span class="note">(ext/chm_adminonly)</span></td><td class="tablecell-special">+A<br><span class="note">(ext/chm_adminonly)</span></td><td class="tablecell-special">+A<br><span class="note">(ext/chm_adminonly.so)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">allowinvite</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+A<br><span class="note">(m_allowinvite)</span></td><td class="tablecell-yes2">+A<br><span class="note">(allowinvite)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+A<br><span class="note">(m_allowinvite)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">auditorium</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+u<br><span class="note">(m_auditorium)</span></td><td class="tablecell-yes2">+u<br><span class="note">(auditorium)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+u<br><span class="note">(m_auditorium)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">autoop</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+w<br><span class="note">(m_autoop)</span></td><td class="tablecell-yes2">+w<br><span class="note">(autoop)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+w<br><span class="note">(m_autoop)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban</th>
|
||||
<td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td></tr>
|
||||
<td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+b</td></tr>
|
||||
<tr>
|
||||
<th scope="row">banexception</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-yes2">+e<br><span class="note">(m_banexception)</span></td><td class="tablecell-yes2">+e<br><span class="note">(banexception)</span></td><td class="tablecell-yes">+e</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-yes">+e</td><td class="tablecell-yes">+e</td><td class="tablecell-yes">+e</td><td class="tablecell-yes">+e</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-special">+e<br><span class="note">(m_banexception)</span></td><td class="tablecell-yes">+e</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+e</td><td class="tablecell-yes">+e</td><td class="tablecell-yes">+e</td><td class="tablecell-yes">+e</td><td class="tablecell-yes">+e</td></tr>
|
||||
<tr>
|
||||
<th scope="row">blockcaps</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+B<br><span class="note">(m_blockcaps)</span></td><td class="tablecell-yes2">+B<br><span class="note">(anticaps, blockcaps)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+G<br><span class="note">(ext/chm_nocaps.so)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+B<br><span class="note">(m_blockcaps)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+G<br><span class="note">(ext/chm_nocaps.so)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">blockcolor</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-yes2">+c<br><span class="note">(m_blockcolor)</span></td><td class="tablecell-yes2">+c<br><span class="note">(blockcolor)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+c<br><span class="note">(chanmodes/nocolor)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-special">+c<br><span class="note">(m_blockcolor)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td></tr>
|
||||
<tr>
|
||||
<th scope="row">blockhighlight</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+V<br><span class="note">(contrib/m_blockhighlight)</span></td><td class="tablecell-yes2">+V<br><span class="note">(contrib/blockhighlight)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">censor</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+G<br><span class="note">(m_censor)</span></td><td class="tablecell-yes2">+G<br><span class="note">(censor)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+G<br><span class="note">(chanmodes/censor)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+V<br><span class="note">(extras/m_blockhighlight)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">delayjoin</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+D<br><span class="note">(m_delayjoin)</span></td><td class="tablecell-yes2">+D<br><span class="note">(delayjoin)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+D<br><span class="note">(chanmodes/delayjoin)</span></td></tr>
|
||||
<tr>
|
||||
<th scope="row">delaymsg</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+d<br><span class="note">(m_delaymsg)</span></td><td class="tablecell-yes2">+d<br><span class="note">(delaymsg)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+D<br><span class="note">(m_delayjoin)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td></tr>
|
||||
<tr>
|
||||
<th scope="row">exemptchanops</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+X<br><span class="note">(m_exemptchanops)</span></td><td class="tablecell-yes2">+X<br><span class="note">(exemptchanops)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+X<br><span class="note">(m_exemptchanops)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">filter</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+g<br><span class="note">(m_filter)</span></td><td class="tablecell-yes2">+g<br><span class="note">(filter)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-partial">(via extban ~T:block:)</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+g<br><span class="note">(m_filter)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">flood</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+f<br><span class="note">(m_messageflood)</span></td><td class="tablecell-yes2">+f<br><span class="note">(messageflood)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+f<br><span class="note">(m_messageflood)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">flood_unreal</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+f<br><span class="note">(chanmodes/floodprot)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td></tr>
|
||||
<tr>
|
||||
<th scope="row">freetarget</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+F</td><td class="tablecell-yes">+F</td><td class="tablecell-yes">+F</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+F</td><td class="tablecell-yes">+F</td><td class="tablecell-yes">+F</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">had_delayjoin</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">halfop</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td><td class="tablecell-yes2">+h<br><span class="note">(m_customprefix, m_halfop)</span></td><td class="tablecell-yes2">+h<br><span class="note">(customprefix)</span></td><td class="tablecell-yes">+h</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+h<br><span class="note">(when enabled)</span></td><td class="tablecell-yes2">+h<br><span class="note">(when enabled)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td><td class="tablecell-special">+h<br><span class="note">(m_customprefix, m_halfop)</span></td><td class="tablecell-yes">+h</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+h<br><span class="note">(when enabled)</span></td><td class="tablecell-special">+h<br><span class="note">(when enabled)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td></tr>
|
||||
<tr>
|
||||
<th scope="row">hiddenbans</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">hidequits</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">history</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+H<br><span class="note">(m_chanhistory)</span></td><td class="tablecell-yes2">+H<br><span class="note">(chanhistory)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+H<br><span class="note">(m_chanhistory)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">invex</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-yes2">+I<br><span class="note">(m_inviteexception)</span></td><td class="tablecell-yes2">+I<br><span class="note">(inviteexception)</span></td><td class="tablecell-yes">+I</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-special">+I<br><span class="note">(m_inviteexception)</span></td><td class="tablecell-yes">+I</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td></tr>
|
||||
<tr>
|
||||
<th scope="row">inviteonly</th>
|
||||
<td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td></tr>
|
||||
<td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td></tr>
|
||||
<tr>
|
||||
<th scope="row">issecure</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+Z<br><span class="note">(chanmodes/issecure)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Z</td></tr>
|
||||
<tr>
|
||||
<th scope="row">joinflood</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+j<br><span class="note">(m_joinflood)</span></td><td class="tablecell-yes2">+j<br><span class="note">(joinflood)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+j</td><td class="tablecell-yes">+j</td><td class="tablecell-yes">+j</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+j<br><span class="note">(m_joinflood)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+j</td><td class="tablecell-yes">+j</td><td class="tablecell-yes">+j</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">key</th>
|
||||
<td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td></tr>
|
||||
<td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td></tr>
|
||||
<tr>
|
||||
<th scope="row">kicknorejoin</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+J</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+J</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">kicknorejoin_insp</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+J<br><span class="note">(m_kicknorejoin)</span></td><td class="tablecell-yes2">+J<br><span class="note">(kicknorejoin)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+J<br><span class="note">(m_kicknorejoin)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">largebanlist</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+L</td><td class="tablecell-yes">+L</td><td class="tablecell-yes">+L</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+L</td><td class="tablecell-yes">+L</td><td class="tablecell-yes">+L</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">limit</th>
|
||||
<td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td></tr>
|
||||
<td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td><td class="tablecell-yes">+l</td></tr>
|
||||
<tr>
|
||||
<th scope="row">moderated</th>
|
||||
<td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td></tr>
|
||||
<td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td><td class="tablecell-yes">+m</td></tr>
|
||||
<tr>
|
||||
<th scope="row">netadminonly</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+N<br><span class="note">(ext/chm_netadminonly)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+N<br><span class="note">(ext/chm_netadminonly)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">nickflood</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+F<br><span class="note">(m_nickflood)</span></td><td class="tablecell-yes2">+F<br><span class="note">(nickflood)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+F<br><span class="note">(m_nickflood)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noamsg</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+T</td><td class="tablecell-yes">+T</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+T</td><td class="tablecell-yes">+T</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noctcp</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-yes2">+C<br><span class="note">(m_noctcp)</span></td><td class="tablecell-yes2">+C<br><span class="note">(noctcp)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+C<br><span class="note">(chanmodes/noctcp)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-special">+C<br><span class="note">(m_noctcp)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noextmsg</th>
|
||||
<td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td></tr>
|
||||
<td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+n</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noforwards</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noinvite</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+V</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+V<br><span class="note">(chanmodes/noinvite)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+V</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+V</td></tr>
|
||||
<tr>
|
||||
<th scope="row">nokick</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+Q<br><span class="note">(m_nokicks)</span></td><td class="tablecell-yes2">+Q<br><span class="note">(nokicks)</span></td><td class="tablecell-yes">+Q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+E</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+Q<br><span class="note">(chanmodes/nokick)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+Q<br><span class="note">(m_nokicks)</span></td><td class="tablecell-yes">+Q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+E</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Q</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noknock</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-special">+p*</td><td class="tablecell-yes2">+K<br><span class="note">(m_knock)</span></td><td class="tablecell-yes2">+K<br><span class="note">(knock)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+p*</td><td class="tablecell-special">+p*</td><td class="tablecell-special">+p*</td><td class="tablecell-special">+p*</td><td class="tablecell-yes2">+K<br><span class="note">(chanmodes/noknock)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+p*</td><td class="tablecell-special">+K<br><span class="note">(m_knock)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes">+K</td></tr>
|
||||
<tr>
|
||||
<th scope="row">nonick</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+N<br><span class="note">(m_nonicks)</span></td><td class="tablecell-yes2">+N<br><span class="note">(nonicks)</span></td><td class="tablecell-yes">+N</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+N<br><span class="note">(chanmodes/nonickchange)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+N<br><span class="note">(m_nonicks)</span></td><td class="tablecell-yes">+N</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+N</td></tr>
|
||||
<tr>
|
||||
<th scope="row">nonotice</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+T<br><span class="note">(m_nonotice)</span></td><td class="tablecell-yes2">+T<br><span class="note">(nonotice)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+N</td><td class="tablecell-yes">+N</td><td class="tablecell-yes2">+T<br><span class="note">(ext/chm_nonotice)</span></td><td class="tablecell-yes2">+T<br><span class="note">(ext/chm_nonotice)</span></td><td class="tablecell-yes">+T</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+T<br><span class="note">(chanmodes/nonotice)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+T<br><span class="note">(m_nonotice)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+N</td><td class="tablecell-yes">+N</td><td class="tablecell-special">+T<br><span class="note">(ext/chm_nonotice)</span></td><td class="tablecell-special">+T<br><span class="note">(ext/chm_nonotice)</span></td><td class="tablecell-yes">+T</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+T</td></tr>
|
||||
<tr>
|
||||
<th scope="row">official-join</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+Y<br><span class="note">(m_ojoin)</span></td><td class="tablecell-yes2">+Y<br><span class="note">(ojoin)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+Y<br><span class="note">(m_ojoin)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">op</th>
|
||||
<td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td></tr>
|
||||
<td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td><td class="tablecell-yes">+o</td></tr>
|
||||
<tr>
|
||||
<th scope="row">operonly</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+O</td><td class="tablecell-yes2">+O<br><span class="note">(m_operchans)</span></td><td class="tablecell-yes2">+O<br><span class="note">(operchans)</span></td><td class="tablecell-yes">+O</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+O</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+O<br><span class="note">(ext/chm_operonly)</span></td><td class="tablecell-yes2">+O<br><span class="note">(ext/chm_operonly)</span></td><td class="tablecell-yes2">+O<br><span class="note">(ext/chm_operonly.so)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+O<br><span class="note">(chanmodes/operonly)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+O</td><td class="tablecell-special">+O<br><span class="note">(m_operchans)</span></td><td class="tablecell-yes">+O</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+O</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+O<br><span class="note">(ext/chm_operonly)</span></td><td class="tablecell-special">+O<br><span class="note">(ext/chm_operonly)</span></td><td class="tablecell-special">+O<br><span class="note">(ext/chm_operonly.so)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+O</td></tr>
|
||||
<tr>
|
||||
<th scope="row">oplevel_apass</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+A</td><td class="tablecell-yes">+A</td><td class="tablecell-yes">+A</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+A</td><td class="tablecell-yes">+A</td><td class="tablecell-yes">+A</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">oplevel_upass</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+U</td><td class="tablecell-yes">+U</td><td class="tablecell-yes">+U</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+U</td><td class="tablecell-yes">+U</td><td class="tablecell-yes">+U</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">opmoderated</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+U<br><span class="note">(contrib/m_opmoderated)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td><td class="tablecell-yes">+z</td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+U<br><span class="note">(extras/m_opmoderated)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td><td class="tablecell-yes">+z</td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">owner</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+q<br><span class="note">(m_customprefix, m_chanprotect)</span></td><td class="tablecell-yes2">+q<br><span class="note">(customprefix)</span></td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+y<br><span class="note">(when enabled)</span></td><td class="tablecell-yes2">+y<br><span class="note">(when enabled)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+q<br><span class="note">(m_customprefix, m_chanprotect)</span></td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+y<br><span class="note">(when enabled)</span></td><td class="tablecell-special">+y<br><span class="note">(when enabled)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td></tr>
|
||||
<tr>
|
||||
<th scope="row">paranoia</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-special">+p*</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+p*</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">permanent</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+P<br><span class="note">(m_permchannels)</span></td><td class="tablecell-yes2">+P<br><span class="note">(permchannels)</span></td><td class="tablecell-yes">+P</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+P</td><td class="tablecell-yes">+P</td><td class="tablecell-yes">+P</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+P<br><span class="note">(chanmodes/permanent)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+P<br><span class="note">(m_permchannels)</span></td><td class="tablecell-yes">+P</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+P</td><td class="tablecell-yes">+P</td><td class="tablecell-yes">+P</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+P</td></tr>
|
||||
<tr>
|
||||
<th scope="row">private</th>
|
||||
<td class="tablecell-yes">+p</td><td class="tablecell-special">+p*</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-special">+p*</td><td class="tablecell-special">+p*</td><td class="tablecell-special">+p*</td><td class="tablecell-special">+p*</td><td class="tablecell-yes">+p</td></tr>
|
||||
<td class="tablecell-yes">+p</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes2">+p*</td><td class="tablecell-yes">+p</td></tr>
|
||||
<tr>
|
||||
<th scope="row">quiet</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-partial">(via extban m:)</td><td class="tablecell-partial">(via extban m:)</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-partial">(via extban ~q:)</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td><td class="tablecell-yes">+q</td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-partial">(via extban ~q:)</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-partial">(via extban m:)</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-partial">(via extban ~q:)</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td><td class="tablecell-yes">+q</td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-partial">(via extban ~q:)</td></tr>
|
||||
<tr>
|
||||
<th scope="row">redirect</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+L<br><span class="note">(m_redirect)</span></td><td class="tablecell-yes2">+L<br><span class="note">(redirect)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+L</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td><td class="tablecell-yes">+f</td><td class="tablecell-yes">+f</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+L<br><span class="note">(chanmodes/link)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+L<br><span class="note">(m_redirect)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+L</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td><td class="tablecell-yes">+f</td><td class="tablecell-yes">+f</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+L</td></tr>
|
||||
<tr>
|
||||
<th scope="row">registered</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-yes2">+r<br><span class="note">(m_services_account)</span></td><td class="tablecell-yes2">+r<br><span class="note">(services_account)</span></td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-special">+r<br><span class="note">(m_services_account)</span></td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td></tr>
|
||||
<tr>
|
||||
<th scope="row">regmoderated</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+M</td><td class="tablecell-yes2">+M<br><span class="note">(m_services_account)</span></td><td class="tablecell-yes2">+M<br><span class="note">(services_account)</span></td><td class="tablecell-yes">+M</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+M</td><td class="tablecell-yes">+M</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+M<br><span class="note">(chanmodes/regonlyspeak)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+M</td><td class="tablecell-special">+M<br><span class="note">(m_services_account)</span></td><td class="tablecell-yes">+M</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+M</td><td class="tablecell-yes">+M</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+M</td></tr>
|
||||
<tr>
|
||||
<th scope="row">regonly</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td><td class="tablecell-yes2">+R<br><span class="note">(m_services_account)</span></td><td class="tablecell-yes2">+R<br><span class="note">(services_account)</span></td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes2">+R<br><span class="note">(chanmodes/regonly)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td><td class="tablecell-special">+R<br><span class="note">(m_services_account)</span></td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+R</td></tr>
|
||||
<tr>
|
||||
<th scope="row">repeat</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+K<br><span class="note">(ext/chm_norepeat.c)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+K<br><span class="note">(ext/chm_norepeat.c)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">repeat_insp</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+E<br><span class="note">(repeat)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+E<br><span class="note">(m_repeat)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">secret</th>
|
||||
<td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td></tr>
|
||||
<td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sslonly</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-yes2">+z<br><span class="note">(m_sslmodes)</span></td><td class="tablecell-yes2">+z<br><span class="note">(sslmodes)</span></td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+S<br><span class="note">(ext/chm_sslonly)</span></td><td class="tablecell-yes2">+S<br><span class="note">(ext/chm_sslonly)</span></td><td class="tablecell-yes2">+S<br><span class="note">(ext/chm_sslonly.c)</span></td><td class="tablecell-yes">+S</td><td class="tablecell-yes2">+z<br><span class="note">(chanmodes/secureonly)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-special">+z<br><span class="note">(m_sslmodes)</span></td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+S<br><span class="note">(ext/chm_sslonly)</span></td><td class="tablecell-special">+S<br><span class="note">(ext/chm_sslonly)</span></td><td class="tablecell-special">+S<br><span class="note">(ext/chm_sslonly.c)</span></td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+z</td></tr>
|
||||
<tr>
|
||||
<th scope="row">stripcolor</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+S<br><span class="note">(m_stripcolor)</span></td><td class="tablecell-yes2">+S<br><span class="note">(stripcolor)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+S<br><span class="note">(chanmodes/stripcolor)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-special">+S<br><span class="note">(m_stripcolor)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+c</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td></tr>
|
||||
<tr>
|
||||
<th scope="row">topiclock</th>
|
||||
<td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td></tr>
|
||||
<td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td><td class="tablecell-yes">+t</td></tr>
|
||||
<tr>
|
||||
<th scope="row">voice</th>
|
||||
<td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td></tr>
|
||||
<p><b>Note</b>: Channel modes for InspIRCd and UnrealIRCd are automatically negotiated on connect; this may not be a complete list.</p><p>* Mode +p corresponds to both “noknock” and “private” on TS6 IRCds, as well as “paranoia” on hybrid.</p>
|
||||
<td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td><td class="tablecell-yes">+v</td></tr>
|
||||
<p>* Mode +p corresponds to both “noknock” and “private” on TS6 IRCds, as well as “paranoia” on hybrid.</p>
|
||||
|
||||
</table>
|
||||
</body>
|
||||
|
@ -16,7 +16,7 @@ ban_noctcp,C:,,,
|
||||
ban_nojoins,,,,~j:
|
||||
ban_nokicks,Q:,,,
|
||||
ban_nonick,N:,~n:,,~n:
|
||||
ban_nonotice,T:,,,~m:notice: (+e only)
|
||||
ban_nonotice,T:,,,
|
||||
ban_not_account,,,$~a:,
|
||||
ban_not_banshare,,,$~j:,
|
||||
ban_not_extgecos,,,$~x:,
|
||||
@ -29,12 +29,7 @@ ban_opertype,O:,,,~O:
|
||||
ban_partmsgs,p:,,,
|
||||
ban_realname,r:,~r:,$r:,~r:
|
||||
ban_server,s:,,$s:,
|
||||
ban_stripcolor,S:,,,~m:color: (+e only)
|
||||
ban_unregistered,,,$~a,
|
||||
ban_stripcolor,S:,,,
|
||||
ban_unregistered_mark,,~M:,,
|
||||
ban_unregistered_matching,U:,,,
|
||||
msgbypass_external,,,,~m:external:
|
||||
msgbypass_censor,,,,~m:censor:
|
||||
msgbypass_moderated,,,,~m:moderated:
|
||||
quiet,m:,~q:,(via cmode +q),~q:
|
||||
timedban_unreal,,,,~t:
|
||||
|
|
@ -44,7 +44,7 @@ td:first-child, th[scope="row"] {
|
||||
}
|
||||
|
||||
.tablecell-planned, .tablecell-yes2 {
|
||||
background-color: #B1FCDE
|
||||
background-color: #92E8DF
|
||||
}
|
||||
|
||||
.tablecell-partial {
|
||||
@ -55,6 +55,17 @@ td:first-child, th[scope="row"] {
|
||||
background-color: #DCB1FC
|
||||
}
|
||||
|
||||
.tablecell-caveats {
|
||||
background-color: #F0C884
|
||||
}
|
||||
|
||||
.tablecell-caveats2 {
|
||||
background-color: #ED9A80
|
||||
}
|
||||
|
||||
.tablecell-no-padding {
|
||||
padding: initial;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
@ -120,7 +131,7 @@ td:first-child, th[scope="row"] {
|
||||
<td class="tablecell-yes">N:</td><td class="tablecell-yes">~n:</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~n:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_nonotice</th>
|
||||
<td class="tablecell-yes">T:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">~m:notice:<br><span class="note">(+e only)</span></td></tr>
|
||||
<td class="tablecell-yes">T:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_not_account</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~a:</td><td class="tablecell-na note">n/a</td></tr>
|
||||
@ -159,10 +170,7 @@ td:first-child, th[scope="row"] {
|
||||
<td class="tablecell-yes">s:</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$s:</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_stripcolor</th>
|
||||
<td class="tablecell-yes">S:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">~m:color:<br><span class="note">(+e only)</span></td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_unregistered</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">$~a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-yes">S:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ban_unregistered_mark</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">~M:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
@ -170,20 +178,8 @@ td:first-child, th[scope="row"] {
|
||||
<th scope="row">ban_unregistered_matching</th>
|
||||
<td class="tablecell-yes">U:</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">msgbypass_external</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~m:external:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">msgbypass_censor</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~m:censor:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">msgbypass_moderated</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~m:moderated:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">quiet</th>
|
||||
<td class="tablecell-yes">m:</td><td class="tablecell-yes">~q:</td><td class="tablecell-partial">(via cmode +q)</td><td class="tablecell-yes">~q:</td></tr>
|
||||
<tr>
|
||||
<th scope="row">timedban_unreal</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">~t:</td></tr>
|
||||
|
||||
|
||||
</table>
|
||||
|
@ -3,9 +3,9 @@
|
||||
Generates HTML versions of the mode list .csv definitions.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import os
|
||||
import os.path
|
||||
import csv
|
||||
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
|
||||
@ -31,12 +31,12 @@ def _format(articlename, text):
|
||||
text, note = text.split(' ', 1)
|
||||
except ValueError:
|
||||
if text.endswith('*'):
|
||||
text = '<td class="tablecell-special">%s</td>' % text
|
||||
text = '<td class="tablecell-yes2">%s</td>' % text
|
||||
else:
|
||||
text = '<td class="tablecell-yes">%s</td>' % text
|
||||
else:
|
||||
text = '%s<br><span class="note">%s</span>' % (text, note)
|
||||
text = '<td class="tablecell-yes2">%s</td>' % text
|
||||
text = '<td class="tablecell-special">%s</td>' % text
|
||||
else:
|
||||
text = '<td class="tablecell-na note">n/a</td>'
|
||||
return text
|
||||
@ -95,7 +95,7 @@ td:first-child, th[scope="row"] {
|
||||
}
|
||||
|
||||
.tablecell-planned, .tablecell-yes2 {
|
||||
background-color: #B1FCDE
|
||||
background-color: #92E8DF
|
||||
}
|
||||
|
||||
.tablecell-partial {
|
||||
@ -106,6 +106,17 @@ td:first-child, th[scope="row"] {
|
||||
background-color: #DCB1FC
|
||||
}
|
||||
|
||||
.tablecell-caveats {
|
||||
background-color: #F0C884
|
||||
}
|
||||
|
||||
.tablecell-caveats2 {
|
||||
background-color: #ED9A80
|
||||
}
|
||||
|
||||
.tablecell-no-padding {
|
||||
padding: initial;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
@ -1,39 +1,39 @@
|
||||
User Mode / IRCd,RFC 1459,hybrid,inspircd,ngircd,p10/ircu,p10/nefarious,p10/snircd,ts6/charybdis,ts6/chatircd,ts6/elemental,ts6/ratbox,unreal
|
||||
admin,,a,,,,a,,a,a,a,a,
|
||||
away,,,,a,,,,,,,,
|
||||
bot,,,B (botmode),B,,B,,,B,B,,B (usermodes/bot)
|
||||
callerid,,g,g (callerid),,,,,g,g,g,g,
|
||||
censor,,,,,,,,,,,,G (usermodes/censor)
|
||||
cloak,,x,x (cloaking),x,x,x,x,x,x,x,,x
|
||||
bot,,,B,B,,B,,,B,B,,B
|
||||
callerid,,g,g,,,,,g,g,g,g,
|
||||
callerid_sslonly,,,,,,,,,t,,,
|
||||
cloak,,x,x,x,x,x,x,x,x,x,,x
|
||||
cloak_fakehost,,,,,,f,,,,,,
|
||||
cloak_hashedhost,,,,,,C,,,,,,
|
||||
cloak_hashedip,,,,,,c,,,,,,
|
||||
cloak_sethost,,,,,,h,h,,,,,
|
||||
deaf,,D,d (deaf),,d,d,d,D,D,D,D,d
|
||||
deaf_commonchan,,G,c (commonchans),C,,q,,,,,,
|
||||
deaf,,D,d,b,d,d,d,D,D,D,D,d
|
||||
deaf_commonchan,,G,c,C,,q,,,,,,
|
||||
debug,,d,,,,,,,,,,
|
||||
filter,,,,,,,,,,,,G
|
||||
floodexempt,,,,F,,,,,,,,
|
||||
helpop,,,h (helpop),,,,,,,,,
|
||||
hidechans,,p,I (hidechans),I,,n,n,,,I,,p (usermodes/privacy)
|
||||
floodexempt,,,,f,,,,,,,,
|
||||
helpop,,,h,,,,,,,,,
|
||||
hidechans,,p,I,I,,n,n,,,I,,p
|
||||
hideidle,,q,,,,I,I,,,,,I
|
||||
hideoper,,H,H (hideoper),,,H,,,,,,H
|
||||
hideoper,,H,H,,,H,,,,,,H
|
||||
invisible,i,i,i,i,i,i,i,i,i,i,i,i
|
||||
locops,,l,,,O,O,O,l,l,l,l,
|
||||
netadmin,,,,,,,,,N,,,
|
||||
noctcp,,,,,,,,,,C,,T (usermodes/noctcp)
|
||||
noforward,,,L (redirect),,,L,,Q,Q,Q,,
|
||||
noctcp,,,,,,,,,,C,,T
|
||||
noforward,,,,,,L,,Q,Q,Q,,
|
||||
noinvite,,,,,,,,,,V,,
|
||||
oper,o,o,o,o,o,o,o,o,o,o,o,o
|
||||
operwall,,,,,,,,z,z,z,z,
|
||||
override,,,,,,X,X,p,p,p,,
|
||||
privdeaf,,,,b,,D,,,,,,D (usermodes/privdeaf)
|
||||
protected,,,,,,,,,,,,q (usermodes/nokick)
|
||||
regdeaf,,R,R (services_account),,,R,R,R,R,R,,R (usermodes/regonlymsg)
|
||||
registered,,r,r (services_account),R,r,r,r,,,,,r
|
||||
privdeaf,,,,,,D,,,,,,
|
||||
protected,,,,,,,,,,,,q
|
||||
regdeaf,,R,R,,,R,R,R,R,R,,R
|
||||
registered,,r,r,R,r,r,r,,,,,r
|
||||
restricted,,,,r,,,,,,,,
|
||||
servprotect,,,k (servprotect),q,k,k,k,S,S,S,S,S (usermodes/servicebot)
|
||||
showwhois,,,W (showwhois),,,W,,,,,,W (usermodes/showwhois)
|
||||
servprotect,,,k,q,k,k,k,S,S,S,S,S
|
||||
showwhois,,,W,,,W,,,,,,W
|
||||
sno_badclientconnections,,u,,,,,,,,,u,
|
||||
sno_botfloods,,b,,,,,,,,,b,
|
||||
sno_clientconnections,,c,,c,,,,,,,c,
|
||||
@ -46,10 +46,9 @@ sno_remoteclientconnections,,F,,,,,,,,,,
|
||||
sno_serverconnects,,e,,,,,,,,,x,
|
||||
sno_skill,,k,,,,,,,,,k,
|
||||
sno_stats,,y,,,,,,,,,y,
|
||||
snomask,s,s,s,s,s,s,s,s,s,s,s,s
|
||||
snomask,s,s,s,s,,s,,s,s,s,s,s
|
||||
ssl,,S,,,,z,,,,,,z
|
||||
sslonlymsg,,,,,,,,,t,,,Z (usermodes/secureonlymsg)
|
||||
stripcolor,,,S (stripcolor),,,,,,,,,
|
||||
stripcolor,,,S,,,,,,,,,
|
||||
vhost,,,,,,,,,,,,t
|
||||
wallops,w,w,w,w,w,w,w,w,w,w,w,w
|
||||
wallops,w,w,w,w,,w,,w,w,w,w,w
|
||||
webirc,,W,,,,,,,,,,
|
||||
|
|
@ -44,7 +44,7 @@ td:first-child, th[scope="row"] {
|
||||
}
|
||||
|
||||
.tablecell-planned, .tablecell-yes2 {
|
||||
background-color: #B1FCDE
|
||||
background-color: #92E8DF
|
||||
}
|
||||
|
||||
.tablecell-partial {
|
||||
@ -55,6 +55,17 @@ td:first-child, th[scope="row"] {
|
||||
background-color: #DCB1FC
|
||||
}
|
||||
|
||||
.tablecell-caveats {
|
||||
background-color: #F0C884
|
||||
}
|
||||
|
||||
.tablecell-caveats2 {
|
||||
background-color: #ED9A80
|
||||
}
|
||||
|
||||
.tablecell-no-padding {
|
||||
padding: initial;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
@ -83,16 +94,16 @@ td:first-child, th[scope="row"] {
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">bot</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+B<br><span class="note">(botmode)</span></td><td class="tablecell-yes">+B</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+B</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+B</td><td class="tablecell-yes">+B</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+B<br><span class="note">(usermodes/bot)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+B</td><td class="tablecell-yes">+B</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+B</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+B</td><td class="tablecell-yes">+B</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+B</td></tr>
|
||||
<tr>
|
||||
<th scope="row">callerid</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+g</td><td class="tablecell-yes2">+g<br><span class="note">(callerid)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-yes">+g</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">censor</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+G<br><span class="note">(usermodes/censor)</span></td></tr>
|
||||
<th scope="row">callerid_sslonly</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+t</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">cloak</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+x</td><td class="tablecell-yes2">+x<br><span class="note">(cloaking)</span></td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+x</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-yes">+x</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+x</td></tr>
|
||||
<tr>
|
||||
<th scope="row">cloak_fakehost</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
@ -107,10 +118,10 @@ td:first-child, th[scope="row"] {
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td><td class="tablecell-yes">+h</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">deaf</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td><td class="tablecell-yes2">+d<br><span class="note">(deaf)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+d</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+b</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+d</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+D</td><td class="tablecell-yes">+d</td></tr>
|
||||
<tr>
|
||||
<th scope="row">deaf_commonchan</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+G</td><td class="tablecell-yes2">+c<br><span class="note">(commonchans)</span></td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+G</td><td class="tablecell-yes">+c</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">debug</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+d</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
@ -119,19 +130,19 @@ td:first-child, th[scope="row"] {
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+G</td></tr>
|
||||
<tr>
|
||||
<th scope="row">floodexempt</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+F</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+f</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">helpop</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+h<br><span class="note">(helpop)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+h</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">hidechans</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+p</td><td class="tablecell-yes2">+I<br><span class="note">(hidechans)</span></td><td class="tablecell-yes">+I</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+p<br><span class="note">(usermodes/privacy)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+n</td><td class="tablecell-yes">+n</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+p</td></tr>
|
||||
<tr>
|
||||
<th scope="row">hideidle</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td><td class="tablecell-yes">+I</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+I</td></tr>
|
||||
<tr>
|
||||
<th scope="row">hideoper</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+H</td><td class="tablecell-yes2">+H<br><span class="note">(hideoper)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+H</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+H</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+H</td><td class="tablecell-yes">+H</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+H</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+H</td></tr>
|
||||
<tr>
|
||||
<th scope="row">invisible</th>
|
||||
<td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td><td class="tablecell-yes">+i</td></tr>
|
||||
@ -143,10 +154,10 @@ td:first-child, th[scope="row"] {
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+N</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noctcp</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+T<br><span class="note">(usermodes/noctcp)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+C</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+T</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noforward</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+L<br><span class="note">(redirect)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+L</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+L</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-yes">+Q</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">noinvite</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+V</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
@ -161,25 +172,25 @@ td:first-child, th[scope="row"] {
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+X</td><td class="tablecell-yes">+X</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-yes">+p</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">privdeaf</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+b</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+D<br><span class="note">(usermodes/privdeaf)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+D</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">protected</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+q<br><span class="note">(usermodes/nokick)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+q</td></tr>
|
||||
<tr>
|
||||
<th scope="row">regdeaf</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td><td class="tablecell-yes2">+R<br><span class="note">(services_account)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+R<br><span class="note">(usermodes/regonlymsg)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+R</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+R</td></tr>
|
||||
<tr>
|
||||
<th scope="row">registered</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-yes2">+r<br><span class="note">(services_account)</span></td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+R</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-yes">+r</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td></tr>
|
||||
<tr>
|
||||
<th scope="row">restricted</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+r</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">servprotect</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+k<br><span class="note">(servprotect)</span></td><td class="tablecell-yes">+q</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+S</td><td class="tablecell-yes2">+S<br><span class="note">(usermodes/servicebot)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+q</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+k</td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+S</td><td class="tablecell-yes">+S</td></tr>
|
||||
<tr>
|
||||
<th scope="row">showwhois</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+W<br><span class="note">(showwhois)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+W</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+W<br><span class="note">(usermodes/showwhois)</span></td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+W</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+W</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+W</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sno_badclientconnections</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+u</td><td class="tablecell-na note">n/a</td></tr>
|
||||
@ -218,22 +229,19 @@ td:first-child, th[scope="row"] {
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+y</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+y</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">snomask</th>
|
||||
<td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td></tr>
|
||||
<td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+s</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td><td class="tablecell-yes">+s</td></tr>
|
||||
<tr>
|
||||
<th scope="row">ssl</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+z</td></tr>
|
||||
<tr>
|
||||
<th scope="row">sslonlymsg</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+t</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+Z<br><span class="note">(usermodes/secureonlymsg)</span></td></tr>
|
||||
<tr>
|
||||
<th scope="row">stripcolor</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes2">+S<br><span class="note">(stripcolor)</span></td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+S</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
<tr>
|
||||
<th scope="row">vhost</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+t</td></tr>
|
||||
<tr>
|
||||
<th scope="row">wallops</th>
|
||||
<td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td></tr>
|
||||
<td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+w</td><td class="tablecell-na note">n/a</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td><td class="tablecell-yes">+w</td></tr>
|
||||
<tr>
|
||||
<th scope="row">webirc</th>
|
||||
<td class="tablecell-na note">n/a</td><td class="tablecell-yes">+W</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td><td class="tablecell-na note">n/a</td></tr>
|
||||
|
@ -52,14 +52,12 @@ Remote versions of the `manage`, `list`, `sync`, and `clear` commands also exist
|
||||
- `commands.loglevel` - Grants access to the `loglevel` command.
|
||||
- `commands.logout.force` - Allows forcing logouts on other users via the `logout` command.
|
||||
- `commands.showchan` - Grants access to the `showchan` command. **With the default permissions set, this is granted to all users.**
|
||||
- `commands.shownet` - Grants access to the `shownet` command (basic info including netname, protocol module, and encoding). **With the default permissions set, this is granted to all users.**
|
||||
- `commands.shownet.extended` - Grants access to extended info in `shownet`, including connected status, target IP:port, and configured PyLink hostname / SID.
|
||||
- `commands.showuser` - Grants access to the `showuser` command. **With the default permissions set, this is granted to all users.**
|
||||
- `commands.status` - Grants access to the `status` command. **With the default permissions set, this is granted to all users.**
|
||||
|
||||
## Exec
|
||||
- `exec.exec` - Grants access to the `exec` and `iexec` commands.
|
||||
- `exec.eval` - Grants access to the `eval`, `ieval`, `peval`, and `pieval` commands.
|
||||
- `exec.exec` - Grants access to the `exec` command.
|
||||
- `exec.eval` - Grants access to the `eval` command.
|
||||
- `exec.inject` - Grants access to the `inject` command.
|
||||
- `exec.threadinfo` - Grants access to the `threadinfo` command.
|
||||
|
||||
|
@ -1,22 +1,17 @@
|
||||
# PyLink Relay Quick Start
|
||||
# PyLink Relay Quick Start Guide
|
||||
|
||||
## What is Relay?
|
||||
PyLink Relay (aka "Relay") provides transparent server-side relaying between channels, letting networks share channels on demand without going through all the fuss of a hard link. Each network retains its own opers and services, with default behaviour being so that oper features (kill, overrides, etc.) are isolated to only work on channels they own. If you're familiar with Janus, you can think of PyLink Relay as being a rewrite of it from scratch (though PyLink can do much more via its other plugins!).
|
||||
|
||||
PyLink Relay is a plugin that provides transparent relays between channels on different networks. On participating networks, PyLink connects as a services server and mirrors messages as well as user lists from relayed channels, the latter by creating "puppet" service clients for all remote users in common channels. Relay offers an alternative to classic IRC linking, letting networks share channels on demand while retaining their services, policies, and distinct branding. By default, Relay also secures channels from remote oper overrides via a CLAIM feature, which restricts /kick, /mode, and /topic changes from un-opped users unless they are granted permissions via CLAIM.
|
||||
This guide goes over some of the basic commands in Relay, as well as all the must-know notes.
|
||||
|
||||
Relay shares many ideas from its predecessor Janus, but is a complete rewrite in Python. This guide goes over some of the basic commands in Relay, as well as some must-know gotchas.
|
||||
## How nick suffixing work
|
||||
|
||||
## Important notes (READ FIRST!)
|
||||
The default Relay configuration in will automatically tag users from other networks with a suffix such as `/net`. The purpose of this is to prevent confusing nick collisions if the same nick is used on multiple linked networks, and ensure that remote networks' nicks effectively use their own namespace.
|
||||
|
||||
### How nick suffixing work
|
||||
How is this relevant to an operator? Firstly, it means that you **cannot ban users** using banmasks such as `*/net1!*@*`! The nick suffix is something PyLink adds artificially; on `net1`'s IRCd, which check the bans locally, the nick suffix doesn't exist and will therefore *not* match anyone.
|
||||
|
||||
By default, Relay will automatically tag users from other networks with a suffix such as `/net`. This prevents confusing nick collisions if the same nick is used on multiple linked networks, and ensure that nicks from remote networks are all isolated into their own namespaces.
|
||||
|
||||
How is this relevant to an operator? It means that you **cannot ban users** using banmasks such as `*/net1!*@*`! The nick suffix is something PyLink adds artificially; on `net1`'s IRCd, which check the bans locally, the nick suffix doesn't exist and will therefore *not* match anyone.
|
||||
|
||||
### Services compatibility
|
||||
|
||||
While PyLink is generally able to run independently of individual networks' services, there are some gotchas. This list briefly details services features that have been known to cause problems with Relay. **Using any of these features in conjunction with Relay is *not* supported.**
|
||||
## Services compatibility
|
||||
While PyLink is generally able to run independently of individual networks's services, there are some gotchas. This list briefly details services features that have been known to cause problems with Relay. **Using any of these features in conjunction with Relay is *not* supported.**
|
||||
|
||||
- Anope, Atheme: **Clones prevention should be DISABLED** (or at a minimum, set to use G/KLINE instead of KILL)
|
||||
- Rationale: it is common for a person to want to connect to multiple networks in a Relay instance, because they are still independent entities. You can still use IRCd-side clones prevention, which sanely blocks connections instead of killing / banning everyone involved.
|
||||
@ -26,7 +21,6 @@ While PyLink is generally able to run independently of individual networks' serv
|
||||
- Rationale: ChanFix is incompatible with Relay CLAIM because it overrides ops on relay channels whenever they appear "opless". This basic op check is unable to consider the case of remote channel services not being set to join channels, and will instead cause join/message/part spam as CLAIM reverts the ChanFix service's mode changes.
|
||||
- *Any*: **Do NOT register a relayed channel on multiple networks**
|
||||
- Rationale: It is very easy for this to start kick or mode wars. (Bad akick mask? Secure ops enabled?)
|
||||
- Clientbot is an exception to this, though you may want to add Clientbot networks to CLAIM so that PyLink doesn't try to reverse modes set by services on the Clientbot network.
|
||||
- *Any*: **Do NOT jupe virtual Relay servers** (e.g. `net.relay`)
|
||||
- Rationale: This will just make PyLink split off - you should instead [delink any problem networks / channels](#dealing-with-disputes-and-emergencies).
|
||||
- Multiple PyLink Relay instances:
|
||||
@ -34,49 +28,35 @@ While PyLink is generally able to run independently of individual networks' serv
|
||||
- **Do NOT connect a network to 2+ separate PyLink instances if there is another network already acting as a hub for them**.
|
||||
- Not following these rules means that it's very easy for the Relay instances to go in a loop should an operator run the wrong command, which will hammer your CPU and relentlessly spam your channels.
|
||||
|
||||
Note: P10-specific services packages have not been particularly tested - your feedback is welcome.
|
||||
|
||||
## Relay commands
|
||||
The concept of relay channels in PyLink is greatly inspired by Janus, though with a few differences in command syntax.
|
||||
|
||||
The basic steps for setting up a relay is to first CREATE the channel with PyLink on the network that owns it, and run LINK from each network that wants to link to it. In most cases, you want to run CREATE on the network where the channel is registered with services.
|
||||
|
||||
Importantly, this means that CREATE and LINK have to be run on different networks for any particular channel, and that you should only run CREATE once for each distinct channel! This setup is intended to allow individual network admins to pick and choose channels they want to participate in.
|
||||
|
||||
First, to list all available channels:
|
||||
Then, to list all available channels:
|
||||
- `/msg PyLink linked`
|
||||
|
||||
To create a channel on Relay:
|
||||
To create a channel:
|
||||
- `/msg PyLink create #channelname`
|
||||
- Note: **you can only create channels on full IRCd links - this will NOT work with Clientbot.**
|
||||
- A channel created on a particular network is considered to be _owned_ by that network; this affects how CLAIM works for instance (see the next section)
|
||||
|
||||
To link to a channel already created on a different network:
|
||||
- `/msg PyLink link othernet #channelname`
|
||||
- You should replace `othernet` with the *short name* for the network that owns the channel.
|
||||
- Note: network names are case sensitive!
|
||||
|
||||
You can also link remote channels while using a different name for it on your network. (This is the third argument to the LINK command)
|
||||
You can also link remote channels to take a different name on your network. (This is the third argument to the LINK command)
|
||||
- `/msg PyLink link othernet #lobby #othernet-lobby`
|
||||
|
||||
To completely remove a relay channel (on the network that created it):
|
||||
To remove a relay channel that you've created:
|
||||
- `/msg PyLink destroy #channelname`
|
||||
|
||||
To delink a channel *linked to another network*:
|
||||
To delink a channel linked to another network:
|
||||
- `/msg PyLink delink #localchannelname`
|
||||
|
||||
To delink one of *your* channels from another network:
|
||||
- `/msg PyLink delink #yourchannelname <name-of-other-network>`
|
||||
|
||||
Then, to list all available channels:
|
||||
- `/msg PyLink linked`
|
||||
|
||||
### Claiming channels
|
||||
|
||||
Channel claiming is a feature which prevents oper override (MODE, KICK, TOPIC, KILL, OJOIN, ...) by other networks' operators from affecting your channels. By default, CLAIM is enabled for all new channels, though this can be configured via the [`relay::enable_default_claim` option](https://github.com/jlu5/PyLink/blob/3.0.0/example-conf.yml#L828-L831). Unless the claimed network list of a channel is _empty__, oper override will only be allowed from networks on the CLAIM list (plus the network that owns the channel).
|
||||
Channel claims are a feature which prevents oper override (MODE, KICK, TOPIC, KILL, OJOIN, ...) from working on channels not owned by or whitelisting a network. By default, CLAIM is enabled for all new channels, though this can be configured in PyLink 2.0+ via the [`relay::enable_default_claim` option](https://github.com/jlu5/PyLink/blob/2.0-beta1/example-conf.yml#L771-L774). Unless the claimed network list of a channel is EMPTY, oper override will only be allowed from networks on that list.
|
||||
|
||||
Note: these commands must be run from the network which owns the channel in question!
|
||||
|
||||
To set a claim:
|
||||
To set a claim (note: for these commands, you must be on the network which created the channel in question!):
|
||||
- `/msg PyLink claim #channel yournet,net2,net3` (the last parameter is a case-sensitive comma-separated list of networks)
|
||||
|
||||
To list claim networks on a channel:
|
||||
@ -86,8 +66,7 @@ To clear the claim list for a channel:
|
||||
- `/msg PyLink claim #channel -`
|
||||
|
||||
### Access control for links (LINKACL)
|
||||
|
||||
LINKACL allows you to allow or deny networks from linking to your channel. New channels are created using a blacklist by default, though this can be configured via the [`relay::linkacl_use_whitelist` option](https://github.com/jlu5/PyLink/blob/3.0.0/example-conf.yml#L823-L826).
|
||||
LINKACL allows you to blacklist or whitelist networks from linking to your channel. The default configuration enables blacklist mode by default, though this can be configured via the [`relay::linkacl_use_whitelist` option](https://github.com/jlu5/PyLink/blob/2.0-beta1/example-conf.yml#L766-L769).
|
||||
|
||||
To change between blacklist and whitelist mode:
|
||||
- `/msg PyLink linkacl whitelist #channel true/false`
|
||||
@ -97,13 +76,12 @@ To view the LINKACL networks for a channel:
|
||||
- `/msg PyLink linkacl #channel list`
|
||||
|
||||
To add a network to the whitelist **OR** remove a network from the blacklist:
|
||||
- `/msg PyLink linkacl #channel allow goodnet`
|
||||
- `/msg PyLink linkacl #channel allow badnet`
|
||||
|
||||
To remove a network from the whitelist **OR** add a network to the blacklist:
|
||||
- `/msg PyLink linkacl #channel deny badnet`
|
||||
- `/msg PyLink linkacl #channel deny goodnet`
|
||||
|
||||
### Adding channel descriptions
|
||||
|
||||
Starting with PyLink 2.0, you can annotate your channels with a description to use in LINKED:
|
||||
|
||||
To view the description for a channel:
|
||||
@ -117,17 +95,16 @@ To remove the description for a channel:
|
||||
|
||||
## Dealing with disputes and emergencies
|
||||
|
||||
The best thing to do in the event of a dispute is to delink the problem networks / channels. In order for individual networks to maintain their autonomy, KILLs and network bans (K/G/ZLINE) will most often *not* behave the way you expect them to.
|
||||
The best thing to do in the event of a dispute is to delink the problem networks / channels. KILLs and network bans (K/G/ZLINE) will most often *not* behave the way you expect it to.
|
||||
|
||||
### Kill handling
|
||||
Special kill handling was introduced in PyLink 2.0, while in previous versions they were always rejected:
|
||||
|
||||
Special kill handling was introduced in PyLink 2.0, while in previous versions they were always bounced:
|
||||
|
||||
1) If the sender was a server and not a client, reject the kill. (This prevents services messups from wreaking havoc across the relay)
|
||||
2) If the target and source networks share a [kill share pool](https://github.com/jlu5/PyLink/blob/3.0.0/example-conf.yml#L782-L792), relay the kill as-is.
|
||||
3) Otherwise, check every channel that the kill target is in:
|
||||
- If the sender is opped or has claim access in a channel, forward the KILL as a kick in that channel.
|
||||
- Otherwise, bounce the kill silently (i.e. rejoin the user immediately).
|
||||
1) If the sender was a server and not a client, reject the kill.
|
||||
2) If the target and source networks are both in a(ny) [kill share pool](https://github.com/jlu5/PyLink/blob/2.0-beta1/example-conf.yml#L725-L735), relay the kill as-is.
|
||||
3) Otherwise, check every channels the kill target is in:
|
||||
- If the killer has claim access in a channel, forward the KILL as a kick to that channel.
|
||||
- Otherwise, bounce the kill silently.
|
||||
|
||||
### Network bans (K/G/ZLINE)
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
### Version Warning
|
||||
This version of the document targets the current stable branch of PyLink, and may be considerably outdated when compared to the **devel** branch where active development takes place. When developing new plugins or protocol modules, we highly recommend targeting the devel branch.
|
||||
|
||||
# PyLink hooks reference
|
||||
|
||||
***Last updated for 3.1-dev (2021-06-13).***
|
||||
***Last updated for 2.0.0 (2018-07-11).***
|
||||
|
||||
In PyLink, protocol modules communicate with plugins through a system of hooks. This has the benefit of being IRCd-independent, allowing most plugins to function regardless of the IRCd being used.
|
||||
Each hook payload is formatted as a Python `list`, with three arguments: `numeric`, `command`, and `args`.
|
||||
@ -19,11 +22,11 @@ The command `:42XAAAAAB PRIVMSG #dev :test` would result in the following raw ho
|
||||
|
||||
- `['42XAAAAAB', 'PRIVMSG', {'target': '#dev', 'text': 'test', 'ts': 1451174041}]`
|
||||
|
||||
On UnrealIRCd, because SETHOST is mapped to CHGHOST, `:jlu5 SETHOST blah` would return the raw hook data of this (with the nick converted into UID automatically by the protocol module):
|
||||
On UnrealIRCd, because SETHOST is mapped to CHGHOST, `:GL SETHOST blah` would return the raw hook data of this (with the nick converted into UID automatically by the protocol module):
|
||||
|
||||
- `['001ZJZW01', 'CHGHOST', {'ts': 1451174512, 'target': '001ZJZW01', 'newhost': 'blah'}]`
|
||||
|
||||
Some hooks, like MODE, are more complex and can include the entire state of a channel. This will be further described later. `:jlu5 MODE #chat +o PyLink-devel` is converted into (pretty-printed for readability):
|
||||
Some hooks, like MODE, are more complex and can include the entire state of a channel. This will be further described later. `:GL MODE #chat +o PyLink-devel` is converted into (pretty-printed for readability):
|
||||
|
||||
```
|
||||
['001ZJZW01',
|
||||
@ -64,9 +67,9 @@ The following hooks represent regular IRC commands sent between servers.
|
||||
- **KICK**: `{'channel': '#channel', 'target': 'UID1', 'text': 'some reason'}`
|
||||
- `text` refers to the kick reason. The `target` and `channel` fields send the target's UID and the channel they were kicked from, and the sender of the hook payload is the kicker.
|
||||
|
||||
- **KILL**: `{'target': killed, 'text': 'Killed (james (absolutely not))', 'userdata': User(...)}`
|
||||
- **KILL**: `{'target': killed, 'text': 'Killed (james (absolutely not))', 'userdata': data}`
|
||||
- `text` refers to the kill reason. `target` is the target's UID.
|
||||
- `userdata` includes a `classes.User` instance, containing the information of the killed user.
|
||||
- The `userdata` key may include an `classes.User` instance, depending on the IRCd. On IRCds where QUITs are explicitly sent (e.g InspIRCd), `userdata` will be `None`. Other IRCds do not explicitly send QUIT messages for killed clients, so the daemon must assume that they've quit, and deliver their last state to plugins that require this info.
|
||||
|
||||
- **MODE**: `{'target': '#channel', 'modes': [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')], 'channeldata': Channel(...)}`
|
||||
- `target` is the target the mode is being set on: it may be either a channel (for channel modes) *or* a UID (for user modes).
|
||||
@ -86,11 +89,10 @@ The following hooks represent regular IRC commands sent between servers.
|
||||
- **PRIVMSG**: `{'target': 'UID3', 'text': 'hi there!'}`
|
||||
- Ditto with NOTICE: STATUSMSG targets (e.g. `@#lounge`) are also allowed here.
|
||||
|
||||
- **QUIT**: `{'text': 'Quit: Bye everyone!', 'userdata': User(...)}`
|
||||
- **QUIT**: `{'text': 'Quit: Bye everyone!'}`
|
||||
- `text` corresponds to the quit reason.
|
||||
- `userdata` includes a `classes.User` instance, containing the information of the killed user.
|
||||
|
||||
- **SQUIT**: `{'target': '800', 'users': ['UID1', 'UID2', 'UID6'], 'name': 'some.server', 'uplink': '24X', 'nicks': {'#channel1: ['tester1', 'tester2'], '#channel3': ['somebot']}, 'serverdata': Server(...), 'affected_servers': ['SID1', 'SID2', 'SID3']`
|
||||
- **SQUIT**: `{'target': '800', 'users': ['UID1', 'UID2', 'UID6'], 'name': 'some.server', 'uplink': '24X', 'nicks': {'#channel1: ['tester1', 'tester2'], '#channel3': ['somebot']}, 'serverdata': Server(...)`
|
||||
- `target` is the SID of the server being split, while `name` is the server's name.
|
||||
- `users` is a list of all UIDs affected by the netsplit. `nicks` maps channels to lists of nicks affected.
|
||||
- `serverdata` provides the `classes.Server` object of the server that split off.
|
||||
@ -100,10 +102,9 @@ The following hooks represent regular IRC commands sent between servers.
|
||||
- `oldtopic` denotes the original topic, and `text` indicates the new one being set.
|
||||
- `setter` is the raw sender field given to us by the IRCd; it may be a `nick!user@host`, a UID, a SID, a server name, or a nick. This is not processed at the protocol level.
|
||||
|
||||
- **UID**: `{'uid': 'UID1', 'ts': 1234567891, 'nick': 'supercoder', 'realhost': 'localhost', 'host': 'admin.testnet.local', 'ident': ident, 'ip': '127.0.0.1', 'secure': True}`
|
||||
- **UID**: `{'uid': 'UID1', 'ts': 1234567891, 'nick': 'supercoder', 'realhost': 'localhost', 'host': 'admin.testnet.local', 'ident': ident, 'ip': '127.0.0.1'}`
|
||||
- This command is used to introduce users; the sender of the message should be the server bursting or announcing the connection.
|
||||
- `ts` refers to the user's signon time.
|
||||
- `secure` is a ternary value (True/False/None) that determines whether the user is connected over a secure connection (SSL/TLS). This value is only available on some IRCds: currently UnrealIRCd, P10, Charybdis TS6, and Hybrid; on other servers this will be `None`.
|
||||
|
||||
### Extra commands (where supported)
|
||||
|
||||
@ -163,13 +164,6 @@ Some hooks do not map directly to IRC commands, but to events that protocol modu
|
||||
At this time, commands that are handled by protocol modules without returning any hook data include PING, PONG, and various commands sent during the initial server linking phase.
|
||||
|
||||
## Changes
|
||||
|
||||
* 2021-06-13 (3.1-dev)
|
||||
- Added the `secure` field to `UID` hooks.
|
||||
* 2019-07-01 (2.1-alpha2)
|
||||
- KILL and QUIT hooks now always include a non-empty `userdata` key. Now, if a QUIT message for a killed user is received before the corresponding KILL (or vice versa), only the first message received will have the corresponding hook payload broadcasted.
|
||||
* 2018-12-27 (2.1-dev)
|
||||
- Add the `affected_servers` argument to SQUIT hooks.
|
||||
* 2018-07-11 (2.0.0)
|
||||
- Version bump for 2.0 stable release; no meaningful content changes.
|
||||
* 2018-01-13 (2.0-alpha2)
|
||||
|
@ -1,6 +1,9 @@
|
||||
### Version Warning
|
||||
This version of the document targets the current stable branch of PyLink, and may be considerably outdated when compared to the **devel** branch where active development takes place. When developing new plugins or protocol modules, we highly recommend targeting the devel branch.
|
||||
|
||||
# PyLink Protocol Module Specification
|
||||
|
||||
***Last updated for 3.1-dev (2021-06-15).***
|
||||
***Last updated for 2.0.0 (2018-07-11).***
|
||||
|
||||
Starting with PyLink 2.x, a *protocol module* is any module containing a class derived from `PyLinkNetworkCore` (e.g. `InspIRCdProtocol`), along with a global `Class` attribute set equal to it (e.g. `Class = InspIRCdProtocol`). These modules do everything from managing connections to providing plugins with an API to send and receive data. New protocol modules may be implemented based off any of the classes in the following inheritance tree, with each containing a different amount of abstraction.
|
||||
|
||||
@ -38,9 +41,8 @@ This class offers the most flexibility because the protocol module can choose ho
|
||||
### `protocols.ircs2s_common.IRCS2SProtocol`
|
||||
`IRCS2SProtocol` is the most complete base server class, including a generic `handle_events()` supporting most IRC S2S message styles (i.e. prefix-less messages, protocols with and without UIDs). It also defines some incoming and outgoing command functions that hardly vary between protocols: `invite()`, `kick()`, `message()`, `notice()`, `numeric()`, `part()`, `quit()`, `squit()`, and `topic()` as of PyLink 2.0. This list is subject to change in future releases.
|
||||
|
||||
### `classes.PyLinkNetworkCoreWithUtils`
|
||||
|
||||
`PyLinkNetworkCoreWithUtils` contains various state checking and IRC-related utility functions. Originally this abstraction was intended to support non-IRC protocols (Discord, Telegram, Slack, ...), but I (jlu5) no longer support this as a development focus. The main reason being is that in order to keep track of IRC server state correctly, PyLink makes a lot of assumptions specific to IRC (e.g. explicit join/part, mode formats, etc.). Trying to reconcile this with other platforms is a large undertaking and ideally requires a different, more generic protocol specification. (In PyLink 2.x there was a [Discord module](https://github.com/PyLink/pylink-discord) that is no longer supported - see https://jlu5.com/blog/the-trouble-with-pylink for a more in depth explanation as to why.)
|
||||
### For non-IRC protocols: `classes.PyLinkNetworkCoreWithUtils`
|
||||
Although no such transports have been implemented yet, PyLink leaves some level of abstraction for non-IRC protocols (e.g. Discord, Telegram, Slack, ...) by providing generic classes that only include state checking and utility functions.
|
||||
|
||||
Subclassing one of the `PyLinkNetworkCore*` classes means that a protocol module only needs to define one method of entry: `connect()`, and must set up its own message handling stack. Protocol configuration validation checks and autoconnect must also be reimplemented. IRC-style utility functions (i.e. `PyLinkNetworkCoreWithUtils` methods) should also be reimplemented / overridden when applicable.
|
||||
|
||||
@ -83,8 +85,6 @@ Unless otherwise noted, the camel-case variants of command functions (e.g. "`spa
|
||||
|
||||
- **`nick`**`(self, source, newnick)` - Changes the nick of a PyLink client.
|
||||
|
||||
- **`oper_notice`**`(self, source, target)` - Sends a notice to all operators on the network.
|
||||
|
||||
- **`notice`**`(self, source, target, text)` - Sends a NOTICE from a PyLink client or server.
|
||||
|
||||
- **`numeric`**`(self, source, numeric, target, text)` - Sends a raw numeric `numeric` with `text` from the `source` server to `target`. This should raise `NotImplementedError` if not supported on a protocol.
|
||||
@ -136,20 +136,14 @@ As of writing, the following protocol capabilities (case-sensitive) are implemen
|
||||
|
||||
### Supported protocol capabilities
|
||||
- `can-host-relay` - whether servers using this protocol can host a relay channel (for sanity reasons, this should be off for anything that's not IRC S2S)
|
||||
- `can-manage-bot-channels` - whether PyLink can manage which channels the bot itself is in. This is off for platforms such as Discord.
|
||||
- `can-spawn-clients` - determines whether any spawned clients are real or virtual (mainly for `services_support`).
|
||||
- `can-track-servers` - determines whether servers are accurately tracked (for `servermaps` and other statistics)
|
||||
- `freeform-nicks` - if set, nicknames for PyLink's virtual clients are not subject to validity and nick collision checks. This implies the `slash-in-nicks` capability.
|
||||
- Note: PyLink already allows incoming nicks to be freeform, provided they are encoded correctly and don't cause parsing conflicts (i.e. containing reserved chars on IRC)
|
||||
- `has-irc-modes` - whether IRC style modes are supported
|
||||
- `has-statusmsg` - whether STATUSMSG messages (e.g. `@#channel`) are supported
|
||||
- `has-ts` - determines whether channel and user timestamps are tracked (and not spoofed)
|
||||
- `has-ts` - determines whether channel and user timestamps are trackable (and not just spoofed)
|
||||
- `slash-in-hosts` - determines whether `/` is allowed in hostnames
|
||||
- `slash-in-nicks` - determines whether `/` is allowed in nicks
|
||||
- `ssl-should-verify` - determines whether TLS certificates should be checked for validity by default - this should be enabled for any protocol modules needing to verify a remote server (e.g. Clientbot or a non-IRC API endpoint), and disabled for most IRC S2S links (where self-signed certs are widespread)
|
||||
- `underscore-in-hosts` - determines whether `_` is allowed in client hostnames (yes, support for this actually varies by IRCd)
|
||||
- `virtual-server` - marks the server as virtual, i.e. controlled by protocol module under a different server. Virtual servers are ignored by `rehash` and `disconnect` in the `networks` plugin.
|
||||
- This is used by pylink-discord as of v0.2.0.
|
||||
- `visible-state-only` - determines whether channels should be autocleared when the PyLink client leaves (for clientbot, etc.)
|
||||
- Note: enabling this in a protocol module lets `coremods/handlers` automatically clean up old channels for you!
|
||||
|
||||
@ -163,7 +157,7 @@ For reference, the `IRCS2SProtocol` class defines the following by default:
|
||||
- `can-track-servers`
|
||||
- `has-ts`
|
||||
|
||||
Whereas `PyLinkNetworkCore` defines no capabilities (i.e. an empty set) by default.
|
||||
Whereas `PyLinkNetworkCore` defines no capabilities (i.e. the empty set) by default.
|
||||
|
||||
## PyLink structures
|
||||
In this section, `self` refers to the network object/protocol module instance itself (i.e. from its own perspective).
|
||||
@ -210,7 +204,7 @@ Modes are stored not stored as strings, but lists of mode pairs in order to ease
|
||||
|
||||
- `self.parse_modes('#chat', ['+ol invalidnick'])`:
|
||||
- `[]`
|
||||
- `self.parse_modes('#chat', ['+o jlu5'])`:
|
||||
- `self.parse_modes('#chat', ['+o GLolol'])`:
|
||||
- `[('+o', '001ZJZW01')]`
|
||||
|
||||
Afterwords, a parsed mode list can be applied to channel name or UID using `self.apply_modes(target, parsed_modelist)`.
|
||||
@ -220,16 +214,16 @@ Afterwords, a parsed mode list can be applied to channel name or UID using `self
|
||||
Internally, modes are stored in `Channel` and `User` objects as sets, **with the `+` prefixing each mode character omitted**. These sets are accessed via the `modes` attribute:
|
||||
|
||||
```
|
||||
<+jlu5> PyLink-devel, eval irc.users[source].modes
|
||||
<+GLolol> PyLink-devel, eval irc.users[source].modes
|
||||
<@PyLink-devel> {('i', None), ('x', None), ('w', None), ('o', None)}
|
||||
<+jlu5> PyLink-devel, eval irc.channels['#chat'].modes
|
||||
<+GLolol> PyLink-devel, eval irc.channels['#chat'].modes
|
||||
<@PyLink-devel> {('n', None), ('t', None)}
|
||||
```
|
||||
|
||||
**Exception**: the owner, admin, op, halfop, and voice channel prefix modes are stored separately as a dict of sets in `Channel.prefixmodes`:
|
||||
|
||||
```
|
||||
<@jlu5> PyLink-devel, eval irc.channels['#chat'].prefixmodes
|
||||
<@GLolol> PyLink-devel, eval irc.channels['#chat'].prefixmodes
|
||||
<+PyLink-devel> {'op': set(), 'halfop': set(), 'voice': {'38QAAAAAA'}, 'owner': set(), 'admin': set()}
|
||||
```
|
||||
|
||||
@ -266,15 +260,6 @@ In short, protocol modules have some very important jobs. If any of these aren't
|
||||
7) Declare the correct set of protocol module capabilities to prevent confusing PyLink's plugins.
|
||||
|
||||
## Changes to this document
|
||||
* 2021-06-15 (3.1-dev)
|
||||
- Added `oper_notice()` function to send notices to opers (GLOBOPS / OPERWALL on most IRCds)
|
||||
- Update notes about non-IRC protocols and PyLinkNetworkCoreWithUtils
|
||||
* 2019-11-02 (2.1-beta1)
|
||||
- Added protocol capability: `can-manage-bot-channels`
|
||||
* 2019-10-10 (2.1-beta1)
|
||||
- Added protocol capability: `has-irc-modes`
|
||||
* 2019-06-23 (2.1-alpha2)
|
||||
- Added new protocol capabilities: `virtual-server` and `freeform-nicks`
|
||||
* 2018-07-11 (2.0.0)
|
||||
- Version bump for 2.0 stable release; no meaningful content changes.
|
||||
* 2018-06-26 (2.0-beta1)
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Writing plugins for PyLink
|
||||
### Version Warning
|
||||
This version of the document targets the current stable branch of PyLink, and may be considerably outdated when compared to the **devel** branch where active development takes place. When developing new plugins or protocol modules, we highly recommend targeting the devel branch.
|
||||
|
||||
***Last updated for 2.1-alpha2 (2019-06-27).***
|
||||
# Writing plugins for PyLink
|
||||
|
||||
Most features in PyLink (Relay, Automode, etc.) are implemented as plugins, which can be mix-and-matched on any particular instance. Without any plugins loaded, PyLink can connect to servers but won't accomplish anything useful.
|
||||
|
||||
@ -23,7 +24,7 @@ Functions intended to be hook handlers therefore take in 4 arguments correspondi
|
||||
|
||||
#### Return codes for hook handlers
|
||||
|
||||
As of PyLink 2.0, the return value of hook handlers are used to determine how the original event will be passed on to further handlers (that is, those created by plugins loaded later, or hook handlers registered with a lower priority).
|
||||
As of PyLink 2.0-alpha3, the return value of hook handlers are used to determine how the original event will be passed on to further handlers (that is, those created by plugins loaded later, or hook handlers registered with a lower priority).
|
||||
|
||||
The following return values are supported so far:
|
||||
|
||||
@ -32,17 +33,10 @@ The following return values are supported so far:
|
||||
|
||||
Hook handlers may raise exceptions without blocking the event from reaching further handlers; these are caught by PyLink and logged appropriately.
|
||||
|
||||
#### Modifying a hook payload
|
||||
|
||||
As of PyLink 2.1, it is acceptable to modify a hook event payload in any plugin handler. This can be used for filtering purposes, e.g. Antispam's part/quit message filtering.
|
||||
|
||||
You should take extra caution not to corrupt hook payloads, especially ones that relate to state keeping. Otherwise, other plugins may fail to function correctly.
|
||||
|
||||
### Hook priorities
|
||||
|
||||
The `priority` option in `utils.add_hook()` allows setting a hook handler's priority when binding it. When multiple modules bind to one hook command, handlers are called in order of decreasing priority (i.e. highest first).
|
||||
|
||||
There is no standard for hook priorities as of 2.0; instead they are declared as necessary. Some priority values used in 2.0 are shown here for reference (the default priority for handlers is **100**):
|
||||
There is no standard for hook priorities as of 2.0; instead they are declared as necessary. Some priority values used in 2.0 are shown here for reference:
|
||||
|
||||
| Module | Commands | Priority | Description |
|
||||
|-------------------|-----------------|----------|-------------|
|
||||
|
236
example-conf.yml
236
example-conf.yml
@ -46,21 +46,12 @@ pylink:
|
||||
# be merged into that of the main PyLink service bot.
|
||||
#spawn_services: true
|
||||
|
||||
# Determines the default ban style that PyLink should use for setting bans (e.g. in Antispam)
|
||||
# $ident, $host, $realhost, $ip, and $nick are among the supported substitutions here.
|
||||
# This defaults to "*!*@$host" if not set.
|
||||
# You should take extra caution that 1) the resulting mask is a valid nick!user@host
|
||||
# 2) generated bans actually match the target user
|
||||
#ban_style: "*!*@$host"
|
||||
#ban_style: "*!$ident@$host" # A possible alternative
|
||||
|
||||
# Defines extra directories to look up plugins and protocol modules in.
|
||||
# Environment variables (e.g. $HOME) and home folders (~) are expanded here, in that order.
|
||||
#plugin_dirs: ["~/my-plugins", "~/pylink-contrib-modules/plugins"]
|
||||
#protocol_dirs: ["~/pylink-contrib-modules/protocols"]
|
||||
|
||||
# Determines whether service bots should return unknown command errors. Defaults to true if not
|
||||
# speciifed.
|
||||
# Determines whether we should show unknown command errors for service bots. Defaults to True.
|
||||
#show_unknown_commands: true
|
||||
|
||||
# Determines whether hideoper modes should be respected in WHOIS replies.
|
||||
@ -81,19 +72,6 @@ pylink:
|
||||
# of all database-enabled plugins to take effect.
|
||||
#save_delay: 300
|
||||
|
||||
# Determines whether that service bots should join preconfigured channels even if they are empty.
|
||||
# This can also be overriden per-network via the "join_empty_channels" variable.
|
||||
# This defaults to False if not set.
|
||||
#join_empty_channels: false
|
||||
|
||||
# Determines where the plugins write their databases. The path can be relative to the directory
|
||||
# PyLink is run from. Defaults to the current directory.
|
||||
#data_dir: ""
|
||||
|
||||
# Determines where the PID file is written. The path can be relative to the directory PyLink is
|
||||
# run from. Defaults to the current directory.
|
||||
#pid_dir: ""
|
||||
|
||||
login:
|
||||
# NOTE: for users migrating from PyLink < 1.1, the old login:user/login:password settings
|
||||
# have been deprecated. We strongly recommend migrating to the new "accounts:" block below, as
|
||||
@ -125,15 +103,6 @@ login:
|
||||
# are supported here as well.
|
||||
#hosts: ["*!*@localhost", "*!*@trusted.isp"]
|
||||
|
||||
# For ADVANCED users: adjusts settings for PyLink's default passlib CryptContext.
|
||||
# As of PyLink 2.0.3, the default is to use pbkdf2_sha256 for new hashes, while also allowing verifying
|
||||
# sha512_crypt for compatibility with PyLink < 2.0.3.
|
||||
# This is configured as a dict of settings, which will be passed into the CryptContext constructor.
|
||||
# See https://passlib.readthedocs.io/en/stable/lib/passlib.context.html for a list of valid options.
|
||||
# Changes to this setting require a rehash to apply.
|
||||
#cryptcontext_settings:
|
||||
#schemes: ["pbkdf2_sha256", "sha512_crypt"]
|
||||
|
||||
permissions:
|
||||
# Permissions blocks in PyLink are define as a mapping of PyLink targets (i.e. hostmasks or
|
||||
# exttargets) to lists of permission nodes. You can find a list of permissions that PyLink and
|
||||
@ -169,6 +138,7 @@ servers:
|
||||
# CHANGE THIS to some abbreviation representing your network; usually
|
||||
# something 3-5 characters should be good.
|
||||
inspnet:
|
||||
|
||||
# Server IP, port, and passwords. The ip: field also supports resolving
|
||||
# hostnames.
|
||||
ip: 127.0.0.1
|
||||
@ -185,66 +155,63 @@ servers:
|
||||
# Hostname we will use to connect to the remote server
|
||||
hostname: "pylink.yournet.local"
|
||||
|
||||
# Sets the server ID (SID) that the main PyLink server should use. For TS6-like servers
|
||||
# (InspIRCd, Charybdis, UnrealIRCd, etc.), this must be three characters:
|
||||
# the first char must be a digit [0-9], and the remaining two may be either uppercase
|
||||
# letters [A-Z] or digits.
|
||||
# Sets the server ID (SID) that the main PyLink server should use.
|
||||
# For TS6-like servers (InspIRCd, Charybdis, UnrealIRCd, etc.), this
|
||||
# must be three characters: the first char must be a digit [0-9], and
|
||||
# the remaining two may be either uppercase letters [A-Z] or digits.
|
||||
sid: "0PY"
|
||||
|
||||
# Server ID range: this specifies the range of server IDs that PyLink# may use for
|
||||
# subservers such as Relay. On TS6, this should be a combination of digits, letters, and #'s.
|
||||
# Each # denotes a range (0-9A-Z) of characters that can be used by PyLink to generate SIDs.
|
||||
# Server ID range: this specifies the range of server IDs that PyLink
|
||||
# may use for subservers such as relay. On TS6, this should be a
|
||||
# combination of digits, letters, and #'s. Each # denotes a range (0-9A-Z)
|
||||
# of characters that can be used by PyLink to generate appropriate SIDs.
|
||||
# You will want to make sure no other servers are using this range.
|
||||
# There must be at least one # in this entry.
|
||||
sidrange: "8##"
|
||||
|
||||
# Sets the protocol module to use for this network - see the README for a list of supported
|
||||
# IRCds.
|
||||
# Sets the protocol module to use for this network - see the README for a
|
||||
# list of supported IRCds.
|
||||
protocol: "inspircd"
|
||||
|
||||
# InspIRCd specific option: sets the target InspIRCd protocol version.
|
||||
# Valid values include:
|
||||
# "insp3" - InspIRCd 3.x (1205) [DEFAULT]
|
||||
# "insp20" - InspIRCd 2.0.x (1202) [legacy, deprecated]
|
||||
#target_version: insp3
|
||||
|
||||
# Sets the max nick length for the network. It is important that this is set correctly, or
|
||||
# PyLink might introduce a nick that is too long and cause netsplits!
|
||||
# This defaults to 30 if not set.
|
||||
# Sets the max nick length for the network. It is important that this is
|
||||
# set correctly, or PyLink might introduce a nick that is too long and
|
||||
# cause netsplits! This defaults to 30 if not set.
|
||||
maxnicklen: 30
|
||||
|
||||
# Toggles TLS/SSL for this network - you should seriously consider using TLS in all server links
|
||||
# Toggles SSL for this network - you should seriously consider using TLS in all your links
|
||||
# for optimal security. Defaults to False if not specified.
|
||||
ssl: true
|
||||
|
||||
# Optional TLS cert/key to pass to the uplink server.
|
||||
# Optional SSL cert/key to pass to the uplink server.
|
||||
#ssl_certfile: pylink-cert.pem
|
||||
#ssl_keyfile: pylink-key.pem
|
||||
|
||||
# New in PyLink 2.0: Determines whether the target server's TLS certificate hostnames should be
|
||||
# New in 2.0: Determines whether the target server's TLS certificate hostnames should be
|
||||
# checked against the hostname we're set to connect to. This defaults to true for Clientbot
|
||||
# networks and others linked to via a hostname. It depends on ssl_accept_invalid_certs being
|
||||
# *disabled* to take effect.
|
||||
#ssl_validate_hostname: true
|
||||
|
||||
# New in PyLink 2.0: When enabled, this disables TLS certificate validation on the target network.
|
||||
# New in 2.0: When enabled, this disables TLS certificate validation on the target network.
|
||||
# This defaults to false (bad certs are rejected) on Clientbot and true for server protocols
|
||||
# (where bad certs are accepted). This disables the ssl_validate_hostname option, forcing it to
|
||||
# be false.
|
||||
# (where bad certs are accepted). This disables the ssl_validate_hostname option,
|
||||
# effectively forcing it to be false.
|
||||
#ssl_accept_invalid_certs: false
|
||||
|
||||
# Optionally, you can set this option to verify the TLS certificate fingerprint of your
|
||||
# uplink. This check works independently of ssl_validate_hostname and ssl_accept_invalid_certs.
|
||||
# Optionally, you can set this option to verify the SSL certificate fingerprint of your
|
||||
# uplink. This check works regardless of whether ssl_validate_hostname and
|
||||
# ssl_accept_invalid_certs are enabled.
|
||||
#ssl_fingerprint: "e0fee1adf795c84eec4735f039503eb18d9c35cc"
|
||||
|
||||
# This sets the hash type for the above TLS certificate fingerprint.
|
||||
# Valid values include md5 and sha1-sha512, and others that may be supported depending on
|
||||
# your system: see https://docs.python.org/3/library/hashlib.html
|
||||
# This defaults to sha256 if not set.
|
||||
# This sets the hash type for the fingerprint (md5, sha1, sha256, etc.)
|
||||
# Valid values include md5 and sha1-sha512, though others may be
|
||||
# supported depending on your system: see
|
||||
# https://docs.python.org/3/library/hashlib.html
|
||||
# This setting defaults to sha256.
|
||||
#ssl_fingerprint_type: sha256
|
||||
|
||||
# Sets autoconnect delay - comment this out or set the value below 1 to disable autoconnect
|
||||
# entirely.
|
||||
# Sets autoconnect delay - comment this out or set the value below 1 to
|
||||
# disable autoconnect entirely.
|
||||
autoconnect: 10
|
||||
|
||||
# Optional autoconnect settings:
|
||||
@ -268,20 +235,21 @@ servers:
|
||||
# https://docs.python.org/3/library/codecs.html#standard-encodings
|
||||
# Changing this setting requires a disconnect and reconnect of the corresponding network
|
||||
# to apply.
|
||||
# This setting is EXPERIMENTAL as of PyLink 1.2.x.
|
||||
#encoding: utf-8
|
||||
|
||||
# Sets the ping frequency in seconds (i.e. how long we should wait between sending pings to
|
||||
# our uplink). When more than two consecutive pings are missed, PyLink will disconnect with
|
||||
# a ping timeout. This defaults to 90 if not set.
|
||||
# Sets the ping frequency in seconds (i.e. how long we should wait between
|
||||
# sending pings to our uplink). When more than two consecutive pings are missed,
|
||||
# PyLink will disconnect with a ping timeout. This defaults to 90 if not set.
|
||||
#pingfreq: 90
|
||||
|
||||
# If relay nick tagging is disabled, this option specifies a list of nick globs to always
|
||||
# tag when introducing remote users *onto* this network.
|
||||
#relay_forcetag_nicks: ["someuser", "Guest*"]
|
||||
|
||||
# Sets the suffix that relay subservers on this network should use. If not specified per
|
||||
# network, this falls back to the value at relay::server_suffix, or the string "relay" if
|
||||
# that is also not set.
|
||||
# Sets the suffix that relay subservers on this network should use.
|
||||
# If not specified per network, this falls back to the value at
|
||||
# relay::server_suffix, or "relay" if that is also not set.
|
||||
#relay_server_suffix: "relay.yournet.net"
|
||||
|
||||
# Determines whether relay will tag nicks on this network. This overrides the relay::tag_nicks
|
||||
@ -305,6 +273,13 @@ servers:
|
||||
ip: ::1
|
||||
port: 8067
|
||||
|
||||
# Determines whether IPv6 should be used for this connection. Should the ip:
|
||||
# above be a hostname instead of an IP, this will also affect whether A records
|
||||
# (IPv4) or AAAA records (IPv6) will be used in resolving it.
|
||||
# As of PyLink 2.0-beta1, you can leave this unset for direct connections to IP addresses;
|
||||
# the address type will be automatically detected.
|
||||
#ipv6: yes
|
||||
|
||||
# Received and sent passwords. For passwordless links using SSL fingerprints, simply set
|
||||
# these two fields to "*" and enable SSL with a cert and key file.
|
||||
recvpass: "coffee"
|
||||
@ -338,13 +313,9 @@ servers:
|
||||
port: 45454
|
||||
|
||||
# When the IP field is set to a hostname, the "ipv6" option determines whether IPv4 or IPv6
|
||||
# addresses should be used when resolving it.
|
||||
# As of PyLink 3.1, this defaults to null, falling back to the system's default preferences
|
||||
# if not set (e.g. /etc/gai.conf on Linux). Previous versions default to making IPv4 connections only.
|
||||
# This option is overridden by "bindhost" if it is also provided.
|
||||
#ipv6: null
|
||||
|
||||
# Specifies the IP to make outgoing connections from, for multi-homed hosts.
|
||||
# addresses should be used when resolving it. You can leave this field blank and use an
|
||||
# explicit bindhost instead, which will let the the address type be automatically detected.
|
||||
#ipv6: false
|
||||
#bindhost: 1111:2222:3333:4444
|
||||
|
||||
# Note: if you are actually using dynamic DNS for an IRC link, consider enabling
|
||||
@ -454,11 +425,6 @@ servers:
|
||||
# and earlier.
|
||||
#ircd: charybdis
|
||||
|
||||
# Determines whether PyLink servers should be marked hidden in /links and /map.
|
||||
# This only takes effect if the 'flatten_links' option in the IRCd is enabled.
|
||||
# Defaults to false if not set.
|
||||
#hidden: false
|
||||
|
||||
# Sample Clientbot configuration, if you want to connect PyLink as a bot to relay somewhere
|
||||
# (or do other bot things).
|
||||
magicnet:
|
||||
@ -506,7 +472,7 @@ servers:
|
||||
|
||||
# Message throttling: when set to a non-zero value, only one message will be sent every X
|
||||
# seconds. If your bot is constantly running into Excess Flood errors, raising this to
|
||||
# something like 0.5 or 1.0 should help. Since PyLink 2.0.2, this defaults to 0 if not set.
|
||||
# something like 0.5 or 1.0 should help. Defaults to 0.005 if not set.
|
||||
throttle_time: 0.3
|
||||
|
||||
# Determines whether messages from unknown clients (servers, clients not sharing in a -n
|
||||
@ -541,10 +507,6 @@ servers:
|
||||
# - "MODE $nick +B"
|
||||
# - "NOTICE somebody :hello, i've connected"
|
||||
|
||||
# Determines whether we should always attempt to rejoin channels we've been removed from.
|
||||
# These attempts take place in the same interval as pingfreq. Defaults to false if not set.
|
||||
#always_autorejoin: true
|
||||
|
||||
# Determines whether oper statuses should be tracked on this Clientbot network. This
|
||||
# defaults to False for the best security, since oper status may allow more access to the
|
||||
# entire PyLink service than what's desired, even when PyLink is only connected as a bot.
|
||||
@ -570,10 +532,6 @@ plugins:
|
||||
# Ctcp plugin: handles basic CTCP replies (VERSION, etc) towards service bots.
|
||||
- ctcp
|
||||
|
||||
# Servprotect plugin: disconnects from networks if too many kills or nick collisions to
|
||||
# PyLink clients are received. Requires the cachetools Python library.
|
||||
- servprotect
|
||||
|
||||
# Relay plugin: Transparent server-side relay between channels (like Janus). See
|
||||
# the relay: block below for configuration.
|
||||
#- relay
|
||||
@ -604,6 +562,10 @@ plugins:
|
||||
# You *will need* to configure it via the "antispam:" configuration block below.
|
||||
#- antispam
|
||||
|
||||
# Servprotect plugin: disconnects from networks if too many kills or nick collisions to
|
||||
# PyLink clients are received.
|
||||
#- servprotect
|
||||
|
||||
# Global plugin: Janus-style global plugin; announces messages to all channels the PyLink
|
||||
# client is in.
|
||||
#- global
|
||||
@ -651,22 +613,18 @@ logging:
|
||||
"#services":
|
||||
loglevel: INFO
|
||||
|
||||
# The directory where the log files are written. The path can be relative to the directory
|
||||
# PyLink is run from. Defaults to "log/".
|
||||
#log_dir: "log"
|
||||
|
||||
files:
|
||||
# Logs to file targets. These will be placed in the folder specified by log_dir with a
|
||||
# filename based on the current instance name and the target name:
|
||||
# instancename-targetname.log
|
||||
# Logs to file targets. These will be placed in the log/ folder in the
|
||||
# PyLink directory, with a filename based on the current instance name
|
||||
# and the target name: instancename-targetname.log
|
||||
|
||||
# When running with "pylink", this will create log/pylink-errors.log
|
||||
# When running with "pylink someconf.yml", this will create log/someconf-errors.log
|
||||
# When running with ./pylink, this will create log/pylink-errors.log
|
||||
# When running with ./pylink someconf.yml, this will create log/someconf-errors.log
|
||||
"errors":
|
||||
loglevel: ERROR
|
||||
|
||||
# Ditto above. When running with "pylink", it will use log/pylink-commands.log
|
||||
# When running with "pylink someconf.yml", this will create log/someconf-commands.log
|
||||
# Ditto above. When running with ./pylink, it will use log/pylink-commands.log
|
||||
# When running with ./pylink someconf.yml, this will create log/someconf-commands.log
|
||||
"commands":
|
||||
loglevel: INFO
|
||||
|
||||
@ -680,8 +638,8 @@ logging:
|
||||
# in the format pylink-commands.log, pylink-commands.log.1, pylink-commands.log.2, etc.
|
||||
# If either max_bytes or backup_count is 0, log rotation will be disabled.
|
||||
|
||||
# Max amount of bytes per file before rotation is done. Defaults to 20 MiB (20971520 bytes).
|
||||
#max_bytes: 20971520
|
||||
# Max amount of bytes per file, before rotation is done. Defaults to 50 MiB (52428800 bytes).
|
||||
#max_bytes: 52428800
|
||||
|
||||
# Amount of backups to make. Defaults to 5.
|
||||
#backup_count: 5
|
||||
@ -693,48 +651,44 @@ changehost:
|
||||
# Sets the networks where Changehost should be enabled. Please note: changehost does NOT support
|
||||
# arbitrarily cloaking clients introduced by PyLink (e.g. relay clients), as doing so would make
|
||||
# ban matching impossible. In these cases, it is the remote admin's job to turn on cloaking on
|
||||
# their IRCd.
|
||||
# You can also add to this list of enabled networks by setting "servers::<server name>::changehost_enable"
|
||||
# to true.
|
||||
# their IRCd!
|
||||
enabled_nets:
|
||||
- inspnet
|
||||
- ts6net
|
||||
|
||||
# Sets the networks where Changehost hosts should be enforced: that is, any attempts
|
||||
# by the user or other services to overwrite a host will be reverted.
|
||||
# You can also add to this list of enabled networks by setting "servers::<server name>::changehost_enforce"
|
||||
# to true.
|
||||
#enforced_nets:
|
||||
# - inspnet
|
||||
|
||||
# Sets the masks that Changehost enforcement should ignore: these can be users with certain
|
||||
# hosts, exttargets, etc.
|
||||
# Since PyLink 2.1, you can also add to this list on a per-network basis by adding options under
|
||||
# "servers::<server name>::changehost_enforce_exceptions".
|
||||
enforce_exceptions:
|
||||
- "*!*@yournet/staff/*"
|
||||
#- "$account"
|
||||
|
||||
# Determines whether Changehost rules should also match the host portion of a mask by IP and
|
||||
# real hosts. These default to false. You can override these on a per-network basis by setting
|
||||
# "servers::<server name>::changehost_match_ip" or "servers::<server name>::changehost_match_realhosts".
|
||||
# real hosts. These default to false.
|
||||
#match_ip: false
|
||||
#match_realhosts: false
|
||||
|
||||
# This sets the hostmasks that Changehost should look for. Whenever someone with a matching nick!user@host
|
||||
# connects, their host will be set to the text defined. The following substitutions are available here:
|
||||
# This sets the hostmasks that Changehost should look for. Whenever someone
|
||||
# with a matching nick!user@host connects, their host will be set to the
|
||||
# text defined. The following substitutions are available here:
|
||||
# $uid, $ts (time of connection), $nick, $realhost, $ident, and $ip.
|
||||
# Invalid characters in hosts are replaced with a "-".
|
||||
# Also, make sure you quote each entry so the YAML parser treats them as raw strings.
|
||||
# Since PyLink 2.1, you can also add to this list on a per-network basis by adding options under
|
||||
# "servers::<server name>::changehost_hosts".
|
||||
# Also, make sure you quote each entry so the YAML parser treats them as
|
||||
# raw strings.
|
||||
hosts:
|
||||
# Here are some examples. Note that to keep your users' details private, you should probably refrain
|
||||
# from using $ip or $realhost, in these hostmasks, unless cloaking is already disabled.
|
||||
|
||||
# Here are some examples. Note that to keep your users' details
|
||||
# private, you should probably refrain from using $ip or $realhost,
|
||||
# in these hostmasks, unless cloaking is already disabled.
|
||||
"*!yourname@*.yournet.com": "$nick.opers.yournet.com"
|
||||
"*!*@localhost": "some-server.hostname"
|
||||
|
||||
# Freenode-style masks are possible with this (though without the hashing)
|
||||
# Freenode-style masks are possible with this (though without the
|
||||
# hashing)
|
||||
"*!*@bnc-server.yournet.com": "yournet/bnc-users/$ident"
|
||||
"*!*@ircip?.mibbit.com": "$ident.$realhost"
|
||||
"WebchatUser*!*@*": "webchat/users/$ident"
|
||||
@ -966,7 +920,7 @@ stats:
|
||||
|
||||
#masshighlight:
|
||||
# This block configures options for antispam's mass highlight blocking. It can also be
|
||||
# overridden (as an entire block) per-network by copying its options to:
|
||||
# overridden (as an entire block) per-network by copying its options under
|
||||
# servers::<server name>::antispam_masshighlight
|
||||
|
||||
# Determines whether mass highlight prevention should be enabled. Defaults to false if not
|
||||
@ -988,7 +942,7 @@ stats:
|
||||
|
||||
#textfilter:
|
||||
# This block configures options for antispam's text spamfilters. It can also be overridden
|
||||
# (as an entire block) per-network by copying its options to:
|
||||
# (as an entire block) per-network by copying its options under
|
||||
# servers::<server name>::antispam_textfilter
|
||||
|
||||
# Determines whether text spamfilters should be enabled. Defaults to false if not set.
|
||||
@ -997,15 +951,11 @@ stats:
|
||||
# Sets the punishment that Antispam's text spamfilter should use.
|
||||
# Valid values include "kill", "kick", "ban", "quiet", and combinations of these strung
|
||||
# together with "+" (e.g. "kick+ban"). Defaults to "kick+ban+block" if not set.
|
||||
# If you want to use Antispam with PM monitoring (see the "watch_pms" option below), you
|
||||
# will want to include at least one punishment that is not channel-specific
|
||||
# ("kill" or "block").
|
||||
# Punishments not supported in a context or network (e.g. kicking for PM spam or "quiet"
|
||||
# on a network that doesn't support it) will be silently dropped if others succeed.
|
||||
# If no punishments succeed, then a warning will be logged.
|
||||
# If you want Antispam to also monitor PM spam, you will want to change this to something
|
||||
# not channel-specific (such as "kill" or "block").
|
||||
#punishment: kick+ban+block
|
||||
|
||||
# Sets the kick / kill message used when a message triggers the text spamfilter.
|
||||
# Sets the kick / kill message used when the text spamfilter is triggered.
|
||||
#reason: "Spam is prohibited"
|
||||
|
||||
# Determines whether PMs to PyLink clients should also be tracked to prevent spam.
|
||||
@ -1018,33 +968,11 @@ stats:
|
||||
# This defaults to false if not set.
|
||||
#watch_pms: false
|
||||
|
||||
# Configures a case-insensitive list of bad strings to block in messages (PRIVMSG, NOTICE).
|
||||
# Configures an (ASCII case-insensitive) list of bad strings to block in messages (PRIVMSG, NOTICE).
|
||||
# Globs are supported; use them to your advantage here.
|
||||
# You can also define server specific lists of bad strings by defining
|
||||
# servers::<server name>::antispam_textfilter_globs
|
||||
# servers::<server name>::antispam_textfilters_globs
|
||||
# the contents of which will be *merged* into the global list of bad strings specified below.
|
||||
#textfilter_globs:
|
||||
# - "*very bad don't say this*"
|
||||
# - "TWFkZSB5b3UgbG9vayE="
|
||||
|
||||
#partquit:
|
||||
# This configures Antispam's part / quit message filter for plugins like Relay. It can also be
|
||||
# overridden (as an entire block) per-network by copying its options to:
|
||||
# servers::<server name>::antispam_partquit
|
||||
|
||||
# Determines whether part and quit messages matching any text spamfilters will be filtered.
|
||||
# These default to true if not set.
|
||||
#watch_parts: false
|
||||
#watch_quits: false
|
||||
|
||||
# Sets the message to use when a part or quit message is filtered.
|
||||
#part_filter_message: Reason filtered
|
||||
#quit_filter_message: Reason filtered
|
||||
|
||||
# Configures a case-insensitive list of bad strings to block in part and quit messages.
|
||||
# Globs are supported; use them to your advantage here.
|
||||
# You can also define server specific lists of bad strings by defining
|
||||
# servers::<server name>::antispam_partquit_globs
|
||||
# the contents of which will be *merged* into the global list of bad strings specified below.
|
||||
#partquit_globs:
|
||||
# - "*some-spammy-site.xyz*"
|
||||
|
32
launcher.py
32
launcher.py
@ -4,11 +4,10 @@ PyLink IRC Services launcher.
|
||||
"""
|
||||
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import signal
|
||||
import time
|
||||
|
||||
from pylinkirc import __version__, conf, real_version, world
|
||||
from pylinkirc import world, conf, __version__, real_version
|
||||
|
||||
try:
|
||||
import psutil
|
||||
@ -18,6 +17,7 @@ except ImportError:
|
||||
args = {}
|
||||
|
||||
def _main():
|
||||
# FIXME: we can't pass logging on to conf until we set up the config...
|
||||
conf.load_conf(args.config)
|
||||
|
||||
from pylinkirc.log import log
|
||||
@ -25,33 +25,28 @@ def _main():
|
||||
|
||||
# Write and check for an existing PID file unless specifically told not to.
|
||||
if not args.no_pid:
|
||||
pid_dir = conf.conf['pylink'].get('pid_dir', '')
|
||||
pidfile = os.path.join(pid_dir, '%s.pid' % conf.confname)
|
||||
pid_exists = False
|
||||
pidfile = '%s.pid' % conf.confname
|
||||
has_pid = False
|
||||
pid = None
|
||||
if os.path.exists(pidfile):
|
||||
try:
|
||||
with open(pidfile) as f:
|
||||
pid = int(f.read())
|
||||
except OSError:
|
||||
log.exception("Could not read PID file %s:", pidfile)
|
||||
else:
|
||||
pid_exists = True
|
||||
|
||||
has_pid = True
|
||||
if psutil is not None and os.name == 'posix':
|
||||
# FIXME: Haven't tested this on other platforms, so not turning it on by default.
|
||||
with open(pidfile) as f:
|
||||
try:
|
||||
pid = int(f.read())
|
||||
proc = psutil.Process(pid)
|
||||
except psutil.NoSuchProcess: # Process doesn't exist!
|
||||
pid_exists = False
|
||||
has_pid = False
|
||||
log.info("Ignoring stale PID %s from PID file %r: no such process exists.", pid, pidfile)
|
||||
else:
|
||||
# This PID got reused for something that isn't us?
|
||||
if not any('pylink' in arg.lower() for arg in proc.cmdline()):
|
||||
log.info("Ignoring stale PID %s from PID file %r: process command line %r is not us", pid, pidfile, proc.cmdline())
|
||||
pid_exists = False
|
||||
has_pid = False
|
||||
|
||||
if pid and pid_exists:
|
||||
if has_pid:
|
||||
if args.rehash:
|
||||
os.kill(pid, signal.SIGUSR1)
|
||||
log.info('OK, rehashed PyLink instance %s (config %r)', pid, args.config)
|
||||
@ -84,7 +79,7 @@ def _main():
|
||||
world._should_remove_pid = True
|
||||
log.error('Cannot stop/rehash PyLink: no process with PID %s exists.', pid)
|
||||
else:
|
||||
log.error('Cannot stop/rehash PyLink: PID file %r does not exist or cannot be read.', pidfile)
|
||||
log.error('Cannot stop/rehash PyLink: PID file %r does not exist.', pidfile)
|
||||
sys.exit(1)
|
||||
|
||||
world._should_remove_pid = True
|
||||
@ -122,7 +117,6 @@ def _main():
|
||||
elif os.name == 'posix':
|
||||
sys.stdout.write("\x1b]2;PyLink %s\x07" % __version__)
|
||||
|
||||
if not args.no_pid:
|
||||
# Write the PID file only after forking.
|
||||
with open(pidfile, 'w') as f:
|
||||
f.write(str(os.getpid()))
|
||||
@ -178,7 +172,7 @@ def main():
|
||||
parser.add_argument("-r", "--restart", help="restarts the PyLink instance with the given config file", action='store_true')
|
||||
parser.add_argument("-s", "--stop", help="stops the PyLink instance with the given config file", action='store_true')
|
||||
parser.add_argument("-R", "--rehash", help="rehashes the PyLink instance with the given config file", action='store_true')
|
||||
parser.add_argument("-d", "--daemonize", help="daemonizes the PyLink instance on POSIX systems", action='store_true')
|
||||
parser.add_argument("-d", "--daemonize", help="[experimental] daemonizes the PyLink instance on POSIX systems", action='store_true')
|
||||
parser.add_argument("-t", "--trace", help="traces through running Python code; useful for debugging", action='store_true')
|
||||
parser.add_argument('--trace-ignore-mods', help='comma-separated list of extra modules to ignore when tracing', action='store', default='')
|
||||
parser.add_argument('--trace-ignore-dirs', help='comma-separated list of extra directories to ignore when tracing', action='store', default='')
|
||||
|
26
log.py
26
log.py
@ -10,13 +10,14 @@ import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
|
||||
from . import conf, world
|
||||
|
||||
__all__ = ['log']
|
||||
from . import world, conf
|
||||
|
||||
# Stores a list of active file loggers.
|
||||
fileloggers = []
|
||||
|
||||
logdir = os.path.join(os.getcwd(), 'log')
|
||||
os.makedirs(logdir, exist_ok=True)
|
||||
|
||||
# TODO: perhaps make this format configurable?
|
||||
_format = '%(asctime)s [%(levelname)s] %(message)s'
|
||||
logformatter = logging.Formatter(_format)
|
||||
@ -34,7 +35,7 @@ world.console_handler.setFormatter(logformatter)
|
||||
world.console_handler.setLevel(_get_console_log_level())
|
||||
|
||||
# Get the main logger object; plugins can import this variable for convenience.
|
||||
log = logging.getLogger('pylinkirc')
|
||||
log = logging.getLogger()
|
||||
log.addHandler(world.console_handler)
|
||||
|
||||
# This is confusing, but we have to set the root logger to accept all events. Only this way
|
||||
@ -46,28 +47,20 @@ def _make_file_logger(filename, level=None):
|
||||
"""
|
||||
Initializes a file logging target with the given filename and level.
|
||||
"""
|
||||
logconf = conf.conf.get('logging', {})
|
||||
|
||||
logdir = logconf.get('log_dir')
|
||||
if logdir is None:
|
||||
logdir = os.path.join(os.getcwd(), 'log')
|
||||
|
||||
os.makedirs(logdir, exist_ok=True)
|
||||
|
||||
# Use log names specific to the current instance, to prevent multiple
|
||||
# PyLink instances from overwriting each others' log files.
|
||||
target = os.path.join(logdir, '%s-%s.log' % (conf.confname, filename))
|
||||
|
||||
logrotconf = logconf.get('filerotation', {})
|
||||
logrotconf = conf.conf.get('logging', {}).get('filerotation', {})
|
||||
|
||||
# Max amount of bytes per file, before rotation is done. Defaults to 20 MiB.
|
||||
maxbytes = logrotconf.get('max_bytes', 20971520)
|
||||
# Max amount of bytes per file, before rotation is done. Defaults to 50 MiB.
|
||||
maxbytes = logrotconf.get('max_bytes', 52428800)
|
||||
|
||||
# Amount of backups to make (e.g. pylink-debug.log, pylink-debug.log.1, pylink-debug.log.2, ...)
|
||||
# Defaults to 5.
|
||||
backups = logrotconf.get('backup_count', 5)
|
||||
|
||||
filelogger = logging.handlers.RotatingFileHandler(target, maxBytes=maxbytes, backupCount=backups, encoding='utf-8')
|
||||
filelogger = logging.handlers.RotatingFileHandler(target, maxBytes=maxbytes, backupCount=backups)
|
||||
filelogger.setFormatter(logformatter)
|
||||
|
||||
# If no log level is specified, use the same one as the console logger.
|
||||
@ -160,3 +153,4 @@ class PyLinkChannelLogger(logging.Handler):
|
||||
return
|
||||
else:
|
||||
self.called = False
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
# antispam.py: Basic services-side spamfilters for IRC
|
||||
|
||||
from pylinkirc import conf, utils
|
||||
import ircmatch
|
||||
|
||||
from pylinkirc import utils, world, conf
|
||||
from pylinkirc.log import log
|
||||
|
||||
mydesc = ("Provides anti-spam functionality.")
|
||||
@ -9,78 +11,6 @@ sbot = utils.register_service("antispam", default_nick="AntiSpam", desc=mydesc)
|
||||
def die(irc=None):
|
||||
utils.unregister_service("antispam")
|
||||
|
||||
_UNICODE_CHARMAP = {
|
||||
'A': 'AΑАᎪᗅᴀ𝐀𝐴𝑨𝒜𝓐𝔄𝔸𝕬𝖠𝗔𝘈𝘼𝙰𝚨𝛢𝜜𝝖𝞐',
|
||||
'B': 'BʙΒВвᏴᗷᛒℬ𐌁𝐁𝐵𝑩𝓑𝔅𝔹𝕭𝖡𝗕𝘉𝘽𝙱𝚩𝛣𝜝𝝗𝞑',
|
||||
'C': 'CϹСᏟℂℭⅭⲤ𐌂𝐂𝐶𝑪𝒞𝓒𝕮𝖢𝗖𝘊𝘾𝙲',
|
||||
'D': 'DᎠᗞᗪᴅⅅⅮ𝐃𝐷𝑫𝒟𝓓𝔇𝔻𝕯𝖣𝗗𝘋𝘿𝙳',
|
||||
'E': 'EΕЕᎬᴇℰ⋿ⴹ𝐄𝐸𝑬𝓔𝔈𝔼𝕰𝖤𝗘𝘌𝙀𝙴𝚬𝛦𝜠𝝚𝞔',
|
||||
'F': 'FϜᖴℱ𝐅𝐹𝑭𝓕𝔉𝔽𝕱𝖥𝗙𝘍𝙁𝙵𝟊',
|
||||
'G': 'GɢԌԍᏀᏳ𝐆𝐺𝑮𝒢𝓖𝔊𝔾𝕲𝖦𝗚𝘎𝙂𝙶',
|
||||
'H': 'HʜΗНнᎻᕼℋℌℍⲎ𝐇𝐻𝑯𝓗𝕳𝖧𝗛𝘏𝙃𝙷𝚮𝛨𝜢𝝜𝞖',
|
||||
'J': 'JЈᎫᒍᴊ𝐉𝐽𝑱𝒥𝓙𝔍𝕁𝕵𝖩𝗝𝘑𝙅𝙹',
|
||||
'K': 'KΚКᏦᛕKⲔ𝐊𝐾𝑲𝒦𝓚𝔎𝕂𝕶𝖪𝗞𝘒𝙆𝙺𝚱𝛫𝜥𝝟𝞙',
|
||||
'L': 'LʟᏞᒪℒⅬ𝐋𝐿𝑳𝓛𝔏𝕃𝕷𝖫𝗟𝘓𝙇𝙻',
|
||||
'M': 'MΜϺМᎷᗰᛖℳⅯⲘ𐌑𝐌𝑀𝑴𝓜𝔐𝕄𝕸𝖬𝗠𝘔𝙈𝙼𝚳𝛭𝜧𝝡𝞛',
|
||||
'N': 'NɴΝℕⲚ𝐍𝑁𝑵𝒩𝓝𝔑𝕹𝖭𝗡𝘕𝙉𝙽𝚴𝛮𝜨𝝢𝞜',
|
||||
'P': 'PΡРᏢᑭᴘᴩℙⲢ𝐏𝑃𝑷𝒫𝓟𝔓𝕻𝖯𝗣𝘗𝙋𝙿𝚸𝛲𝜬𝝦𝞠',
|
||||
'Q': 'Qℚⵕ𝐐𝑄𝑸𝒬𝓠𝔔𝕼𝖰𝗤𝘘𝙌𝚀',
|
||||
'R': 'RƦʀᎡᏒᖇᚱℛℜℝ𝐑𝑅𝑹𝓡𝕽𝖱𝗥𝘙𝙍𝚁',
|
||||
'S': 'SЅՏᏕᏚ𝐒𝑆𝑺𝒮𝓢𝔖𝕊𝕾𝖲𝗦𝘚𝙎𝚂',
|
||||
'T': 'TΤτТтᎢᴛ⊤⟙Ⲧ𐌕𝐓𝑇𝑻𝒯𝓣𝔗𝕋𝕿𝖳𝗧𝘛𝙏𝚃𝚻𝛕𝛵𝜏𝜯𝝉𝝩𝞃𝞣𝞽',
|
||||
'U': 'UՍሀᑌ∪⋃𝐔𝑈𝑼𝒰𝓤𝔘𝕌𝖀𝖴𝗨𝘜𝙐𝚄',
|
||||
'V': 'VѴ٧۷ᏙᐯⅤⴸ𝐕𝑉𝑽𝒱𝓥𝔙𝕍𝖁𝖵𝗩𝘝𝙑𝚅',
|
||||
'W': 'WԜᎳᏔ𝐖𝑊𝑾𝒲𝓦𝔚𝕎𝖂𝖶𝗪𝘞𝙒𝚆',
|
||||
'X': 'XΧХ᙭ᚷⅩ╳Ⲭⵝ𐌗𐌢𝐗𝑋𝑿𝒳𝓧𝔛𝕏𝖃𝖷𝗫𝘟𝙓𝚇𝚾𝛸𝜲𝝬𝞦',
|
||||
'Y': 'YΥϒУҮᎩᎽⲨ𝐘𝑌𝒀𝒴𝓨𝔜𝕐𝖄𝖸𝗬𝘠𝙔𝚈𝚼𝛶𝜰𝝪𝞤',
|
||||
'Z': 'ZΖᏃℤℨ𝐙𝑍𝒁𝒵𝓩𝖅𝖹𝗭𝘡𝙕𝚉𝚭𝛧𝜡𝝛𝞕',
|
||||
'a': 'aɑαа⍺𝐚𝑎𝒂𝒶𝓪𝔞𝕒𝖆𝖺𝗮𝘢𝙖𝚊𝛂𝛼𝜶𝝰𝞪',
|
||||
'b': 'bƄЬᏏᖯ𝐛𝑏𝒃𝒷𝓫𝔟𝕓𝖇𝖻𝗯𝘣𝙗𝚋',
|
||||
'c': 'cϲсᴄⅽⲥ𝐜𝑐𝒄𝒸𝓬𝔠𝕔𝖈𝖼𝗰𝘤𝙘𝚌',
|
||||
'd': 'ⅾdԁᏧᑯⅆⅾ𝐝𝑑𝒅𝒹𝓭𝔡𝕕𝖉𝖽𝗱𝘥𝙙𝚍',
|
||||
'e': 'eеҽ℮ℯⅇ𝐞𝑒𝒆𝓮𝔢𝕖𝖊𝖾𝗲𝘦𝙚𝚎ᥱ',
|
||||
'f': 'fſϝքẝ𝐟𝑓𝒇𝒻𝓯𝔣𝕗𝖋𝖿𝗳𝘧𝙛𝚏𝟋',
|
||||
'g': 'gƍɡցᶃℊ𝐠𝑔𝒈𝓰𝔤𝕘𝖌𝗀𝗴𝘨𝙜𝚐',
|
||||
'h': 'hһհᏂℎ𝐡𝒉𝒽𝓱𝔥𝕙𝖍𝗁𝗵𝘩𝙝𝚑',
|
||||
'i': 'iıɩɪιіӏᎥℹⅈⅰ⍳ꙇ𝐢𝑖𝒊𝒾𝓲𝔦𝕚𝖎𝗂𝗶𝘪𝙞𝚒𝚤𝛊𝜄𝜾𝝸𝞲',
|
||||
'j': 'jϳјⅉ𝐣𝑗𝒋𝒿𝓳𝔧𝕛𝖏𝗃𝗷𝘫𝙟𝚓',
|
||||
'k': 'k𝐤𝑘𝒌𝓀𝓴𝔨𝕜𝖐𝗄𝗸𝘬𝙠𝚔',
|
||||
'l': 'ⅼ',
|
||||
'm': 'ⅿm',
|
||||
'n': 'nոռ𝐧𝑛𝒏𝓃𝓷𝔫𝕟𝖓𝗇𝗻𝘯𝙣𝚗ᥒ',
|
||||
'o': 'ⲟഠοо',
|
||||
'p': 'pρϱр⍴ⲣ𝐩𝑝𝒑𝓅𝓹𝔭𝕡𝖕𝗉𝗽𝘱𝙥𝚙𝛒𝛠𝜌𝜚𝝆𝝔𝞀𝞎𝞺𝟈',
|
||||
'q': 'qԛգզ𝐪𝑞𝒒𝓆𝓺𝔮𝕢𝖖𝗊𝗾𝘲𝙦𝚚',
|
||||
'r': 'rгᴦⲅ𝐫𝑟𝒓𝓇𝓻𝔯𝕣𝖗𝗋𝗿𝘳𝙧𝚛',
|
||||
's': 'sƽѕꜱ𝐬𝑠𝒔𝓈𝓼𝔰𝕤𝖘𝗌𝘀𝘴𝙨𝚜',
|
||||
't': 't𝐭𝑡𝒕𝓉𝓽𝔱𝕥𝖙𝗍𝘁𝘵𝙩𝚝',
|
||||
'u': 'uʋυսᴜ𝐮𝑢𝒖𝓊𝓾𝔲𝕦𝖚𝗎𝘂𝘶𝙪𝚞𝛖𝜐𝝊𝞄𝞾ᥙ',
|
||||
'v': 'vνѵטᴠⅴ∨⋁𝐯𝑣𝒗𝓋𝓿𝔳𝕧𝖛𝗏𝘃𝘷𝙫𝚟𝛎𝜈𝝂𝝼𝞶',
|
||||
'w': 'wɯѡԝաᴡ𝐰𝑤𝒘𝓌𝔀𝔴𝕨𝖜𝗐𝘄𝘸𝙬𝚠',
|
||||
'x': 'x×хᕁᕽ᙮ⅹ⤫⤬⨯𝐱𝑥𝒙𝓍𝔁𝔵𝕩𝖝𝗑𝘅𝘹𝙭𝚡',
|
||||
'y': 'yɣʏγуүყᶌỿℽ𝐲𝑦𝒚𝓎𝔂𝔶𝕪𝖞𝗒𝘆𝘺𝙮𝚢𝛄𝛾𝜸𝝲𝞬',
|
||||
'z': 'zᴢ𝐳𝑧𝒛𝓏𝔃𝔷𝕫𝖟𝗓𝘇𝘻𝙯𝚣',
|
||||
'/': '᜵⁄∕⧸/',
|
||||
'\\': '⧵﹨⧹\',
|
||||
' ': '\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\xa0\u202f\u205f',
|
||||
'.': '․.',
|
||||
'-': '˗╴﹣-−⎼',
|
||||
'!': '﹗!ǃⵑ︕',
|
||||
':': ':˸։፡᛬⁚∶⠆︓﹕',
|
||||
'#': '#﹟'
|
||||
}
|
||||
|
||||
def _prep_maketrans(data):
|
||||
from_s = ''
|
||||
to_s = ''
|
||||
for target, chars in data.items():
|
||||
from_s += chars
|
||||
to_s += target * len(chars)
|
||||
|
||||
return str.maketrans(from_s, to_s)
|
||||
|
||||
UNICODE_CHARMAP = _prep_maketrans(_UNICODE_CHARMAP)
|
||||
|
||||
PUNISH_OPTIONS = ['kill', 'ban', 'quiet', 'kick', 'block']
|
||||
EXEMPT_OPTIONS = ['voice', 'halfop', 'op']
|
||||
DEFAULT_EXEMPT_OPTION = 'halfop'
|
||||
@ -285,9 +215,8 @@ utils.add_hook(handle_masshighlight, 'NOTICE', priority=1000)
|
||||
TEXTFILTER_DEFAULTS = {
|
||||
'reason': "Spam is prohibited",
|
||||
'punishment': 'kick+ban+block',
|
||||
'watch_pms': False,
|
||||
'enabled': False,
|
||||
'munge_unicode': True,
|
||||
'watch_pms': 'false',
|
||||
'enabled': False
|
||||
}
|
||||
def handle_textfilter(irc, source, command, args):
|
||||
"""Antispam text filter handler."""
|
||||
@ -348,12 +277,10 @@ def handle_textfilter(irc, source, command, args):
|
||||
|
||||
if irc.get_service_option('antispam', 'strip_formatting', True):
|
||||
text = utils.strip_irc_formatting(text)
|
||||
if txf_settings.get('munge_unicode', TEXTFILTER_DEFAULTS['munge_unicode']):
|
||||
text = str.translate(text, UNICODE_CHARMAP)
|
||||
|
||||
punished = False
|
||||
for filterglob in txf_globs:
|
||||
if utils.match_text(filterglob, text):
|
||||
if ircmatch.match(1, filterglob, text):
|
||||
log.info("(%s) antispam: punishing %s => %s for text filter %r",
|
||||
irc.name,
|
||||
irc.get_friendly_name(source),
|
||||
@ -366,44 +293,3 @@ def handle_textfilter(irc, source, command, args):
|
||||
|
||||
utils.add_hook(handle_textfilter, 'PRIVMSG', priority=999)
|
||||
utils.add_hook(handle_textfilter, 'NOTICE', priority=999)
|
||||
|
||||
PARTQUIT_DEFAULTS = {
|
||||
'watch_quits': True,
|
||||
'watch_parts': True,
|
||||
'part_filter_message': "Reason filtered",
|
||||
'quit_filter_message': "Reason filtered",
|
||||
}
|
||||
def handle_partquit(irc, source, command, args):
|
||||
"""Antispam part/quit message filter."""
|
||||
text = args.get('text')
|
||||
pq_settings = irc.get_service_option('antispam', 'partquit',
|
||||
PARTQUIT_DEFAULTS)
|
||||
|
||||
if not text:
|
||||
return # No text to match against
|
||||
elif command == 'QUIT' and not pq_settings.get('watch_quits', True):
|
||||
return # Not enabled
|
||||
elif command == 'PART' and not pq_settings.get('watch_parts', True):
|
||||
return
|
||||
|
||||
# Merge together global and local partquit filter lists.
|
||||
pq_globs = set(conf.conf.get('antispam', {}).get('partquit_globs', [])) | \
|
||||
set(irc.serverdata.get('antispam_partquit_globs', []))
|
||||
if not pq_globs:
|
||||
return
|
||||
|
||||
for filterglob in pq_globs:
|
||||
if utils.match_text(filterglob, text):
|
||||
# For parts, also log the affected channels
|
||||
if command == 'PART':
|
||||
filtered_message = pq_settings.get('part_filter_message', PARTQUIT_DEFAULTS['part_filter_message'])
|
||||
log.info('(%s) antispam: filtered part message from %s on %s due to part/quit filter glob %s',
|
||||
irc.name, irc.get_hostmask(source), ','.join(args['channels']), filterglob)
|
||||
else:
|
||||
filtered_message = pq_settings.get('quit_filter_message', PARTQUIT_DEFAULTS['quit_filter_message'])
|
||||
log.info('(%s) antispam: filtered quit message from %s due to part/quit filter glob %s',
|
||||
irc.name, args['userdata'].nick, filterglob)
|
||||
args['text'] = filtered_message
|
||||
break
|
||||
utils.add_hook(handle_partquit, 'PART', priority=999)
|
||||
utils.add_hook(handle_partquit, 'QUIT', priority=999)
|
||||
|
@ -3,11 +3,10 @@ automode.py - Provide simple channel ACL management by giving prefix modes to us
|
||||
hostmasks or exttargets.
|
||||
"""
|
||||
import collections
|
||||
import string
|
||||
|
||||
from pylinkirc import conf, structures, utils, world
|
||||
from pylinkirc.coremods import permissions
|
||||
from pylinkirc import utils, conf, world, structures
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import permissions
|
||||
|
||||
mydesc = ("The \x02Automode\x02 plugin provides simple channel ACL management by giving prefix modes "
|
||||
"to users matching hostmasks or exttargets.")
|
||||
@ -95,16 +94,10 @@ def _check_automode_access(irc, uid, channel, command):
|
||||
|
||||
def match(irc, channel, uids=None):
|
||||
"""
|
||||
Set modes on matching users. If uids is not given, check all users in the channel and give
|
||||
them modes as needed.
|
||||
Automode matcher engine.
|
||||
"""
|
||||
if isinstance(channel, int) or str(channel).startswith(tuple(string.digits)):
|
||||
channel = '#' + str(channel) # Mangle channels on networks where they're stored as an ID
|
||||
dbentry = db.get(irc.name+channel)
|
||||
if not irc.has_cap('has-irc-modes'):
|
||||
log.debug('(%s) automode: skipping match() because IRC modes are not supported on this protocol', irc.name)
|
||||
return
|
||||
elif dbentry is None:
|
||||
if dbentry is None:
|
||||
return
|
||||
|
||||
modebot_uid = modebot.uids.get(irc.name)
|
||||
@ -169,13 +162,10 @@ utils.add_hook(handle_services_login, 'PYLINK_RELAY_SERVICES_LOGIN')
|
||||
|
||||
def _get_channel_pair(irc, source, chanpair, perm=None):
|
||||
"""
|
||||
Fetches the network and channel given a channel pair, also optionally checking the caller's permissions.
|
||||
Fetches the network and channel given a channel pair,
|
||||
also optionally checking the caller's permissions.
|
||||
"""
|
||||
log.debug('(%s) Looking up chanpair %s', irc.name, chanpair)
|
||||
|
||||
if '#' not in chanpair and chanpair.startswith(tuple(string.digits)):
|
||||
chanpair = '#' + chanpair # Mangle channels on networks where they're stored by ID
|
||||
|
||||
try:
|
||||
network, channel = chanpair.split('#', 1)
|
||||
except ValueError:
|
||||
@ -183,6 +173,9 @@ def _get_channel_pair(irc, source, chanpair, perm=None):
|
||||
channel = '#' + channel
|
||||
channel = irc.to_lower(channel)
|
||||
|
||||
if not irc.is_channel(channel):
|
||||
raise ValueError("Invalid channel name %s." % channel)
|
||||
|
||||
if network:
|
||||
ircobj = world.networkobjects.get(network)
|
||||
else:
|
||||
@ -218,9 +211,6 @@ def setacc(irc, source, args):
|
||||
|
||||
\x02SETACC #staffchan $channel:#mainchan:op o
|
||||
"""
|
||||
if not irc.has_cap('has-irc-modes'):
|
||||
error(irc, "IRC style modes are not supported on this protocol.")
|
||||
return
|
||||
|
||||
try:
|
||||
chanpair, mask, modes = args
|
||||
@ -241,7 +231,7 @@ def setacc(irc, source, args):
|
||||
reply(irc, "Done. \x02%s\x02 now has modes \x02+%s\x02 in \x02%s\x02." % (mask, modes, channel))
|
||||
|
||||
# Join the Automode bot to the channel persistently.
|
||||
modebot.add_persistent_channel(ircobj, 'automode', channel)
|
||||
modebot.add_persistent_channel(irc, 'automode', channel)
|
||||
|
||||
modebot.add_cmd(setacc, aliases=('setaccess', 'set'), featured=True)
|
||||
|
||||
@ -295,7 +285,7 @@ def delacc(irc, source, args):
|
||||
if not dbentry:
|
||||
log.debug("Automode: purging empty channel pair %s/%s", ircobj.name, channel)
|
||||
del db[ircobj.name+channel]
|
||||
modebot.remove_persistent_channel(ircobj, 'automode', channel)
|
||||
modebot.remove_persistent_channel(irc, 'automode', channel)
|
||||
|
||||
modebot.add_cmd(delacc, aliases=('delaccess', 'del'), featured=True)
|
||||
|
||||
@ -375,7 +365,7 @@ def clearacc(irc, source, args):
|
||||
del db[ircobj.name+channel]
|
||||
log.info('(%s) %s cleared modes on %s', ircobj.name, irc.get_hostmask(source), channel)
|
||||
reply(irc, "Done. Removed all Automode access entries for \x02%s\x02." % channel)
|
||||
modebot.remove_persistent_channel(ircobj, 'automode', channel)
|
||||
modebot.remove_persistent_channel(irc, 'automode', channel)
|
||||
else:
|
||||
error(irc, "No Automode access entries exist for \x02%s\x02." % channel)
|
||||
|
||||
|
@ -3,9 +3,9 @@ bots.py: Spawn virtual users/bots on a PyLink server and make them interact
|
||||
with things.
|
||||
"""
|
||||
from pylinkirc import utils
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import permissions
|
||||
|
||||
|
||||
@utils.add_cmd
|
||||
def spawnclient(irc, source, args):
|
||||
"""<nick> <ident> <host>
|
||||
@ -39,10 +39,7 @@ def quit(irc, source, args):
|
||||
irc.error("Not enough arguments. Needs 1-2: nick, reason (optional).")
|
||||
return
|
||||
|
||||
u = irc.nick_to_uid(nick, filterfunc=irc.is_internal_client)
|
||||
if u is None:
|
||||
irc.error("Unknown user %r" % nick)
|
||||
return
|
||||
u = irc.nick_to_uid(nick)
|
||||
|
||||
if irc.pseudoclient.uid == u:
|
||||
irc.error("Cannot quit the main PyLink client!")
|
||||
@ -72,13 +69,13 @@ def joinclient(irc, source, args):
|
||||
try:
|
||||
# Check if the first argument is an existing PyLink client. If it is not,
|
||||
# then assume that the first argument was actually the channels being joined.
|
||||
u = irc.nick_to_uid(args[0], filterfunc=irc.is_internal_client)
|
||||
u = irc.nick_to_uid(args[0])
|
||||
|
||||
if u is None: # First argument isn't one of our clients
|
||||
if not irc.is_internal_client(u): # First argument isn't one of our clients
|
||||
raise IndexError
|
||||
|
||||
clist = args[1]
|
||||
except IndexError: # No valid nick was given; shift arguments one to the left.
|
||||
except IndexError: # No nick was given; shift arguments one to the left.
|
||||
u = irc.pseudoclient.uid
|
||||
try:
|
||||
clist = args[0]
|
||||
@ -117,7 +114,7 @@ def joinclient(irc, source, args):
|
||||
except KeyError:
|
||||
modes = []
|
||||
|
||||
# Signal the join to other plugins
|
||||
# Call a join hook manually so other plugins like relay can understand it.
|
||||
irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_JOIN', {'channel': real_channel, 'users': [u],
|
||||
'modes': modes, 'parse_as': 'JOIN'}])
|
||||
irc.reply("Done.")
|
||||
@ -141,7 +138,7 @@ def nick(irc, source, args):
|
||||
except IndexError:
|
||||
irc.error("Not enough arguments. Needs 1-2: nick (optional), newnick.")
|
||||
return
|
||||
u = irc.nick_to_uid(nick, filterfunc=irc.is_internal_client)
|
||||
u = irc.nick_to_uid(nick)
|
||||
|
||||
if newnick in ('0', u): # Allow /nick 0 to work
|
||||
newnick = u
|
||||
@ -156,7 +153,7 @@ def nick(irc, source, args):
|
||||
|
||||
irc.nick(u, newnick)
|
||||
irc.reply("Done.")
|
||||
# Signal the nick change to other plugins
|
||||
# Ditto above: manually send a NICK change hook payload to other plugins.
|
||||
irc.call_hooks([u, 'PYLINK_BOTSPLUGIN_NICK', {'newnick': newnick, 'oldnick': nick, 'parse_as': 'NICK'}])
|
||||
|
||||
@utils.add_cmd
|
||||
@ -174,8 +171,8 @@ def part(irc, source, args):
|
||||
|
||||
# First, check if the first argument is an existing PyLink client. If it is not,
|
||||
# then assume that the first argument was actually the channels being parted.
|
||||
u = irc.nick_to_uid(nick, filterfunc=irc.is_internal_client)
|
||||
if u is None: # First argument isn't one of our clients
|
||||
u = irc.nick_to_uid(nick)
|
||||
if not irc.is_internal_client(u): # First argument isn't one of our clients
|
||||
raise IndexError
|
||||
|
||||
except IndexError: # No nick was given; shift arguments one to the left.
|
||||
@ -220,11 +217,12 @@ def msg(irc, source, args):
|
||||
|
||||
# First, check if the first argument is an existing PyLink client. If it is not,
|
||||
# then assume that the first argument was actually the message TARGET.
|
||||
sourceuid = irc.nick_to_uid(msgsource, filterfunc=irc.is_internal_client)
|
||||
|
||||
if sourceuid is None or not text: # First argument isn't one of our clients
|
||||
sourceuid = irc.nick_to_uid(msgsource)
|
||||
if not irc.is_internal_client(sourceuid): # First argument isn't one of our clients
|
||||
raise IndexError
|
||||
|
||||
if not text:
|
||||
raise IndexError
|
||||
except IndexError:
|
||||
try:
|
||||
sourceuid = irc.pseudoclient.uid
|
||||
@ -238,26 +236,12 @@ def msg(irc, source, args):
|
||||
irc.error('No text given.')
|
||||
return
|
||||
|
||||
try:
|
||||
int_u = int(target)
|
||||
except:
|
||||
int_u = None
|
||||
|
||||
if int_u and int_u in irc.users:
|
||||
real_target = int_u # Some protocols use numeric UIDs
|
||||
elif target in irc.users:
|
||||
real_target = target
|
||||
elif not irc.is_channel(target):
|
||||
# Convert nick of the message target to a UID, if the target isn't a channel or UID
|
||||
potential_targets = irc.nick_to_uid(target, multi=True)
|
||||
if not potential_targets: # Unknown target user, if target isn't a valid channel name
|
||||
if not irc.is_channel(target):
|
||||
# Convert nick of the message target to a UID, if the target isn't a channel
|
||||
real_target = irc.nick_to_uid(target)
|
||||
if real_target is None: # Unknown target user, if target isn't a valid channel name
|
||||
irc.error('Unknown user %r.' % target)
|
||||
return
|
||||
elif len(potential_targets) > 1:
|
||||
irc.error('Multiple users with the nick %r found: please select the right UID: %s' % (target, str(potential_targets)))
|
||||
return
|
||||
else:
|
||||
real_target = potential_targets[0]
|
||||
else:
|
||||
real_target = target
|
||||
|
||||
|
@ -1,16 +1,20 @@
|
||||
"""
|
||||
Changehost plugin - automatically changes the hostname of matching users.
|
||||
"""
|
||||
from pylinkirc import utils, world, conf
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import permissions
|
||||
|
||||
import string
|
||||
|
||||
from pylinkirc import conf, utils, world
|
||||
from pylinkirc.coremods import permissions
|
||||
from pylinkirc.log import log
|
||||
# ircmatch library from https://github.com/mammon-ircd/ircmatch
|
||||
# (pip install ircmatch)
|
||||
import ircmatch
|
||||
|
||||
# Characters allowed in a hostname.
|
||||
allowed_chars = string.ascii_letters + '-./:' + string.digits
|
||||
|
||||
def _changehost(irc, target):
|
||||
def _changehost(irc, target, args):
|
||||
changehost_conf = conf.conf.get("changehost")
|
||||
|
||||
if target not in irc.users:
|
||||
@ -19,25 +23,24 @@ def _changehost(irc, target):
|
||||
log.debug('(%s) Skipping changehost on internal client %s', irc.name, target)
|
||||
return
|
||||
|
||||
if irc.name not in changehost_conf.get('enabled_nets') and not irc.serverdata.get('changehost_enable'):
|
||||
if not changehost_conf:
|
||||
log.warning("(%s) Missing 'changehost:' configuration block; "
|
||||
"Changehost will not function correctly!", irc.name)
|
||||
return
|
||||
elif irc.name not in changehost_conf.get('enabled_nets'):
|
||||
# We're not enabled on the network, break.
|
||||
return
|
||||
|
||||
match_ip = irc.get_service_option('changehost', 'match_ip', default=False)
|
||||
match_realhosts = irc.get_service_option('changehost', 'match_realhosts', default=False)
|
||||
match_ip = changehost_conf.get('match_ip', False)
|
||||
match_realhosts = changehost_conf.get('match_realhosts', False)
|
||||
|
||||
changehost_hosts = irc.get_service_options('changehost', 'hosts', dict)
|
||||
changehost_hosts = changehost_conf.get('hosts')
|
||||
if not changehost_hosts:
|
||||
log.warning("(%s) No hosts were defined in changehost::hosts; "
|
||||
"Changehost will not function correctly!", irc.name)
|
||||
return
|
||||
|
||||
args = irc.users[target].get_fields()
|
||||
|
||||
# $host is explicitly forbidden by default because it can cause recursive
|
||||
# loops when IP or real host masks are used to match a target. vHost
|
||||
# updates do not affect these fields, so any further host application will
|
||||
# cause the vHost to grow rapidly in size.
|
||||
# That said, it is possible to get away with this expansion if you're
|
||||
# careful enough, and that's why this hidden option exists.
|
||||
args = args.copy()
|
||||
if not changehost_conf.get('force_host_expansion'):
|
||||
del args['host']
|
||||
|
||||
@ -54,6 +57,16 @@ def _changehost(irc, target):
|
||||
# Substitute using the fields provided the hook data. This means
|
||||
# that the following variables are available for substitution:
|
||||
# $uid, $ts, $nick, $realhost, $ident, and $ip.
|
||||
|
||||
# $host is explicitly forbidden by default because it can cause
|
||||
# recursive loops when IP or real host masks are used to match a
|
||||
# target. vHost updates do not affect these fields, so any further
|
||||
# execution of 'applyhosts' will cause $host to expand again to
|
||||
# the user's new host, causing the vHost to grow rapidly in size.
|
||||
# That said, it is possible to get away with this expansion if
|
||||
# you're careful with what you're doing, and that is why this
|
||||
# hidden option exists. -GLolol
|
||||
|
||||
try:
|
||||
new_host = template.substitute(args)
|
||||
except KeyError as e:
|
||||
@ -65,8 +78,6 @@ def _changehost(irc, target):
|
||||
if char not in allowed_chars:
|
||||
new_host = new_host.replace(char, '-')
|
||||
|
||||
# Only send a host change if something has changed
|
||||
if new_host != irc.users[target].host:
|
||||
irc.update_client(target, 'HOST', new_host)
|
||||
|
||||
# Only operate on the first match.
|
||||
@ -78,23 +89,26 @@ def handle_uid(irc, sender, command, args):
|
||||
"""
|
||||
|
||||
target = args['uid']
|
||||
_changehost(irc, target)
|
||||
_changehost(irc, target, args)
|
||||
|
||||
utils.add_hook(handle_uid, 'UID')
|
||||
|
||||
def handle_chghost(irc, sender, command, args):
|
||||
"""
|
||||
Handles incoming CHGHOST requests for optional host-change enforcement.
|
||||
"""
|
||||
changehost_conf = conf.conf.get("changehost", {})
|
||||
changehost_conf = conf.conf.get("changehost")
|
||||
if not changehost_conf:
|
||||
return
|
||||
|
||||
target = args['target']
|
||||
|
||||
if (not irc.is_internal_client(sender)) and (not irc.is_internal_server(sender)):
|
||||
if irc.name in changehost_conf.get('enforced_nets', []) or irc.serverdata.get('changehost_enforce'):
|
||||
if irc.name in changehost_conf.get('enforced_nets', []):
|
||||
log.debug('(%s) Enforce for network is on, re-checking host for target %s/%s',
|
||||
irc.name, target, irc.get_friendly_name(target))
|
||||
|
||||
for ex in irc.get_service_options("changehost", "enforce_exceptions", list):
|
||||
for ex in changehost_conf.get("enforce_exceptions", []):
|
||||
if irc.match_host(ex, target):
|
||||
log.debug('(%s) Skipping host change for target %s; they are exempted by mask %s',
|
||||
irc.name, target, ex)
|
||||
@ -102,15 +116,9 @@ def handle_chghost(irc, sender, command, args):
|
||||
|
||||
userobj = irc.users.get(target)
|
||||
if userobj:
|
||||
_changehost(irc, target)
|
||||
utils.add_hook(handle_chghost, 'CHGHOST')
|
||||
_changehost(irc, target, userobj.get_fields())
|
||||
|
||||
def handle_svslogin(irc, sender, command, args):
|
||||
"""
|
||||
Handles services account changes for changehost.
|
||||
"""
|
||||
_changehost(irc, sender)
|
||||
utils.add_hook(handle_svslogin, 'CLIENT_SERVICES_LOGIN')
|
||||
utils.add_hook(handle_chghost, 'CHGHOST')
|
||||
|
||||
@utils.add_cmd
|
||||
def applyhosts(irc, sender, args):
|
||||
@ -128,7 +136,7 @@ def applyhosts(irc, sender, args):
|
||||
irc.error("Unknown network '%s'." % network)
|
||||
return
|
||||
|
||||
for user in network.users.copy():
|
||||
_changehost(network, user)
|
||||
for user, userdata in network.users.copy().items():
|
||||
_changehost(network, user, userdata.__dict__)
|
||||
|
||||
irc.reply("Done.")
|
||||
|
@ -1,12 +1,13 @@
|
||||
# commands.py: base PyLink commands
|
||||
import sys
|
||||
import time
|
||||
|
||||
from pylinkirc import __version__, conf, real_version, utils, world
|
||||
from pylinkirc import utils, __version__, world, real_version
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import permissions
|
||||
|
||||
from pylinkirc.coremods.login import pwd_context
|
||||
|
||||
default_permissions = {"*!*@*": ['commands.status', 'commands.showuser', 'commands.showchan', 'commands.shownet']}
|
||||
default_permissions = {"*!*@*": ['commands.status', 'commands.showuser', 'commands.showchan']}
|
||||
|
||||
def main(irc=None):
|
||||
"""Commands plugin main function, called on plugin load."""
|
||||
@ -31,31 +32,30 @@ def status(irc, source, args):
|
||||
irc.reply('Operator access: \x02%s\x02' % bool(irc.is_oper(source)))
|
||||
|
||||
_none = '\x1D(none)\x1D'
|
||||
_notavail = '\x1DN/A\x1D'
|
||||
def _do_showuser(irc, source, u):
|
||||
"""Helper function for showuser."""
|
||||
# Some protocol modules store UIDs as ints; make sure we check for that.
|
||||
try:
|
||||
int_u = int(u)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if int_u in irc.users:
|
||||
u = int_u
|
||||
@utils.add_cmd
|
||||
def showuser(irc, source, args):
|
||||
"""<user>
|
||||
|
||||
Shows information about <user>."""
|
||||
permissions.check_permissions(irc, source, ['commands.showuser'])
|
||||
try:
|
||||
target = args[0]
|
||||
except IndexError:
|
||||
irc.error("Not enough arguments. Needs 1: nick.")
|
||||
return
|
||||
u = irc.nick_to_uid(target) or target
|
||||
# Only show private info if the person is calling 'showuser' on themselves,
|
||||
# or is an oper.
|
||||
verbose = irc.is_oper(source) or u == source
|
||||
|
||||
if u not in irc.users:
|
||||
irc.error('Unknown user %r.' % u)
|
||||
irc.error('Unknown user %r.' % target)
|
||||
return
|
||||
|
||||
f = lambda s: irc.reply(' ' + s, private=True)
|
||||
f = lambda s: irc.reply(s, private=True)
|
||||
|
||||
userobj = irc.users[u]
|
||||
irc.reply('Showing information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident,
|
||||
userobj.host, userobj.realname), private=True)
|
||||
f('Showing information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident,
|
||||
userobj.host, userobj.realname))
|
||||
|
||||
sid = irc.get_server(u)
|
||||
serverobj = irc.servers[sid]
|
||||
@ -63,135 +63,20 @@ def _do_showuser(irc, source, u):
|
||||
|
||||
# Show connected server & nick TS if available
|
||||
serverinfo = '%s[%s]' % (serverobj.name, sid) \
|
||||
if irc.has_cap('can-track-servers') else None
|
||||
if irc.has_cap('can-track-servers') else 'N/A'
|
||||
tsinfo = '%s [UTC] (%s)' % (time.asctime(time.gmtime(int(ts))), ts) \
|
||||
if irc.has_cap('has-ts') else None
|
||||
if tsinfo or serverinfo:
|
||||
f('\x02Home server\x02: %s; \x02Nick TS:\x02 %s' % (serverinfo or _notavail, tsinfo or _notavail))
|
||||
if irc.has_cap('has-ts') else 'N/A'
|
||||
f('\x02Home server\x02: %s; \x02Nick TS:\x02 %s' % (serverinfo, tsinfo))
|
||||
|
||||
if verbose: # Oper/self only data: user modes, channels in, account info, etc.
|
||||
f('\x02User modes\x02: %s' % irc.join_modes(userobj.modes, sort=True))
|
||||
f('\x02Protocol UID\x02: %s; \x02Real host\x02: %s; \x02IP\x02: %s' % \
|
||||
(u, userobj.realhost or _notavail, userobj.ip))
|
||||
(u, userobj.realhost, userobj.ip))
|
||||
channels = sorted(userobj.channels)
|
||||
f('\x02Channels\x02: %s' % (' '.join(map(str, channels)) or _none))
|
||||
f('\x02Channels\x02: %s' % (' '.join(channels) or _none))
|
||||
f('\x02PyLink identification\x02: %s; \x02Services account\x02: %s; \x02Away status\x02: %s' % \
|
||||
((userobj.account or _none), (userobj.services_account or _none), userobj.away or _none))
|
||||
f('\x02User modes\x02: %s' % irc.join_modes(userobj.modes, sort=True))
|
||||
|
||||
# Show relay user data if available
|
||||
relay = world.plugins.get('relay')
|
||||
if relay:
|
||||
try:
|
||||
userpair = relay.get_orig_user(irc, u) or (irc.name, u)
|
||||
remoteusers = relay.relayusers[userpair].items()
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
nicks = []
|
||||
if remoteusers:
|
||||
# Display all of the user's relay subclients, if there are any
|
||||
nicks.append('%s:\x02%s\x02' % (userpair[0],
|
||||
world.networkobjects[userpair[0]].users[userpair[1]].nick))
|
||||
for r in remoteusers:
|
||||
remotenet, remoteuser = r
|
||||
remoteirc = world.networkobjects[remotenet]
|
||||
nicks.append('%s:\x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick))
|
||||
f("\x02Relay nicks\x02: %s" % ', '.join(nicks))
|
||||
if verbose:
|
||||
# Show the relay channels the user is in, if applicable
|
||||
relaychannels = []
|
||||
for ch in irc.users[u].channels:
|
||||
relayentry = relay.get_relay(irc, ch)
|
||||
if relayentry:
|
||||
relaychannels.append(''.join(relayentry))
|
||||
if relaychannels and verbose:
|
||||
f("\x02Relay channels\x02: %s" % ' '.join(relaychannels))
|
||||
|
||||
@utils.add_cmd
|
||||
def showuser(irc, source, args):
|
||||
"""<user>
|
||||
|
||||
Shows information about <user>."""
|
||||
permissions.check_permissions(irc, source, ['commands.showuser'])
|
||||
target = ' '.join(args)
|
||||
|
||||
if not target:
|
||||
irc.error("Not enough arguments. Needs 1: nick.")
|
||||
return
|
||||
|
||||
users = irc.nick_to_uid(target, multi=True) or [target]
|
||||
|
||||
for user in users:
|
||||
_do_showuser(irc, source, user)
|
||||
|
||||
@utils.add_cmd
|
||||
def shownet(irc, source, args):
|
||||
"""[<network name>]
|
||||
|
||||
Shows information about <network name>, or the current network if no argument is given."""
|
||||
permissions.check_permissions(irc, source, ['commands.shownet'])
|
||||
try:
|
||||
extended = permissions.check_permissions(irc, source, ['commands.shownet.extended'])
|
||||
except utils.NotAuthorizedError:
|
||||
extended = False
|
||||
|
||||
try:
|
||||
target = args[0]
|
||||
except IndexError:
|
||||
target = irc.name
|
||||
|
||||
try:
|
||||
netobj = world.networkobjects[target]
|
||||
serverdata = netobj.serverdata
|
||||
except KeyError:
|
||||
netobj = None
|
||||
|
||||
# If we have extended access, also look for disconnected networks
|
||||
if extended and target in conf.conf['servers']:
|
||||
serverdata = conf.conf['servers'][target]
|
||||
else:
|
||||
irc.error('Unknown network %r' % target)
|
||||
return
|
||||
|
||||
# Get extended protocol details: IRCd type, virtual server info
|
||||
protocol_name = serverdata.get('protocol')
|
||||
ircd_type = None
|
||||
|
||||
# A bit of hardcoding here :(
|
||||
if protocol_name == 'ts6':
|
||||
ircd_type = serverdata.get('ircd', 'charybdis[default]')
|
||||
elif protocol_name == 'inspircd':
|
||||
ircd_type = serverdata.get('target_version', 'insp20[default]')
|
||||
elif protocol_name == 'p10':
|
||||
ircd_type = serverdata.get('ircd') or serverdata.get('p10_ircd') or 'nefarious[default]'
|
||||
|
||||
if protocol_name and ircd_type:
|
||||
protocol_name = '%s/%s' % (protocol_name, ircd_type)
|
||||
elif netobj and not protocol_name: # Show virtual server detail if applicable
|
||||
try:
|
||||
parent_name = netobj.virtual_parent.name
|
||||
except AttributeError:
|
||||
parent_name = None
|
||||
protocol_name = 'none; virtual server defined by \x02%s\x02' % parent_name
|
||||
|
||||
irc.reply('Information on network \x02%s\x02: \x02%s\x02' %
|
||||
(target, netobj.get_full_network_name() if netobj else '\x1dCurrently not connected\x1d'))
|
||||
|
||||
irc.reply('\x02PyLink protocol module\x02: %s; \x02Encoding\x02: %s' %
|
||||
(protocol_name, netobj.encoding if netobj else serverdata.get('encoding', 'utf-8[default]')))
|
||||
|
||||
# Extended info: target host, defined hostname / SID
|
||||
if extended:
|
||||
connected = netobj and netobj.connected.is_set()
|
||||
irc.reply('\x02Connected?\x02 %s' % ('\x0303true' if connected else '\x0304false'))
|
||||
|
||||
if serverdata.get('ip'):
|
||||
irc.reply('\x02Server target\x02: \x1f%s:%s' % (serverdata['ip'], serverdata.get('port')))
|
||||
if serverdata.get('hostname'):
|
||||
irc.reply('\x02PyLink hostname\x02: %s; \x02SID:\x02 %s; \x02SID range:\x02 %s' %
|
||||
(serverdata.get('hostname') or _none,
|
||||
serverdata.get('sid') or _none,
|
||||
serverdata.get('sidrange') or _none))
|
||||
|
||||
@utils.add_cmd
|
||||
def showchan(irc, source, args):
|
||||
@ -244,24 +129,16 @@ def showchan(irc, source, args):
|
||||
nick = irc.prefixmodes.get(irc.cmodes.get(pmode, ''), '') + nick
|
||||
nicklist.append(nick)
|
||||
|
||||
f('\x02User list\x02: %s' % ' '.join(nicklist))
|
||||
|
||||
# Show relay info, if applicable
|
||||
relay = world.plugins.get('relay')
|
||||
if relay:
|
||||
relayentry = relay.get_relay(irc, channel)
|
||||
if relayentry:
|
||||
relays = ['\x02%s\x02' % ''.join(relayentry)]
|
||||
relays += [''.join(link) for link in relay.db[relayentry]['links']]
|
||||
f('\x02Relayed channels:\x02 %s' % (' '.join(relays)))
|
||||
while nicklist[:20]: # 20 nicks per line to prevent message cutoff.
|
||||
f('\x02User list\x02: %s' % ' '.join(nicklist[:20]))
|
||||
nicklist = nicklist[20:]
|
||||
|
||||
@utils.add_cmd
|
||||
def version(irc, source, args):
|
||||
"""takes no arguments.
|
||||
|
||||
Returns the version of the currently running PyLink instance."""
|
||||
py_version = utils.NORMALIZEWHITESPACE_RE.sub(' ', sys.version)
|
||||
irc.reply("PyLink version \x02%s\x02 (in VCS: %s), running on Python %s." % (__version__, real_version, py_version))
|
||||
irc.reply("PyLink version \x02%s\x02 (in VCS: %s), released under the Mozilla Public License version 2.0." % (__version__, real_version))
|
||||
irc.reply("The source of this program is available at \x02%s\x02." % world.source)
|
||||
|
||||
@utils.add_cmd
|
||||
|
@ -1,11 +1,10 @@
|
||||
# ctcp.py: Handles basic CTCP requests.
|
||||
import datetime
|
||||
import random
|
||||
import datetime
|
||||
|
||||
from pylinkirc import utils
|
||||
from pylinkirc.log import log
|
||||
|
||||
|
||||
def handle_ctcp(irc, source, command, args):
|
||||
"""
|
||||
CTCP event handler.
|
||||
|
@ -1,9 +1,9 @@
|
||||
# example.py: An example PyLink plugin.
|
||||
import random
|
||||
|
||||
from pylinkirc import utils
|
||||
from pylinkirc.log import log
|
||||
|
||||
import random
|
||||
|
||||
# Example PRIVMSG hook that returns "hi there!" when PyLink's nick is mentioned
|
||||
# in a channel.
|
||||
|
||||
|
@ -2,23 +2,23 @@
|
||||
exec.py: Provides commands for executing raw code and debugging PyLink.
|
||||
"""
|
||||
import pprint
|
||||
# These imports are not strictly necessary, but make the following modules
|
||||
# easier to access through eval and exec.
|
||||
import threading
|
||||
|
||||
from pylinkirc import utils, world, conf
|
||||
from pylinkirc.coremods import permissions
|
||||
from pylinkirc import *
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import permissions
|
||||
|
||||
# These imports are not strictly necessary, but make the following modules
|
||||
# easier to access through eval and exec.
|
||||
import re
|
||||
import time
|
||||
import pylinkirc
|
||||
import importlib
|
||||
|
||||
exec_locals_dict = {}
|
||||
PPRINT_MAX_LINES = 20
|
||||
PPRINT_WIDTH = 200
|
||||
|
||||
if not conf.conf['pylink'].get("debug_enabled", False):
|
||||
raise RuntimeError("pylink::debug_enabled must be enabled to load this plugin. "
|
||||
"This should ONLY be used in test environments for debugging and development, "
|
||||
"as anyone with access to this plugin's commands can run arbitrary code as the PyLink user!")
|
||||
|
||||
def _exec(irc, source, args, locals_dict=None):
|
||||
"""<code>
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
# fantasy.py: Adds FANTASY command support, to allow calling commands in channels
|
||||
from pylinkirc import conf, utils, world
|
||||
from pylinkirc import utils, world, conf
|
||||
from pylinkirc.log import log
|
||||
|
||||
|
||||
def handle_fantasy(irc, source, command, args):
|
||||
"""Fantasy command handler."""
|
||||
|
||||
@ -42,9 +41,10 @@ def handle_fantasy(irc, source, command, args):
|
||||
conf.conf['pylink'].get('prefixes', {}).get(botname))]
|
||||
|
||||
# If responding to nick is enabled, add variations of the current nick
|
||||
# to the prefix list: "<nick>,", "<nick>:", and "@<nick>" (for Discord and other protocols)
|
||||
# to the prefix list: "<nick>," and "<nick>:"
|
||||
nick = irc.to_lower(irc.users[servuid].nick)
|
||||
nick_prefixes = [nick+',', nick+':', '@'+nick]
|
||||
|
||||
nick_prefixes = [nick+',', nick+':']
|
||||
if respondtonick:
|
||||
prefixes += nick_prefixes
|
||||
|
||||
|
@ -4,6 +4,7 @@ games.py: Creates a bot providing a few simple games.
|
||||
import random
|
||||
|
||||
from pylinkirc import utils
|
||||
from pylinkirc.log import log
|
||||
|
||||
mydesc = "The \x02Games\x02 plugin provides simple games for IRC."
|
||||
|
||||
|
@ -3,8 +3,8 @@
|
||||
import string
|
||||
|
||||
from pylinkirc import conf, utils, world
|
||||
from pylinkirc.coremods import permissions
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import permissions
|
||||
|
||||
DEFAULT_FORMAT = "[$sender@$fullnetwork] $text"
|
||||
|
||||
@ -28,8 +28,7 @@ def g(irc, source, args):
|
||||
netcount = 0
|
||||
chancount = 0
|
||||
for netname, ircd in world.networkobjects.items():
|
||||
# Skip networks that aren't ready and dummy networks which don't have .pseudoclient set
|
||||
if ircd.connected.is_set() and ircd.pseudoclient:
|
||||
if ircd.connected.is_set(): # Only attempt to send to connected networks
|
||||
netcount += 1
|
||||
for channel in ircd.pseudoclient.channels:
|
||||
|
||||
@ -37,7 +36,7 @@ def g(irc, source, args):
|
||||
|
||||
skip = False
|
||||
for exempt in local_exempt_channels:
|
||||
if ircd.match_text(exempt, str(channel)):
|
||||
if irc.match_text(exempt, channel):
|
||||
log.debug('global: Skipping channel %s%s for exempt %r', netname, channel, exempt)
|
||||
skip = True
|
||||
break
|
||||
|
@ -1,12 +1,11 @@
|
||||
"""Networks plugin - allows you to manipulate connections to various configured networks."""
|
||||
import importlib
|
||||
import threading
|
||||
import types
|
||||
import threading
|
||||
|
||||
import pylinkirc
|
||||
from pylinkirc import utils, world
|
||||
from pylinkirc.coremods import control, permissions
|
||||
from pylinkirc import utils, world, conf, classes
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import control, permissions
|
||||
|
||||
REMOTE_IN_USE = threading.Event()
|
||||
|
||||
@ -27,14 +26,10 @@ def disconnect(irc, source, args):
|
||||
except KeyError: # Unknown network.
|
||||
irc.error('No such network "%s" (case sensitive).' % netname)
|
||||
return
|
||||
|
||||
if network.has_cap('virtual-server'):
|
||||
irc.error('"%s" is a virtual server and cannot be directly disconnected.' % netname)
|
||||
return
|
||||
|
||||
log.info('Disconnecting network %r per %s', netname, irc.get_hostmask(source))
|
||||
control.remove_network(network)
|
||||
irc.reply("Done. If you want to reconnect this network, use the 'rehash' command.")
|
||||
log.info('Disconnecting network %r per %s', netname, irc.get_hostmask(source))
|
||||
|
||||
control.remove_network(network)
|
||||
|
||||
@utils.add_cmd
|
||||
def autoconnect(irc, source, args):
|
||||
@ -182,17 +177,7 @@ def reloadproto(irc, source, args):
|
||||
irc.error('Not enough arguments (needs 1: protocol module name)')
|
||||
return
|
||||
|
||||
# Reload the dependency libraries first
|
||||
importlib.reload(pylinkirc.classes)
|
||||
log.debug('networks.reloadproto: reloading %s', pylinkirc.classes)
|
||||
|
||||
for common_name in pylinkirc.protocols.common_modules:
|
||||
module = utils._get_protocol_module(common_name)
|
||||
log.debug('networks.reloadproto: reloading %s', module)
|
||||
importlib.reload(module)
|
||||
|
||||
proto = utils._get_protocol_module(name)
|
||||
log.debug('networks.reloadproto: reloading %s', proto)
|
||||
importlib.reload(proto)
|
||||
|
||||
irc.reply("Done. You will have to manually disconnect and reconnect any network using the %r module for changes to apply." % name)
|
||||
|
@ -4,8 +4,8 @@ opercmds.py: Provides a subset of network management commands.
|
||||
import argparse
|
||||
|
||||
from pylinkirc import utils, world
|
||||
from pylinkirc.coremods import permissions
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import permissions
|
||||
|
||||
# Having a hard limit here is sensible because otherwise it can flood the client or server off.
|
||||
CHECKBAN_MAX_RESULTS = 200
|
||||
@ -333,29 +333,6 @@ def jupe(irc, source, args):
|
||||
|
||||
irc.reply("Done.")
|
||||
|
||||
def _try_find_target(irc, nick):
|
||||
"""
|
||||
Tries to find the target UID for the given nick, raising LookupError if it doesn't exist or is ambiguous.
|
||||
"""
|
||||
try:
|
||||
int_u = int(nick)
|
||||
except:
|
||||
int_u = None
|
||||
|
||||
if int_u and int_u in irc.users:
|
||||
return int_u # Some protocols use numeric UIDs
|
||||
elif nick in irc.users:
|
||||
return nick
|
||||
|
||||
potential_targets = irc.nick_to_uid(nick, multi=True)
|
||||
if not potential_targets:
|
||||
# Whatever we were told to kick doesn't exist!
|
||||
raise LookupError("No such target %r." % nick)
|
||||
elif len(potential_targets) > 1:
|
||||
raise LookupError("Multiple users with the nick %r found: please select the right UID: %s" % (nick, str(potential_targets)))
|
||||
else:
|
||||
return potential_targets[0]
|
||||
|
||||
@utils.add_cmd
|
||||
def kick(irc, source, args):
|
||||
"""<channel> <user> [<reason>]
|
||||
@ -370,11 +347,16 @@ def kick(irc, source, args):
|
||||
irc.error("Not enough arguments. Needs 2-3: channel, target, reason (optional).")
|
||||
return
|
||||
|
||||
targetu = irc.nick_to_uid(target)
|
||||
|
||||
if channel not in irc.channels: # KICK only works on channels that exist.
|
||||
irc.error("Unknown channel %r." % channel)
|
||||
return
|
||||
|
||||
targetu = _try_find_target(irc, target)
|
||||
if not targetu:
|
||||
# Whatever we were told to kick doesn't exist!
|
||||
irc.error("No such target nick %r." % target)
|
||||
return
|
||||
|
||||
sender = irc.pseudoclient.uid
|
||||
irc.kick(sender, channel, targetu, reason)
|
||||
@ -397,16 +379,25 @@ def kill(irc, source, args):
|
||||
|
||||
# Convert the source and target nicks to UIDs.
|
||||
sender = irc.pseudoclient.uid
|
||||
targetu = irc.nick_to_uid(target)
|
||||
userdata = irc.users.get(targetu)
|
||||
|
||||
targetu = _try_find_target(irc, target)
|
||||
|
||||
if irc.pseudoclient.uid == targetu:
|
||||
if targetu not in irc.users:
|
||||
# Whatever we were told to kick doesn't exist!
|
||||
irc.error("No such nick %r." % target)
|
||||
return
|
||||
elif irc.pseudoclient.uid == targetu:
|
||||
irc.error("Cannot kill the main PyLink client!")
|
||||
return
|
||||
|
||||
userdata = irc.users.get(targetu)
|
||||
|
||||
reason = "Requested by %s: %s" % (irc.get_friendly_name(source), reason)
|
||||
# Deliver a more complete kill reason if our target is a non-PyLink client.
|
||||
# We skip this for PyLink clients so that relayed kills don't get
|
||||
# "Killed (abc (...))" tacked on both here and by the receiving IRCd.
|
||||
if not irc.is_internal_client(targetu):
|
||||
reason = "Killed (%s (Requested by %s: %s))" % (
|
||||
irc.get_friendly_name(sender),
|
||||
irc.get_friendly_name(source),
|
||||
reason)
|
||||
|
||||
irc.kill(sender, targetu, reason)
|
||||
|
||||
@ -482,24 +473,23 @@ def chghost(irc, source, args):
|
||||
"""<user> <new host>
|
||||
|
||||
Changes the visible host of the target user."""
|
||||
_chgfield(irc, source, args, 'host')
|
||||
chgfield(irc, source, args, 'host')
|
||||
|
||||
@utils.add_cmd
|
||||
def chgident(irc, source, args):
|
||||
"""<user> <new ident>
|
||||
|
||||
Changes the ident of the target user."""
|
||||
_chgfield(irc, source, args, 'ident')
|
||||
chgfield(irc, source, args, 'ident')
|
||||
|
||||
@utils.add_cmd
|
||||
def chgname(irc, source, args):
|
||||
"""<user> <new name>
|
||||
|
||||
Changes the GECOS (realname) of the target user."""
|
||||
_chgfield(irc, source, args, 'name', 'GECOS')
|
||||
chgfield(irc, source, args, 'name', 'GECOS')
|
||||
|
||||
def _chgfield(irc, source, args, human_field, internal_field=None):
|
||||
"""Helper function for chghost/chgident/chgname."""
|
||||
def chgfield(irc, source, args, human_field, internal_field=None):
|
||||
permissions.check_permissions(irc, source, ['opercmds.chg' + human_field])
|
||||
try:
|
||||
target = args[0]
|
||||
@ -509,7 +499,10 @@ def _chgfield(irc, source, args, human_field, internal_field=None):
|
||||
return
|
||||
|
||||
# Find the user
|
||||
targetu = _try_find_target(irc, target)
|
||||
targetu = irc.nick_to_uid(target)
|
||||
if targetu not in irc.users:
|
||||
irc.error("No such nick %r." % target)
|
||||
return
|
||||
|
||||
internal_field = internal_field or human_field.upper()
|
||||
irc.update_client(targetu, internal_field, new)
|
||||
|
@ -1,10 +1,9 @@
|
||||
"""
|
||||
raw.py: Provides a 'raw' command for sending raw text to IRC.
|
||||
"""
|
||||
from pylinkirc import utils
|
||||
from pylinkirc.coremods import permissions
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc import conf
|
||||
from pylinkirc.coremods import permissions
|
||||
from pylinkirc import utils
|
||||
|
||||
@utils.add_cmd
|
||||
def raw(irc, source, args):
|
||||
@ -12,12 +11,15 @@ def raw(irc, source, args):
|
||||
|
||||
Sends raw text to the IRC server.
|
||||
|
||||
Use with caution - This command is only officially supported on Clientbot networks."""
|
||||
if not conf.conf['pylink'].get("raw_enabled", False):
|
||||
raise RuntimeError("Raw commands are not supported on this protocol")
|
||||
This command is not officially supported on non-Clientbot networks, where it
|
||||
requires a separate permission."""
|
||||
|
||||
if irc.protoname == 'clientbot':
|
||||
# exec.raw is included for backwards compatibility with PyLink 1.x
|
||||
permissions.check_permissions(irc, source, ['raw.raw', 'exec.raw'])
|
||||
perms = ['raw.raw', 'exec.raw']
|
||||
else:
|
||||
perms = ['raw.raw.unsupported_network']
|
||||
permissions.check_permissions(irc, source, perms)
|
||||
|
||||
args = ' '.join(args)
|
||||
if not args.strip():
|
||||
|
480
plugins/relay.py
480
plugins/relay.py
@ -1,41 +1,26 @@
|
||||
# relay.py: PyLink Relay plugin
|
||||
import base64
|
||||
import inspect
|
||||
import string
|
||||
import threading
|
||||
import time
|
||||
import threading
|
||||
import string
|
||||
from collections import defaultdict
|
||||
import inspect
|
||||
|
||||
from pylinkirc import conf, structures, utils, world
|
||||
from pylinkirc.coremods import permissions
|
||||
from pylinkirc import utils, world, conf, structures
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import permissions
|
||||
|
||||
# Sets the timeout to wait for as individual servers / the PyLink daemon to start up.
|
||||
TCONDITION_TIMEOUT = 2
|
||||
|
||||
CHANNEL_DELINKED_MSG = "Channel delinked."
|
||||
RELAY_UNLOADED_MSG = "Relay plugin unloaded."
|
||||
|
||||
try:
|
||||
import cachetools
|
||||
except ImportError as e:
|
||||
raise ImportError("PyLink Relay requires cachetools as of PyLink 3.0: https://pypi.org/project/cachetools/") from e
|
||||
|
||||
try:
|
||||
import unidecode
|
||||
except ImportError:
|
||||
log.info('relay: unidecode not found; disabling unicode nicks support')
|
||||
USE_UNIDECODE = False
|
||||
else:
|
||||
USE_UNIDECODE = conf.conf.get('relay', {}).get('use_unidecode', True)
|
||||
|
||||
### GLOBAL (statekeeping) VARIABLES
|
||||
relayusers = defaultdict(dict)
|
||||
relayservers = defaultdict(dict)
|
||||
spawnlocks = defaultdict(threading.Lock)
|
||||
spawnlocks_servers = defaultdict(threading.Lock)
|
||||
|
||||
# Claim bounce cache to prevent kick/mode/topic loops
|
||||
__claim_bounce_timeout = conf.conf.get('relay', {}).get('claim_bounce_timeout', 5)
|
||||
claim_bounce_cache = cachetools.TTLCache(float('inf'), __claim_bounce_timeout)
|
||||
claim_bounce_cache_lock = threading.Lock()
|
||||
spawnlocks = defaultdict(threading.RLock)
|
||||
spawnlocks_servers = defaultdict(threading.RLock)
|
||||
channels_init_in_progress = defaultdict(threading.Event)
|
||||
|
||||
dbname = conf.get_database_name('pylinkrelay')
|
||||
datastore = structures.PickleDataStore('pylinkrelay', dbname)
|
||||
@ -63,6 +48,10 @@ def initialize_all(irc):
|
||||
if network == irc.name:
|
||||
initialize_channel(irc, channel)
|
||||
|
||||
# Wait for all IRC objects to be created first. This prevents
|
||||
# relay servers from being spawned too early (before server authentication),
|
||||
# which would break connections.
|
||||
if world.started.wait(TCONDITION_TIMEOUT):
|
||||
t = threading.Thread(target=_initialize_all, daemon=True,
|
||||
name='relay initialize_all thread from network %r' % irc.name)
|
||||
t.start()
|
||||
@ -129,26 +118,8 @@ def die(irc=None):
|
||||
except KeyError:
|
||||
log.debug('relay.die: failed to clear persistent channels:', exc_info=True)
|
||||
|
||||
IRC_ASCII_ALLOWED_CHARS = string.digits + string.ascii_letters + '^|\\-_[]{}`'
|
||||
FALLBACK_SEPARATOR = '|'
|
||||
FALLBACK_CHARACTER = '-'
|
||||
|
||||
def _replace_special(text):
|
||||
"""
|
||||
Replaces brackets and spaces by similar IRC-representable characters.
|
||||
"""
|
||||
for pair in {('(', '['), (')', ']'), (' ', FALLBACK_CHARACTER), ('<', '['), ('>', ']')}:
|
||||
text = text.replace(pair[0], pair[1])
|
||||
return text
|
||||
|
||||
def _sanitize(text, extrachars=''):
|
||||
"""Replaces characters not in IRC_ASCII_ALLOWED_CHARS with FALLBACK_CHARACTER."""
|
||||
whitelist = IRC_ASCII_ALLOWED_CHARS + extrachars
|
||||
for char in text:
|
||||
if char not in whitelist:
|
||||
text = text.replace(char, FALLBACK_CHARACTER)
|
||||
return text
|
||||
|
||||
allowed_chars = string.digits + string.ascii_letters + '/^|\\-_[]{}`'
|
||||
fallback_separator = '|'
|
||||
def normalize_nick(irc, netname, nick, times_tagged=0, uid=''):
|
||||
"""
|
||||
Creates a normalized nickname for the given nick suitable for introduction to a remote network
|
||||
@ -157,25 +128,6 @@ def normalize_nick(irc, netname, nick, times_tagged=0, uid=''):
|
||||
UID is optional for checking regular nick changes, to make sure that the sender doesn't get
|
||||
marked as nick-colliding with itself.
|
||||
"""
|
||||
if irc.has_cap('freeform-nicks'): # ☺
|
||||
return nick
|
||||
|
||||
is_unicode_capable = irc.casemapping in ('utf8', 'utf-8', 'rfc7700')
|
||||
if USE_UNIDECODE and not is_unicode_capable:
|
||||
decoded_nick = unidecode.unidecode(nick).strip()
|
||||
netname = unidecode.unidecode(netname).strip()
|
||||
if decoded_nick:
|
||||
nick = decoded_nick
|
||||
else:
|
||||
# XXX: The decoded version of the nick is empty, YUCK!
|
||||
# Base64 the nick for now, since (interestingly) we don't enforce UIDs to always be
|
||||
# ASCII strings.
|
||||
nick = base64.b64encode(nick.encode(irc.encoding, 'replace'), altchars=b'[]')
|
||||
nick = nick.decode()
|
||||
|
||||
# Normalize spaces to hyphens, () => []
|
||||
nick = _replace_special(nick)
|
||||
netname = _replace_special(netname)
|
||||
|
||||
# Get the nick/net separator
|
||||
separator = irc.serverdata.get('separator') or \
|
||||
@ -209,34 +161,22 @@ def normalize_nick(irc, netname, nick, times_tagged=0, uid=''):
|
||||
irc.serverdata.get('relay_force_slashes')
|
||||
|
||||
if '/' not in separator or not protocol_allows_slashes:
|
||||
separator = separator.replace('/', FALLBACK_SEPARATOR)
|
||||
nick = nick.replace('/', FALLBACK_SEPARATOR)
|
||||
separator = separator.replace('/', fallback_separator)
|
||||
nick = nick.replace('/', fallback_separator)
|
||||
|
||||
# Loop over every character in the nick, making sure that it only contains valid
|
||||
# characters.
|
||||
if not is_unicode_capable:
|
||||
nick = _sanitize(nick, extrachars='/')
|
||||
else:
|
||||
# UnrealIRCd 4's forbidden nick chars, from
|
||||
# https://github.com/unrealircd/unrealircd/blob/02d69e7d8/src/modules/charsys.c#L152-L163
|
||||
for char in """!+%@&~#$:'\"?*,.""":
|
||||
nick = nick.replace(char, FALLBACK_CHARACTER)
|
||||
|
||||
if nick.startswith(tuple(string.digits)):
|
||||
if nick.startswith(tuple(string.digits+'-')):
|
||||
# On TS6 IRCds, nicks that start with 0-9 are only allowed if
|
||||
# they match the UID of the originating server. Otherwise, you'll
|
||||
# get nasty protocol violation SQUITs!
|
||||
nick = '_' + nick
|
||||
elif nick.startswith('-'):
|
||||
# Nicks starting with - are likewise not valid.
|
||||
nick = '_' + nick[1:]
|
||||
nick = '_' + nick
|
||||
|
||||
# Maximum allowed length that relay nicks may have, minus the /network tag if used.
|
||||
allowedlength = maxnicklen
|
||||
|
||||
# Track how many times the given nick has been tagged. If this is 0, no tag is used.
|
||||
# If this is 1, a /network tag is added. Otherwise, keep adding one character to the
|
||||
# separator: jlu5 -> jlu5/net1 -> jlu5//net1 -> ...
|
||||
# separator: GLolol -> GLolol/net1 -> GLolol//net1 -> ...
|
||||
if times_tagged >= 1:
|
||||
suffix = "%s%s%s" % (separator[0]*times_tagged, separator[1:], netname)
|
||||
allowedlength -= len(suffix)
|
||||
@ -247,6 +187,12 @@ def normalize_nick(irc, netname, nick, times_tagged=0, uid=''):
|
||||
if times_tagged >= 1:
|
||||
nick += suffix
|
||||
|
||||
# Loop over every character in the nick, making sure that it only contains valid
|
||||
# characters.
|
||||
for char in nick:
|
||||
if char not in allowed_chars:
|
||||
nick = nick.replace(char, fallback_separator)
|
||||
|
||||
while irc.nick_to_uid(nick) not in (None, uid):
|
||||
# The nick we want exists: Increase the separator length by 1 if the user was already
|
||||
# tagged, but couldn't be created due to a nick conflict. This can happen when someone
|
||||
@ -309,8 +255,8 @@ def spawn_relay_server(irc, remoteirc):
|
||||
if irc.connected.is_set():
|
||||
try:
|
||||
suffix = irc.serverdata.get('relay_server_suffix', conf.conf.get('relay', {}).get('server_suffix', 'relay'))
|
||||
# Strip any leading .'s
|
||||
suffix = suffix.lstrip('.')
|
||||
# Strip any leading or trailing .'s
|
||||
suffix = suffix.strip('.')
|
||||
|
||||
# On some IRCds (e.g. InspIRCd), we have to delay endburst to prevent triggering
|
||||
# join flood protections that are counted locally.
|
||||
@ -355,12 +301,13 @@ def get_relay_server_sid(irc, remoteirc, spawn_if_missing=True):
|
||||
|
||||
log.debug('(%s) Grabbing spawnlocks_servers[%s] from thread %r in function %r', irc.name, irc.name,
|
||||
threading.current_thread().name, inspect.currentframe().f_code.co_name)
|
||||
with spawnlocks_servers[irc.name]:
|
||||
if spawnlocks_servers[irc.name].acquire(timeout=TCONDITION_TIMEOUT):
|
||||
try:
|
||||
sid = relayservers[irc.name][remoteirc.name]
|
||||
except KeyError:
|
||||
if not spawn_if_missing:
|
||||
log.debug('(%s) get_relay_server_sid: %s.relay doesn\'t have a known SID, ignoring.', irc.name, remoteirc.name)
|
||||
spawnlocks_servers[irc.name].release()
|
||||
return
|
||||
|
||||
log.debug('(%s) get_relay_server_sid: %s.relay doesn\'t have a known SID, spawning.', irc.name, remoteirc.name)
|
||||
@ -372,6 +319,7 @@ def get_relay_server_sid(irc, remoteirc, spawn_if_missing=True):
|
||||
return
|
||||
|
||||
log.debug('(%s) get_relay_server_sid: got %s for %s.relay (round 2)', irc.name, sid, remoteirc.name)
|
||||
spawnlocks_servers[irc.name].release()
|
||||
return sid
|
||||
|
||||
def _has_common_pool(sourcenet, targetnet, namespace):
|
||||
@ -400,19 +348,8 @@ def spawn_relay_user(irc, remoteirc, user, times_tagged=0, reuse_sid=None):
|
||||
return
|
||||
|
||||
nick = normalize_nick(remoteirc, irc.name, userobj.nick, times_tagged=times_tagged)
|
||||
|
||||
# Sanitize UTF8 for networks that don't support it
|
||||
ident = _sanitize(userobj.ident, extrachars='~')
|
||||
|
||||
# Truncate idents at 10 characters, because TS6 won't like them otherwise!
|
||||
ident = ident[:10]
|
||||
|
||||
# HACK: hybrid will reject idents that start with a symbol
|
||||
if remoteirc.protoname == 'hybrid':
|
||||
goodchars = tuple(string.ascii_letters + string.digits + '~')
|
||||
if not ident.startswith(goodchars):
|
||||
ident = 'r' + ident
|
||||
|
||||
ident = userobj.ident[:10]
|
||||
# Normalize hostnames
|
||||
host = normalize_host(remoteirc, userobj.host)
|
||||
realname = userobj.realname
|
||||
@ -423,7 +360,7 @@ def spawn_relay_user(irc, remoteirc, user, times_tagged=0, reuse_sid=None):
|
||||
# Try to get the oper type, adding an "(on <networkname>)" suffix similar to what
|
||||
# Janus does.
|
||||
if hasattr(userobj, 'opertype'):
|
||||
log.debug('(%s) spawn_relay_user: setting OPERTYPE of client for %r to %s',
|
||||
log.debug('(%s) relay.get_remote_user: setting OPERTYPE of client for %r to %s',
|
||||
irc.name, user, userobj.opertype)
|
||||
opertype = userobj.opertype
|
||||
else:
|
||||
@ -498,18 +435,9 @@ def get_remote_user(irc, remoteirc, user, spawn_if_missing=True, times_tagged=0,
|
||||
if sbot:
|
||||
return sbot.uids.get(remoteirc.name)
|
||||
|
||||
# Ignore invisible users - used to skip joining users who are offline or invisible on
|
||||
# external transports
|
||||
if user in irc.users:
|
||||
hide = getattr(irc.users[user], '_invisible', False)
|
||||
if hide:
|
||||
log.debug('(%s) get_remote_user: ignoring user %s since they are marked invisible', irc.name,
|
||||
user)
|
||||
return
|
||||
|
||||
log.debug('(%s) Grabbing spawnlocks[%s] from thread %r in function %r', irc.name, irc.name,
|
||||
threading.current_thread().name, inspect.currentframe().f_code.co_name)
|
||||
with spawnlocks[irc.name]:
|
||||
if spawnlocks[irc.name].acquire(timeout=TCONDITION_TIMEOUT):
|
||||
# Be sort-of thread safe: lock the user spawns for the current net first.
|
||||
u = None
|
||||
try:
|
||||
@ -521,11 +449,24 @@ def get_remote_user(irc, remoteirc, user, spawn_if_missing=True, times_tagged=0,
|
||||
if spawn_if_missing:
|
||||
u = spawn_relay_user(irc, remoteirc, user, times_tagged=times_tagged, reuse_sid=reuse_sid)
|
||||
|
||||
# This is a sanity check to make sure netsplits and other state resets
|
||||
# don't break the relayer. If it turns out there was a client in our relayusers
|
||||
# cache for the requested UID, but it doesn't match the request,
|
||||
# assume it was a leftover from the last split and replace it with a new one.
|
||||
# XXX: this technically means that PyLink is desyncing somewhere, and that we should
|
||||
# fix this in core properly...
|
||||
if u and ((u not in remoteirc.users) or remoteirc.users[u].remote != (irc.name, user)):
|
||||
log.warning('(%s) Possible desync? Got invalid relay UID %s for %s on %s',
|
||||
irc.name, u, irc.get_friendly_name(user), remoteirc.name)
|
||||
u = spawn_relay_user(irc, remoteirc, user, times_tagged=times_tagged)
|
||||
|
||||
spawnlocks[irc.name].release()
|
||||
|
||||
return u
|
||||
else:
|
||||
log.debug('(%s) skipping spawn_relay_user(%s, %s, %s, ...); the local server (%s) is not ready yet',
|
||||
irc.name, irc.name, remoteirc.name, user, irc.name)
|
||||
log.debug('(%s) get_remote_user: current thread is %s',
|
||||
log.debug('(%s) spawn_relay_user: current thread is %s',
|
||||
irc.name, threading.current_thread().name)
|
||||
|
||||
def get_orig_user(irc, user, targetirc=None):
|
||||
@ -566,7 +507,7 @@ def get_relay(irc, channel):
|
||||
"""Finds the matching relay entry name for the given network, channel
|
||||
pair, if one exists."""
|
||||
|
||||
chanpair = (irc.name, irc.to_lower(str(channel)))
|
||||
chanpair = (irc.name, irc.to_lower(channel))
|
||||
|
||||
if chanpair in db: # This chanpair is a shared channel; others link to it
|
||||
return chanpair
|
||||
@ -673,32 +614,9 @@ def remove_channel(irc, channel):
|
||||
del relayusers[remoteuser][irc.name]
|
||||
irc.quit(user, 'Left all shared channels.')
|
||||
|
||||
def _claim_should_bounce(irc, channel):
|
||||
"""
|
||||
Returns whether we should bounce the next action that fails CLAIM.
|
||||
This is used to prevent kick/mode/topic wars with services.
|
||||
"""
|
||||
with claim_bounce_cache_lock:
|
||||
if irc.name not in claim_bounce_cache: # Nothing in the cache to worry about
|
||||
return True
|
||||
|
||||
limit = irc.get_service_option('relay', 'claim_bounce_limit', default=15)
|
||||
if limit < 0: # Disabled
|
||||
return True
|
||||
elif limit < 5: # Anything below this is just asking for desyncs...
|
||||
log.warning('(%s) relay: the minimum supported value for relay::claim_bounce_limit is 5.', irc.name)
|
||||
limit = 5
|
||||
|
||||
success = claim_bounce_cache[irc.name] <= limit
|
||||
ttl = claim_bounce_cache.ttl
|
||||
if not success:
|
||||
log.warning("(%s) relay: %s received more than %s claim bounces in %s seconds - your channel may be desynced!",
|
||||
irc.name, channel, limit, ttl)
|
||||
return success
|
||||
|
||||
def check_claim(irc, channel, sender, chanobj=None):
|
||||
"""
|
||||
Checks whether the sender of a kick/mode/topic change passes CLAIM checks for
|
||||
Checks whether the sender of a kick/mode change passes CLAIM checks for
|
||||
a given channel. This returns True if any of the following criteria are met:
|
||||
|
||||
1) No relay exists for the channel in question.
|
||||
@ -719,23 +637,13 @@ def check_claim(irc, channel, sender, chanobj=None):
|
||||
log.debug('(%s) relay.check_claim: sender modes (%s/%s) are %s (mlist=%s)', irc.name,
|
||||
sender, channel, sender_modes, mlist)
|
||||
# XXX: stop hardcoding modes to check for and support mlist in isHalfopPlus and friends
|
||||
success = (not relay) or irc.name == relay[0] or not db[relay]['claim'] or \
|
||||
return (not relay) or irc.name == relay[0] or not db[relay]['claim'] or \
|
||||
irc.name in db[relay]['claim'] or \
|
||||
(any([mode in sender_modes for mode in {'y', 'q', 'a', 'o', 'h'}])
|
||||
(any([mode in sender_modes for mode in ('y', 'q', 'a', 'o', 'h')])
|
||||
and not irc.is_privileged_service(sender)) \
|
||||
or irc.is_internal_client(sender) or \
|
||||
irc.is_internal_server(sender)
|
||||
|
||||
# Increment claim_bounce_cache, checked in _claim_should_bounce()
|
||||
if not success:
|
||||
with claim_bounce_cache_lock:
|
||||
if irc.name not in claim_bounce_cache:
|
||||
claim_bounce_cache[irc.name] = 1
|
||||
else:
|
||||
claim_bounce_cache[irc.name] += 1
|
||||
|
||||
return success
|
||||
|
||||
def get_supported_umodes(irc, remoteirc, modes):
|
||||
"""Given a list of user modes, filters out all of those not supported by the
|
||||
remote network."""
|
||||
@ -787,7 +695,16 @@ def get_supported_umodes(irc, remoteirc, modes):
|
||||
|
||||
def is_relay_client(irc, user):
|
||||
"""Returns whether the given user is a relay client."""
|
||||
return user in irc.users and hasattr(irc.users[user], 'remote')
|
||||
try:
|
||||
if irc.users[user].remote:
|
||||
# Is the .remote attribute set? If so, don't relay already
|
||||
# relayed clients; that'll trigger an endless loop!
|
||||
return True
|
||||
except AttributeError: # Nope, it isn't.
|
||||
pass
|
||||
except KeyError: # The user doesn't exist?!?
|
||||
return True
|
||||
return False
|
||||
isRelayClient = is_relay_client
|
||||
|
||||
def iterate_all(origirc, func, extra_args=(), kwargs=None):
|
||||
@ -828,7 +745,7 @@ def relay_joins(irc, channel, users, ts, targetirc=None, **kwargs):
|
||||
"""
|
||||
|
||||
log.debug('(%s) relay.relay_joins: called on %r with users %r, targetirc=%s', irc.name, channel,
|
||||
users, targetirc)
|
||||
['%s/%s' % (user, irc.get_friendly_name(user)) for user in users], targetirc)
|
||||
|
||||
if ts < 750000:
|
||||
current_ts = int(time.time())
|
||||
@ -840,9 +757,6 @@ def relay_joins(irc, channel, users, ts, targetirc=None, **kwargs):
|
||||
def _relay_joins_loop(irc, remoteirc, channel, users, ts, burst=True):
|
||||
queued_users = []
|
||||
|
||||
if not remoteirc.connected.is_set():
|
||||
return # Remote network is not ready yet.
|
||||
|
||||
remotechan = get_remote_channel(irc, remoteirc, channel)
|
||||
if remotechan is None:
|
||||
# If there is no link on the current network for the channel in question,
|
||||
@ -1162,8 +1076,8 @@ def get_supported_cmodes(irc, remoteirc, channel, modes):
|
||||
# First, we expand extbans from the local IRCd into a named mode and argument pair. Then, we
|
||||
# can figure out how to relay it.
|
||||
for extban_name, extban_prefix in irc.extbans_acting.items():
|
||||
# Acting extbans are generally only supported with +b and +e
|
||||
if name in {'ban', 'banexception'} and arg.startswith(extban_prefix):
|
||||
# Acting extbans are only supported with +b (e.g. +b m:n!u@h)
|
||||
if name == 'ban' and arg.startswith(extban_prefix):
|
||||
orig_supported_char, old_arg = supported_char, arg
|
||||
|
||||
if extban_name in remoteirc.cmodes:
|
||||
@ -1357,7 +1271,7 @@ def handle_join(irc, numeric, command, args):
|
||||
modes = []
|
||||
for user in users:
|
||||
# XXX: Find the diff of the new and old mode lists of the channel. Not pretty, but I'd
|
||||
# rather not change the 'users' format of SJOIN just for this. -jlu5
|
||||
# rather not change the 'users' format of SJOIN just for this. -GL
|
||||
try:
|
||||
oldmodes = set(chandata.get_prefix_modes(user))
|
||||
except KeyError:
|
||||
@ -1373,18 +1287,14 @@ def handle_join(irc, numeric, command, args):
|
||||
irc.name, user, channel, modediff, oldmodes, newmodes)
|
||||
for modename in modediff:
|
||||
modechar = irc.cmodes.get(modename)
|
||||
# Special case for U-lined servers: allow them to join with ops, but don't forward this mode change on.
|
||||
# Special case for U-lined servers: allow them to join with ops,
|
||||
# but don't forward this mode change on.
|
||||
if modechar and not irc.is_privileged_service(numeric):
|
||||
modes.append(('-%s' % modechar, user))
|
||||
|
||||
if modes:
|
||||
if _claim_should_bounce(irc, channel):
|
||||
log.debug('(%s) relay.handle_join: reverting modes on BURST: %s', irc.name, irc.join_modes(modes))
|
||||
irc.mode(irc.sid, channel, modes)
|
||||
else:
|
||||
# HACK: pretend we managed to deop the caller, so that they can't bypass claim entirely
|
||||
log.debug('(%s) relay.handle_join: fake reverting modes on BURST: %s', irc.name, irc.join_modes(modes))
|
||||
irc.apply_modes(channel, modes)
|
||||
|
||||
relay_joins(irc, channel, users, ts, burst=False)
|
||||
utils.add_hook(handle_join, 'JOIN')
|
||||
@ -1396,7 +1306,7 @@ def handle_quit(irc, numeric, command, args):
|
||||
log.debug('(%s) Grabbing spawnlocks[%s] from thread %r in function %r', irc.name, irc.name,
|
||||
threading.current_thread().name, inspect.currentframe().f_code.co_name)
|
||||
|
||||
with spawnlocks[irc.name]:
|
||||
if spawnlocks[irc.name].acquire(timeout=TCONDITION_TIMEOUT):
|
||||
|
||||
def _handle_quit_func(irc, remoteirc, user):
|
||||
try: # Try to quit the client. If this fails because they're missing, bail.
|
||||
@ -1406,6 +1316,7 @@ def handle_quit(irc, numeric, command, args):
|
||||
|
||||
iterate_all_present(irc, numeric, _handle_quit_func)
|
||||
del relayusers[(irc.name, numeric)]
|
||||
spawnlocks[irc.name].release()
|
||||
|
||||
utils.add_hook(handle_quit, 'QUIT')
|
||||
|
||||
@ -1476,7 +1387,7 @@ def handle_part(irc, numeric, command, args):
|
||||
for user in irc.channels[channel].users.copy():
|
||||
if (not irc.is_internal_client(user)) and (not is_relay_client(irc, user)):
|
||||
irc.call_hooks([irc.sid, 'CLIENTBOT_SERVICE_KICKED', {'channel': channel, 'target': user,
|
||||
'text': 'Clientbot was force parted (%s)' % text or 'None',
|
||||
'text': 'Clientbot was force parted (Reason: %s)' % text or 'None',
|
||||
'parse_as': 'KICK'}])
|
||||
irc.join(irc.pseudoclient.uid, channel)
|
||||
|
||||
@ -1521,9 +1432,6 @@ def handle_messages(irc, numeric, command, args):
|
||||
log.debug('(%s) relay.handle_messages: dropping PM from server %s to %s',
|
||||
irc.name, numeric, target)
|
||||
return
|
||||
elif not irc.has_cap('can-spawn-clients') and not world.plugins.get('relay_clientbot'):
|
||||
# For consistency, only read messages from clientbot networks if relay_clientbot is loaded
|
||||
return
|
||||
|
||||
relay = get_relay(irc, target)
|
||||
remoteusers = relayusers[(irc.name, numeric)]
|
||||
@ -1531,7 +1439,6 @@ def handle_messages(irc, numeric, command, args):
|
||||
avail_prefixes = {v: k for k, v in irc.prefixmodes.items()}
|
||||
prefixes = []
|
||||
# Split up @#channel prefixes and the like into their prefixes and target components
|
||||
if isinstance(target, str):
|
||||
while target and target[0] in avail_prefixes:
|
||||
prefixes.append(avail_prefixes[target[0]])
|
||||
target = target[1:]
|
||||
@ -1692,7 +1599,7 @@ def handle_kick(irc, source, command, args):
|
||||
if (not irc.has_cap('can-spawn-clients')) and irc.pseudoclient and target == irc.pseudoclient.uid:
|
||||
for user in irc.channels[channel].users:
|
||||
if (not irc.is_internal_client(user)) and (not is_relay_client(irc, user)):
|
||||
reason = "Clientbot kicked by %s (%s)" % (irc.get_friendly_name(source), text)
|
||||
reason = "Clientbot kicked by %s (Reason: %s)" % (irc.get_friendly_name(source), text)
|
||||
irc.call_hooks([irc.sid, 'CLIENTBOT_SERVICE_KICKED', {'channel': channel, 'target': user,
|
||||
'text': reason, 'parse_as': 'KICK'}])
|
||||
|
||||
@ -1762,16 +1669,17 @@ def handle_kick(irc, source, command, args):
|
||||
del relayusers[(irc.name, target)][remoteirc.name]
|
||||
remoteirc.quit(real_target, 'Left all shared channels.')
|
||||
|
||||
# Kick was a relay client but sender does not pass CLAIM restrictions. Bounce a rejoin unless we've reached our limit.
|
||||
if is_relay_client(irc, target) and not check_claim(irc, channel, kicker):
|
||||
if _claim_should_bounce(irc, channel):
|
||||
homenet, real_target = get_orig_user(irc, target)
|
||||
homeirc = world.networkobjects.get(homenet)
|
||||
homenick = homeirc.users[real_target].nick if homeirc else '<ghost user>'
|
||||
homechan = get_remote_channel(irc, homeirc, channel)
|
||||
|
||||
log.debug('(%s) relay.handle_kick: kicker %s is not opped... We should rejoin the target user %s', irc.name, kicker, real_target)
|
||||
# FIXME: make the check slightly more advanced: i.e. halfops can't kick ops, admins can't kick owners, etc.
|
||||
# Home network is not in the channel's claim AND the kicker is not
|
||||
# opped. We won't propograte the kick then.
|
||||
# TODO: make the check slightly more advanced: i.e. halfops can't
|
||||
# kick ops, admins can't kick owners, etc.
|
||||
modes = get_prefix_modes(homeirc, irc, homechan, real_target)
|
||||
|
||||
# Join the kicked client back with its respective modes.
|
||||
@ -1843,11 +1751,8 @@ def handle_mode(irc, numeric, command, args):
|
||||
get_relay_server_sid(remoteirc, irc) or remoteirc.sid
|
||||
|
||||
if not remoteirc.has_cap('can-spawn-clients'):
|
||||
if numeric in irc.servers and not irc.servers[numeric].has_eob:
|
||||
log.debug('(%s) Not relaying modes from server %s/%s to %s as it has not finished bursting',
|
||||
irc.name, numeric, irc.get_friendly_name(numeric), remoteirc.name)
|
||||
else:
|
||||
friendly_modes = []
|
||||
|
||||
for modepair in modes:
|
||||
modechar = modepair[0][-1]
|
||||
if modechar in irc.prefixmodes:
|
||||
@ -1876,19 +1781,10 @@ def handle_mode(irc, numeric, command, args):
|
||||
# Set hideoper on remote opers, to prevent inflating
|
||||
# /lusers and various /stats
|
||||
hideoper_mode = remoteirc.umodes.get('hideoper')
|
||||
try:
|
||||
use_hideoper = conf.conf['relay']['hideoper']
|
||||
except KeyError:
|
||||
use_hideoper = True
|
||||
|
||||
# If Relay oper hiding is enabled, don't allow unsetting +H
|
||||
if use_hideoper and ('-%s' % hideoper_mode, None) in modes:
|
||||
modes.remove(('-%s' % hideoper_mode, None))
|
||||
|
||||
modes = get_supported_umodes(irc, remoteirc, modes)
|
||||
|
||||
if hideoper_mode:
|
||||
if ('+o', None) in modes and use_hideoper:
|
||||
if ('+o', None) in modes:
|
||||
modes.append(('+%s' % hideoper_mode, None))
|
||||
elif ('-o', None) in modes:
|
||||
modes.append(('-%s' % hideoper_mode, None))
|
||||
@ -1918,15 +1814,15 @@ def handle_mode(irc, numeric, command, args):
|
||||
|
||||
if irc.is_privileged_service(numeric):
|
||||
# Special hack for "U-lined" servers - ignore changes to SIMPLE modes and
|
||||
# attempts to op its own clients (trying to change status for others
|
||||
# attempts to op u-lined clients (trying to change status for others
|
||||
# SHOULD be reverted).
|
||||
# This is for compatibility with Anope's DEFCON for the most part, as well as
|
||||
# silly people who try to register a channel multiple times via relay.
|
||||
reversed_modes = [modepair for modepair in reversed_modes if
|
||||
# Include prefix modes if target isn't also U-lined
|
||||
# Mode is a prefix mode but target isn't ulined, revert
|
||||
((modepair[0][-1] in irc.prefixmodes and not
|
||||
irc.is_privileged_service(modepair[1]))
|
||||
# Include all list modes (bans, etc.)
|
||||
# Tried to set a list mode, revert
|
||||
or modepair[0][-1] in irc.cmodes['*A'])
|
||||
]
|
||||
modes.clear() # Clear the mode list so nothing is relayed below
|
||||
@ -1959,14 +1855,9 @@ def handle_mode(irc, numeric, command, args):
|
||||
irc.name, str(modepair), target)
|
||||
|
||||
if reversed_modes:
|
||||
if _claim_should_bounce(irc, target):
|
||||
log.debug('(%s) relay.handle_mode: Reversing mode changes %r on %s with %r.',
|
||||
irc.name, args['modes'], target, reversed_modes)
|
||||
log.debug('(%s) relay.handle_mode: Reversing mode changes of %r with %r.',
|
||||
irc.name, args['modes'], reversed_modes)
|
||||
irc.mode(irc.sid, target, reversed_modes)
|
||||
else:
|
||||
log.debug('(%s) relay.handle_mode: Fake reversing mode changes %r on %s with %r.',
|
||||
irc.name, args['modes'], target, reversed_modes)
|
||||
irc.apply_modes(target, reversed_modes)
|
||||
|
||||
if modes:
|
||||
iterate_all(irc, _handle_mode_loop, extra_args=(numeric, command, target, modes))
|
||||
@ -2000,7 +1891,7 @@ def handle_topic(irc, numeric, command, args):
|
||||
remoteirc.topic_burst(rsid, remotechan, topic)
|
||||
iterate_all(irc, _handle_topic_loop, extra_args=(numeric, command, args))
|
||||
|
||||
elif oldtopic and _claim_should_bounce(irc, channel): # Topic change blocked by claim.
|
||||
elif oldtopic: # Topic change blocked by claim.
|
||||
irc.topic_burst(irc.sid, channel, oldtopic)
|
||||
|
||||
utils.add_hook(handle_topic, 'TOPIC')
|
||||
@ -2070,7 +1961,7 @@ def handle_kill(irc, numeric, command, args):
|
||||
|
||||
# Then, forward to the home network.
|
||||
hsender = get_relay_server_sid(origirc, irc, spawn_if_missing=False) or \
|
||||
origirc.sid
|
||||
homeirc.sid
|
||||
log.debug('(%s) relay.handle_kill: forwarding kill to %s/%s@%s as '
|
||||
'kick on %s', irc.name, realuser[1], target_nick,
|
||||
realuser[0], homechan)
|
||||
@ -2085,9 +1976,13 @@ def handle_kill(irc, numeric, command, args):
|
||||
irc.sjoin(irc.sid, localchan, [(modes, client)])
|
||||
|
||||
# Target user was local.
|
||||
elif userdata:
|
||||
reason = 'Killed (%s (%s))' % (irc.get_friendly_name(numeric), args['text'])
|
||||
handle_quit(irc, target, 'KILL', {'text': reason})
|
||||
else:
|
||||
# IMPORTANT: some IRCds (charybdis) don't send explicit QUIT messages
|
||||
# for locally killed clients, while others (inspircd) do!
|
||||
# If we receive a user object in 'userdata' instead of None, it means
|
||||
# that the KILL hasn't been handled by a preceding QUIT message.
|
||||
if userdata:
|
||||
handle_quit(irc, target, 'KILL', {'text': args['text']})
|
||||
|
||||
utils.add_hook(handle_kill, 'KILL')
|
||||
|
||||
@ -2095,22 +1990,6 @@ def handle_away(irc, numeric, command, args):
|
||||
iterate_all_present(irc, numeric,
|
||||
lambda irc, remoteirc, user:
|
||||
remoteirc.away(user, args['text']))
|
||||
|
||||
# Check invisible flag, used by external transports to hide offline users
|
||||
if not irc.is_internal_client(numeric):
|
||||
invisible = args.get('now_invisible')
|
||||
log.debug('(%s) relay.handle_away: invisible flag: %s', irc.name, invisible)
|
||||
if invisible:
|
||||
# User is now invisible - quit them
|
||||
log.debug('(%s) relay.handle_away: quitting user %s due to invisible flag', irc.name, numeric)
|
||||
handle_quit(irc, numeric, 'AWAY_NOW_INVISIBLE', {'text': "User has gone offline"})
|
||||
elif invisible is False:
|
||||
# User is no longer invisible - join them to all channels
|
||||
log.debug('(%s) relay.handle_away: rejoining user %s due to invisible flag', irc.name, numeric)
|
||||
for channel in irc.users[numeric].channels:
|
||||
c = irc.channels[channel]
|
||||
relay_joins(irc, channel, [numeric], c.ts, burst=True)
|
||||
|
||||
utils.add_hook(handle_away, 'AWAY')
|
||||
|
||||
def handle_invite(irc, source, command, args):
|
||||
@ -2157,17 +2036,19 @@ def handle_disconnect(irc, numeric, command, args):
|
||||
# them from our relay clients index.
|
||||
log.debug('(%s) Grabbing spawnlocks[%s] from thread %r in function %r', irc.name, irc.name,
|
||||
threading.current_thread().name, inspect.currentframe().f_code.co_name)
|
||||
with spawnlocks[irc.name]:
|
||||
if spawnlocks[irc.name].acquire(timeout=TCONDITION_TIMEOUT):
|
||||
for k, v in relayusers.copy().items():
|
||||
if irc.name in v:
|
||||
del relayusers[k][irc.name]
|
||||
if k[0] == irc.name:
|
||||
del relayusers[k]
|
||||
spawnlocks[irc.name].release()
|
||||
|
||||
# SQUIT all relay pseudoservers spawned for us, and remove them
|
||||
# from our relay subservers index.
|
||||
log.debug('(%s) Grabbing spawnlocks_servers[%s] from thread %r in function %r', irc.name, irc.name,
|
||||
threading.current_thread().name, inspect.currentframe().f_code.co_name)
|
||||
with spawnlocks_servers[irc.name]:
|
||||
if spawnlocks_servers[irc.name].acquire(timeout=TCONDITION_TIMEOUT):
|
||||
|
||||
def _handle_disconnect_loop(irc, remoteirc):
|
||||
name = remoteirc.name
|
||||
@ -2189,6 +2070,8 @@ def handle_disconnect(irc, numeric, command, args):
|
||||
except KeyError: # Already removed; ignore.
|
||||
pass
|
||||
|
||||
spawnlocks_servers[irc.name].release()
|
||||
|
||||
# Announce the disconnects to every leaf channel where the disconnected network is the owner
|
||||
announcement = conf.conf.get('relay', {}).get('disconnect_announcement')
|
||||
log.debug('(%s) relay: last connection successful: %s', irc.name, args.get('was_successful'))
|
||||
@ -2223,37 +2106,20 @@ def handle_disconnect(irc, numeric, command, args):
|
||||
|
||||
utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT")
|
||||
|
||||
def forcetag_nick(irc, target):
|
||||
def nick_collide(irc, target):
|
||||
"""
|
||||
Force tags the target UID's nick, if it is a relay client.
|
||||
|
||||
This method is used to handle nick collisions between relay clients and outside ones.
|
||||
|
||||
Returns the new nick if the operation succeeded; otherwise returns False.
|
||||
Handles nick collisions on relay clients and attempts to fix nicks.
|
||||
"""
|
||||
remote = get_orig_user(irc, target)
|
||||
if remote is None:
|
||||
return False
|
||||
|
||||
remotenet, remoteuser = remote
|
||||
try:
|
||||
remotenet, remoteuser = get_orig_user(irc, target)
|
||||
remoteirc = world.networkobjects[remotenet]
|
||||
|
||||
nick = remoteirc.users[remoteuser].nick
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
# Force a tagged nick by setting times_tagged to 1.
|
||||
newnick = normalize_nick(irc, remotenet, nick, times_tagged=1)
|
||||
log.debug('(%s) relay.forcetag_nick: Fixing nick of relay client %r (%s) to %s',
|
||||
log.debug('(%s) relay.nick_collide: Fixing nick of relay client %r (%s) to %s',
|
||||
irc.name, target, nick, newnick)
|
||||
|
||||
if nick == newnick:
|
||||
log.debug('(%s) relay.forcetag_nick: New nick %s for %r matches old nick %s',
|
||||
irc.name, newnick, target, nick)
|
||||
return False
|
||||
|
||||
irc.nick(target, newnick)
|
||||
return newnick
|
||||
|
||||
def handle_save(irc, numeric, command, args):
|
||||
target = args['target']
|
||||
@ -2262,7 +2128,7 @@ def handle_save(irc, numeric, command, args):
|
||||
# Nick collision!
|
||||
# It's one of our relay clients; try to fix our nick to the next
|
||||
# available normalized nick.
|
||||
forcetag_nick(irc, target)
|
||||
nick_collide(irc, target)
|
||||
else:
|
||||
# Somebody else on the network (not a PyLink client) had a nick collision;
|
||||
# relay this as a nick change appropriately.
|
||||
@ -2277,7 +2143,7 @@ def handle_svsnick(irc, numeric, command, args):
|
||||
target = args['target']
|
||||
|
||||
if is_relay_client(irc, target):
|
||||
forcetag_nick(irc, target)
|
||||
nick_collide(irc, target)
|
||||
|
||||
utils.add_hook(handle_svsnick, "SVSNICK")
|
||||
|
||||
@ -2348,7 +2214,7 @@ def create(irc, source, args):
|
||||
creator = irc.get_hostmask(source)
|
||||
# Create the relay database entry with the (network name, channel name)
|
||||
# pair - this is just a dict with various keys.
|
||||
db[(irc.name, str(channel))] = {'links': set(),
|
||||
db[(irc.name, channel)] = {'links': set(),
|
||||
'blocked_nets': set(),
|
||||
'creator': creator,
|
||||
'ts': time.time(),
|
||||
@ -2374,11 +2240,11 @@ def destroy(irc, source, args):
|
||||
|
||||
Removes the given channel from the PyLink Relay, delinking all networks linked to it. If the home network is given and you are logged in as admin, this can also remove relay channels from other networks."""
|
||||
try: # Two args were given: first one is network name, second is channel.
|
||||
channel = args[1]
|
||||
channel = irc.to_lower(args[1])
|
||||
network = args[0]
|
||||
except IndexError:
|
||||
try: # One argument was given; assume it's just the channel.
|
||||
channel = args[0]
|
||||
channel = irc.to_lower(args[0])
|
||||
network = irc.name
|
||||
except IndexError:
|
||||
irc.error("Not enough arguments. Needs 1-2: channel, network (optional).")
|
||||
@ -2395,11 +2261,7 @@ def destroy(irc, source, args):
|
||||
else:
|
||||
permissions.check_permissions(irc, source, ['relay.destroy.remote'])
|
||||
|
||||
# Allow deleting old channels if the local network's casemapping ever changes
|
||||
if (network, channel) in db:
|
||||
entry = (network, channel)
|
||||
else:
|
||||
entry = (network, irc.to_lower(channel))
|
||||
if entry in db:
|
||||
stop_relay(entry)
|
||||
del db[entry]
|
||||
@ -2460,17 +2322,14 @@ def link(irc, source, args):
|
||||
|
||||
args = link_parser.parse_args(args)
|
||||
|
||||
# Normalize channel case. For the target channel it's possible for the local and remote casemappings
|
||||
# to differ - if we find the unnormalized channel name in the list, we should just use that.
|
||||
# This mainly affects channels with e.g. | in them.
|
||||
channel_orig = str(args.channel)
|
||||
channel_norm = irc.to_lower(channel_orig)
|
||||
|
||||
localchan = irc.to_lower(str(args.localchannel or args.channel))
|
||||
# Normalize channel case
|
||||
channel = irc.to_lower(args.channel)
|
||||
localchan = irc.to_lower(args.localchannel or args.channel)
|
||||
remotenet = args.remotenet
|
||||
|
||||
if not irc.is_channel(localchan):
|
||||
irc.error('Invalid channel %r.' % localchan)
|
||||
for c in (channel, localchan):
|
||||
if not irc.is_channel(c):
|
||||
irc.error('Invalid channel %r.' % c)
|
||||
return
|
||||
|
||||
if remotenet == irc.name:
|
||||
@ -2509,15 +2368,12 @@ def link(irc, source, args):
|
||||
irc.error('Channel %r is already part of a relay.' % localchan)
|
||||
return
|
||||
|
||||
if (remotenet, channel_orig) in db:
|
||||
channel = channel_orig
|
||||
elif (remotenet, channel_norm) in db:
|
||||
channel = channel_norm
|
||||
else:
|
||||
try:
|
||||
entry = db[(remotenet, channel)]
|
||||
except KeyError:
|
||||
irc.error('No such relay %r exists.' % args.channel)
|
||||
return
|
||||
entry = db[(remotenet, channel)]
|
||||
|
||||
else:
|
||||
whitelist_mode = entry.get('use_whitelist', False)
|
||||
if ((not whitelist_mode) and irc.name in entry['blocked_nets']) or \
|
||||
(whitelist_mode and irc.name not in entry.get('allowed_nets', set())):
|
||||
@ -2795,6 +2651,73 @@ def linkacl(irc, source, args):
|
||||
else:
|
||||
irc.error('Unknown subcommand %r: valid ones are ALLOW, DENY, and LIST.' % cmd)
|
||||
|
||||
@utils.add_cmd
|
||||
def showuser(irc, source, args):
|
||||
"""<user>
|
||||
|
||||
Shows relay data about the given user. This supplements the 'showuser' command in the 'commands' plugin, which provides more general information."""
|
||||
try:
|
||||
target = args[0]
|
||||
except IndexError:
|
||||
# No errors here; showuser from the commands plugin already does this
|
||||
# for us.
|
||||
return
|
||||
u = irc.nick_to_uid(target)
|
||||
if u:
|
||||
irc.reply("Showing relay information on user \x02%s\x02:" % irc.users[u].nick, private=True)
|
||||
try:
|
||||
userpair = get_orig_user(irc, u) or (irc.name, u)
|
||||
remoteusers = relayusers[userpair].items()
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
nicks = []
|
||||
if remoteusers:
|
||||
nicks.append('%s:\x02%s\x02' % (userpair[0],
|
||||
world.networkobjects[userpair[0]].users[userpair[1]].nick))
|
||||
for r in remoteusers:
|
||||
remotenet, remoteuser = r
|
||||
remoteirc = world.networkobjects[remotenet]
|
||||
nicks.append('%s:\x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick))
|
||||
irc.reply("\x02Relay nicks\x02: %s" % ', '.join(nicks), private=True)
|
||||
relaychannels = []
|
||||
for ch in irc.users[u].channels:
|
||||
relay = get_relay(irc, ch)
|
||||
if relay:
|
||||
relaychannels.append(''.join(relay))
|
||||
if relaychannels and (irc.is_oper(source) or u == source):
|
||||
irc.reply("\x02Relay channels\x02: %s" % ' '.join(relaychannels), private=True)
|
||||
|
||||
@utils.add_cmd
|
||||
def showchan(irc, source, args):
|
||||
"""<user>
|
||||
|
||||
Shows relay data about the given channel. This supplements the 'showchan' command in the 'commands' plugin, which provides more general information."""
|
||||
try:
|
||||
channel = irc.to_lower(args[0])
|
||||
except IndexError:
|
||||
return
|
||||
if channel not in irc.channels:
|
||||
return
|
||||
|
||||
f = lambda s: irc.reply(s, private=True)
|
||||
|
||||
c = irc.channels[channel]
|
||||
|
||||
# Only show verbose info if caller is oper or is in the target channel.
|
||||
verbose = source in c.users or irc.is_oper(source)
|
||||
secret = ('s', None) in c.modes
|
||||
if secret and not verbose:
|
||||
# Hide secret channels from normal users.
|
||||
return
|
||||
|
||||
else:
|
||||
relayentry = get_relay(irc, channel)
|
||||
if relayentry:
|
||||
relays = ['\x02%s\x02' % ''.join(relayentry)]
|
||||
relays += [''.join(link) for link in db[relayentry]['links']]
|
||||
f('\x02Relayed channels:\x02 %s' % (' '.join(relays)))
|
||||
|
||||
@utils.add_cmd
|
||||
def save(irc, source, args):
|
||||
"""takes no arguments.
|
||||
@ -2861,7 +2784,7 @@ def modedelta(irc, source, args):
|
||||
Mode names are defined using PyLink named modes, and not IRC mode characters: you can find a
|
||||
list of channel named modes and the characters they map to on different IRCds at:
|
||||
|
||||
https://raw.githack.com/jlu5/PyLink/devel/docs/modelists/channel-modes.html
|
||||
https://raw.githack.com/GLolol/PyLink/devel/docs/modelists/channel-modes.html
|
||||
|
||||
Examples of setting modes:
|
||||
|
||||
@ -2926,8 +2849,7 @@ def modedelta(irc, source, args):
|
||||
|
||||
if modes:
|
||||
old_modes = db[relay].get('modedelta', [])
|
||||
db[relay]['modedelta'] = modes
|
||||
target_modes = modes.copy()
|
||||
db[relay]['modedelta'] = target_modes = modes
|
||||
log.debug('channel: %s', str(channel))
|
||||
irc.reply('Set the mode delta for \x02%s\x02 to: %s' % (channel, modes))
|
||||
else: # No modes given, so show the list.
|
||||
@ -2947,7 +2869,8 @@ def modedelta(irc, source, args):
|
||||
continue
|
||||
|
||||
remote_modes = []
|
||||
# For each leaf channel, unset the old mode delta and set the new one if applicable.
|
||||
# For each leaf channel, unset the old mode delta and set the new one
|
||||
# if applicable.
|
||||
log.debug('(%s) modedelta target modes for %s/%s: %s', irc.name, remotenet, remotechan, target_modes)
|
||||
for modepair in target_modes:
|
||||
modeprefix = modepair[0][0]
|
||||
@ -3008,24 +2931,3 @@ def chandesc(irc, source, args):
|
||||
irc.reply('Done. Updated the description for \x02%s\x02.' % channel)
|
||||
else:
|
||||
irc.reply('Description for \x02%s\x02: %s' % (channel, db[relay].get('description') or '\x1D(none)\x1D'))
|
||||
|
||||
@utils.add_cmd
|
||||
def forcetag(irc, source, args):
|
||||
"""<nick>
|
||||
|
||||
Attempts to forcetag the given nick, if it is a relay client.
|
||||
"""
|
||||
try:
|
||||
nick = args[0]
|
||||
except IndexError:
|
||||
irc.error("Not enough arguments. Needs 1: target nick.")
|
||||
return
|
||||
|
||||
permissions.check_permissions(irc, source, ['relay.forcetag'])
|
||||
|
||||
uid = irc.nick_to_uid(nick) or nick
|
||||
result = forcetag_nick(irc, uid)
|
||||
if result:
|
||||
irc.reply('Done. Forcetagged %s to %s' % (nick, result))
|
||||
else:
|
||||
irc.error('User %s is already tagged or not a relay client.' % nick)
|
||||
|
@ -1,9 +1,8 @@
|
||||
# relay_clientbot.py: Clientbot extensions for Relay
|
||||
import shlex
|
||||
import string
|
||||
import time
|
||||
|
||||
from pylinkirc import conf, utils, world
|
||||
from pylinkirc import utils, conf, world
|
||||
from pylinkirc.log import log
|
||||
|
||||
# Clientbot default styles:
|
||||
@ -27,8 +26,6 @@ def color_text(s):
|
||||
"""
|
||||
Returns a colorized version of the given text based on a simple hash algorithm.
|
||||
"""
|
||||
if not s:
|
||||
return s
|
||||
colors = ('03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '15')
|
||||
hash_output = hash(s.encode())
|
||||
num = hash_output % len(colors)
|
||||
@ -55,19 +52,15 @@ def cb_relay_core(irc, source, command, args):
|
||||
# Be less floody on startup: don't relay non-PRIVMSGs for the first X seconds after connect.
|
||||
startup_delay = relay_conf.get('clientbot_startup_delay', 20)
|
||||
|
||||
target = args.get('target')
|
||||
if isinstance(target, str):
|
||||
# Remove STATUSMSG prefixes (e.g. @#channel) before checking whether target is a channel
|
||||
target = target.lstrip(''.join(irc.prefixmodes.values()))
|
||||
|
||||
# Special case for CTCPs.
|
||||
if real_command == 'MESSAGE':
|
||||
# CTCP action, format accordingly
|
||||
if (not args.get('is_notice')) and args['text'].startswith('\x01ACTION ') and args['text'].endswith('\x01'):
|
||||
args['text'] = args['text'][8:-1]
|
||||
|
||||
real_command = 'ACTION'
|
||||
|
||||
elif not irc.is_channel(target):
|
||||
elif not irc.is_channel(args['target'].lstrip(''.join(irc.prefixmodes.values()))):
|
||||
# Target is a user; handle this accordingly.
|
||||
if relay_conf.get('allow_clientbot_pms'):
|
||||
real_command = 'PNOTICE' if args.get('is_notice') else 'PM'
|
||||
@ -87,7 +80,7 @@ def cb_relay_core(irc, source, command, args):
|
||||
# Try to fetch the format for the given command from the relay:clientbot_styles:$command
|
||||
# key, falling back to one defined in default_styles above, and then nothing if not found
|
||||
# there.
|
||||
text_template = irc.get_service_options('relay', 'clientbot_styles', dict).get(
|
||||
text_template = irc.get_service_option('relay', 'clientbot_styles', {}).get(
|
||||
real_command, default_styles.get(real_command, ''))
|
||||
text_template = string.Template(text_template)
|
||||
|
||||
@ -121,7 +114,7 @@ def cb_relay_core(irc, source, command, args):
|
||||
|
||||
# Figure out where the message is destined to.
|
||||
stripped_target = target = args.get('channel') or args.get('target')
|
||||
if isinstance(target, str):
|
||||
if target is not None:
|
||||
# HACK: cheap fix to prevent @#channel messages from interpreted as non-channel specific
|
||||
stripped_target = target.lstrip(''.join(irc.prefixmodes.values()))
|
||||
|
||||
@ -146,7 +139,7 @@ def cb_relay_core(irc, source, command, args):
|
||||
try:
|
||||
identhost = irc.get_hostmask(source).split('!')[-1]
|
||||
except KeyError: # User got removed due to quit
|
||||
identhost = '%s@%s' % (args['userdata'].ident, args['userdata'].host)
|
||||
identhost = '%s@%s' % (args['olduser'].ident, args['olduser'].host)
|
||||
# This is specifically spaced so that ident@host is only shown for users that have
|
||||
# one, and not servers.
|
||||
identhost = ' (%s)' % identhost
|
||||
@ -226,12 +219,11 @@ utils.add_hook(cb_relay_core, 'RELAY_RAW_MODE')
|
||||
|
||||
@utils.add_cmd
|
||||
def rpm(irc, source, args):
|
||||
"""<target nick/UID> <text>
|
||||
"""<target> <text>
|
||||
|
||||
Sends PMs to users over Relay, if Clientbot PMs are enabled.
|
||||
If the target nick has spaces in it, you may quote the nick as "nick".
|
||||
Sends PMs to users over the relay, if Clientbot PMs are enabled.
|
||||
"""
|
||||
args = shlex.split(' '.join(args)) # HACK: use shlex.split so that quotes are preserved
|
||||
|
||||
try:
|
||||
target = args[0]
|
||||
text = ' '.join(args[1:])
|
||||
@ -254,21 +246,15 @@ def rpm(irc, source, args):
|
||||
'administratively disabled.')
|
||||
return
|
||||
|
||||
if target in irc.users:
|
||||
uids = [target]
|
||||
else:
|
||||
uids = irc.nick_to_uid(target, multi=True, filterfunc=lambda u: relay.is_relay_client(irc, u))
|
||||
|
||||
if not uids:
|
||||
uid = irc.nick_to_uid(target)
|
||||
if not uid:
|
||||
irc.error('Unknown user %s.' % target)
|
||||
return
|
||||
elif len(uids) > 1:
|
||||
targets = ['\x02%s\x02: %s @ %s' % (uid, irc.get_hostmask(uid), irc.users[uid].remote[0]) for uid in uids]
|
||||
irc.error('Please select the target you want to PM: %s' % (', '.join(targets)))
|
||||
elif not relay.is_relay_client(irc, uid):
|
||||
irc.error('%s is not a relay user.' % target)
|
||||
return
|
||||
else:
|
||||
assert not irc.is_internal_client(source), "rpm is not allowed from PyLink bots"
|
||||
|
||||
# Send the message through relay by faking a hook for its handler.
|
||||
relay.handle_messages(irc, source, 'RELAY_CLIENTBOT_PRIVMSG', {'target': uids[0], 'text': text})
|
||||
relay.handle_messages(irc, source, 'RELAY_CLIENTBOT_PRIVMSG', {'target': uid, 'text': text})
|
||||
irc.reply('Message sent.')
|
||||
|
@ -1,10 +1,10 @@
|
||||
# servermaps.py: Maps out connected IRC servers.
|
||||
|
||||
import collections
|
||||
|
||||
from pylinkirc import utils, world
|
||||
from pylinkirc.coremods import permissions
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.coremods import permissions
|
||||
|
||||
import collections
|
||||
|
||||
DEFAULT_PERMISSIONS = {"$ircop": ['servermaps.localmap']}
|
||||
|
||||
|
@ -1,27 +1,16 @@
|
||||
# servprotect.py: Protects against KILL and nick collision floods
|
||||
from expiringdict import ExpiringDict
|
||||
|
||||
import threading
|
||||
|
||||
from pylinkirc import conf, utils
|
||||
from pylinkirc import utils, conf
|
||||
from pylinkirc.log import log
|
||||
|
||||
try:
|
||||
from cachetools import TTLCache
|
||||
except ImportError:
|
||||
log.warning('servprotect: expiringdict support is deprecated as of PyLink 3.0; consider installing cachetools instead')
|
||||
from expiringdict import ExpiringDict as TTLCache
|
||||
|
||||
# check for definitions
|
||||
servprotect_conf = conf.conf.get('servprotect', {})
|
||||
length = servprotect_conf.get('length', 10)
|
||||
age = servprotect_conf.get('age', 10)
|
||||
|
||||
def _new_cache_dict():
|
||||
return TTLCache(length, age)
|
||||
|
||||
savecache = _new_cache_dict()
|
||||
killcache = _new_cache_dict()
|
||||
lock = threading.Lock()
|
||||
savecache = ExpiringDict(max_len=length, max_age_seconds=age)
|
||||
killcache = ExpiringDict(max_len=length, max_age_seconds=age)
|
||||
|
||||
def handle_kill(irc, numeric, command, args):
|
||||
"""
|
||||
@ -30,7 +19,6 @@ def handle_kill(irc, numeric, command, args):
|
||||
"""
|
||||
|
||||
if (args['userdata'] and irc.is_internal_server(args['userdata'].server)) or irc.is_internal_client(args['target']):
|
||||
with lock:
|
||||
if killcache.setdefault(irc.name, 1) >= length:
|
||||
log.error('(%s) servprotect: Too many kills received, aborting!', irc.name)
|
||||
irc.disconnect()
|
||||
@ -46,7 +34,6 @@ def handle_save(irc, numeric, command, args):
|
||||
automatically disconnects from the network.
|
||||
"""
|
||||
if irc.is_internal_client(args['target']):
|
||||
with lock:
|
||||
if savecache.setdefault(irc.name, 0) >= length:
|
||||
log.error('(%s) servprotect: Too many nick collisions, aborting!', irc.name)
|
||||
irc.disconnect()
|
||||
|
@ -1,13 +1,12 @@
|
||||
"""
|
||||
stats.py: Simple statistics for PyLink IRC Services.
|
||||
"""
|
||||
import datetime
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from pylinkirc import conf, utils, world
|
||||
from pylinkirc.coremods import permissions
|
||||
from pylinkirc import utils, world, conf
|
||||
from pylinkirc.log import log
|
||||
|
||||
from pylinkirc.coremods import permissions
|
||||
|
||||
def timediff(before, now):
|
||||
"""
|
||||
@ -113,11 +112,13 @@ def handle_stats(irc, source, command, args):
|
||||
# 243/RPL_STATSOLINE: "O <hostmask> * <nick> [:<info>]"
|
||||
# New style accounts only!
|
||||
for accountname, accountdata in conf.conf['login'].get('accounts', {}).items():
|
||||
networks = accountdata.get('networks', [])
|
||||
if irc.name in networks or not networks:
|
||||
hosts = ' '.join(accountdata.get('hosts', ['*@*']))
|
||||
needoper = 'needoper' if accountdata.get('require_oper') else ''
|
||||
_num(243, "O %s * %s :%s" % (hosts, accountname, needoper))
|
||||
_num(243, "O %s * %s :network_filter:%s require_oper:%s" %
|
||||
(' '.join(accountdata.get('hosts', [])) or '*@*',
|
||||
accountname,
|
||||
','.join(accountdata.get('networks', [])) or '*',
|
||||
bool(accountdata.get('require_oper'))
|
||||
)
|
||||
)
|
||||
|
||||
elif stats_type == 'u':
|
||||
# 242/RPL_STATSUPTIME: ":Server Up <days> days <hours>:<minutes>:<seconds>"
|
||||
|
@ -1,2 +1 @@
|
||||
# Abstract modules containing shared protocol code; modules higher in the hierarchy go first
|
||||
common_modules = ['ircs2s_common', 'ts6_common']
|
||||
# Stub so that pylinkirc.protocols is a module
|
||||
|
@ -6,17 +6,14 @@ clientbot.py: Clientbot (regular IRC bot) protocol module for PyLink.
|
||||
# that a regular server would have (e.g. spawning virtual users for things like Relay). Somehow it
|
||||
# works on most networks though!
|
||||
|
||||
import base64
|
||||
import string
|
||||
import threading
|
||||
import time
|
||||
import threading
|
||||
import base64
|
||||
|
||||
from pylinkirc import utils, world
|
||||
from pylinkirc.classes import *
|
||||
from pylinkirc import utils, conf
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.protocols.ircs2s_common import *
|
||||
|
||||
__all__ = ['ClientbotBaseProtocol', 'ClientbotWrapperProtocol']
|
||||
from pylinkirc.classes import *
|
||||
|
||||
FALLBACK_REALNAME = 'PyLink Relay Mirror Client'
|
||||
|
||||
@ -24,198 +21,18 @@ FALLBACK_REALNAME = 'PyLink Relay Mirror Client'
|
||||
IRCV3_CAPABILITIES = {'multi-prefix', 'sasl', 'away-notify', 'userhost-in-names', 'chghost', 'account-notify',
|
||||
'account-tag', 'extended-join'}
|
||||
|
||||
class ClientbotBaseProtocol(PyLinkNetworkCoreWithUtils):
|
||||
class ClientbotWrapperProtocol(IRCCommonProtocol):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.protocol_caps |= {'visible-state-only', 'slash-in-nicks', 'slash-in-hosts', 'underscore-in-hosts',
|
||||
'freeform-nicks'}
|
||||
self.protocol_caps = {'visible-state-only', 'slash-in-nicks', 'slash-in-hosts', 'underscore-in-hosts',
|
||||
'ssl-should-verify'}
|
||||
|
||||
self.has_eob = False
|
||||
|
||||
# Remove conf key checks for those not needed for Clientbot.
|
||||
self.conf_keys -= {'recvpass', 'sendpass', 'sid', 'sidrange', 'hostname'}
|
||||
|
||||
def _get_UID(self, nick, ident=None, host=None, spawn_new=False):
|
||||
"""
|
||||
Fetches the UID for the given nick, creating one if it does not already exist and spawn_new is True.
|
||||
|
||||
To prevent message spoofing, this will only return an external (non-PyLink) client or the PyLink bot itself.
|
||||
"""
|
||||
#log.debug('(%s) _get_UID: searching for nick %s', self.name, nick, stack_info=True)
|
||||
idsource = self.nick_to_uid(nick, filterfunc=lambda uid: uid == self.pseudoclient.uid or not self.is_internal_client(uid))
|
||||
|
||||
if idsource is None and spawn_new:
|
||||
# If this sender doesn't already exist, spawn a new client.
|
||||
idsource = self.spawn_client(nick, ident or 'unknown', host or 'unknown',
|
||||
server=self.uplink, realname=FALLBACK_REALNAME).uid
|
||||
return idsource or nick # Return input if missing per upstream spec
|
||||
|
||||
def away(self, source, text):
|
||||
"""STUB: sets away messages for internal clients."""
|
||||
log.debug('(%s) away: target is %s, internal client? %s', self.name, source, self.is_internal_client(source))
|
||||
|
||||
if self.users[source].away != text:
|
||||
if not self.is_internal_client(source):
|
||||
log.debug('(%s) away: sending AWAY hook from %s with text %r', self.name, source, text)
|
||||
self.call_hooks([source, 'AWAY', {'text': text}])
|
||||
|
||||
self.users[source].away = text
|
||||
|
||||
def join(self, client, channel):
|
||||
"""STUB: sends a virtual join (CLIENTBOT_JOIN) from the client to channel."""
|
||||
self._channels[channel].users.add(client)
|
||||
self.users[client].channels.add(channel)
|
||||
|
||||
if self.pseudoclient and client != self.pseudoclient:
|
||||
log.debug('(%s) join: faking JOIN of client %s/%s to %s', self.name, client,
|
||||
self.get_friendly_name(client), channel)
|
||||
self.call_hooks([client, 'CLIENTBOT_JOIN', {'channel': channel}])
|
||||
|
||||
def kick(self, source, channel, target, reason=''):
|
||||
"""STUB: rejoins users on kick attempts, for server transports where kicking users from channels is not implemented."""
|
||||
if not self.is_internal_client(target):
|
||||
log.info("(%s) Rejoining user %s to %s since kicks are not supported here.", self.name, target, channel)
|
||||
self.join(target, channel)
|
||||
self.call_hooks([None, 'JOIN', {'channel': channel, 'users': [target], 'modes': []}])
|
||||
elif channel in self.channels:
|
||||
self.channels[channel].remove_user(target)
|
||||
self.users[target].channels.discard(channel)
|
||||
self.call_hooks([source, 'CLIENTBOT_KICK', {'channel': channel, 'target': target, 'text': reason}])
|
||||
else:
|
||||
log.warning('(%s) Possible desync? Tried to kick() on non-existent channel %s', self.name, channel)
|
||||
|
||||
def message(self, source, target, text, notice=False):
|
||||
"""STUB: Sends messages to the target."""
|
||||
if self.pseudoclient and self.pseudoclient.uid != source:
|
||||
self.call_hooks([source, 'CLIENTBOT_MESSAGE', {'target': target, 'is_notice': notice, 'text': text}])
|
||||
|
||||
def nick(self, source, newnick):
|
||||
"""STUB: sends a virtual nick change (CLIENTBOT_NICK)."""
|
||||
assert source, "No source given?"
|
||||
self.call_hooks([source, 'CLIENTBOT_NICK', {'newnick': newnick}])
|
||||
self.users[source].nick = newnick
|
||||
|
||||
def notice(self, source, target, text):
|
||||
"""Sends notices to the target."""
|
||||
# Wrap around message(), which does all the text formatting for us.
|
||||
self.message(source, target, text, notice=True)
|
||||
|
||||
def sjoin(self, server, channel, users, ts=None, modes=set()):
|
||||
"""STUB: bursts joins from a server."""
|
||||
# This stub only updates the state internally with the users given. modes and TS are currently ignored.
|
||||
puids = {u[-1] for u in users}
|
||||
for user in puids:
|
||||
self.users[user].channels.add(channel)
|
||||
|
||||
self._channels[channel].users |= puids
|
||||
nicks = {self.get_friendly_name(u) for u in puids}
|
||||
self.call_hooks([server, 'CLIENTBOT_SJOIN', {'channel': channel, 'nicks': nicks}])
|
||||
|
||||
# Note: clientbot clients are initialized with umode +i by default
|
||||
def spawn_client(self, nick, ident='unknown', host='unknown.host', realhost=None, modes={('i', None)},
|
||||
server=None, ip='0.0.0.0', realname='', ts=None, opertype=None,
|
||||
manipulatable=False):
|
||||
"""
|
||||
STUB: Pretends to spawn a new client with a subset of the given options.
|
||||
"""
|
||||
|
||||
server = server or self.sid
|
||||
uid = self.uidgen.next_uid(prefix=nick)
|
||||
|
||||
ts = ts or int(time.time())
|
||||
|
||||
log.debug('(%s) spawn_client stub called, saving nick %s as PUID %s', self.name, nick, uid)
|
||||
u = self.users[uid] = User(self, nick, ts, uid, server, ident=ident, host=host, realname=realname,
|
||||
manipulatable=manipulatable, realhost=realhost, ip=ip)
|
||||
self.servers[server].users.add(uid)
|
||||
|
||||
self.apply_modes(uid, modes)
|
||||
|
||||
return u
|
||||
|
||||
def spawn_server(self, name, sid=None, uplink=None, desc=None, internal=True):
|
||||
"""
|
||||
STUB: Pretends to spawn a new server with a subset of the given options.
|
||||
"""
|
||||
if internal:
|
||||
# Use a custom pseudo-SID format for internal servers to prevent any server name clashes
|
||||
sid = self.sidgen.next_sid(prefix=name)
|
||||
else:
|
||||
# For others servers, just use the server name as the SID.
|
||||
sid = name
|
||||
|
||||
self.servers[sid] = Server(self, uplink, name, internal=internal)
|
||||
return sid
|
||||
|
||||
def squit(self, source, target, text):
|
||||
"""STUB: SQUITs a server."""
|
||||
# What this actually does is just handle the SQUIT internally: i.e.
|
||||
# Removing pseudoclients and pseudoservers.
|
||||
squit_data = self._squit(source, 'CLIENTBOT_VIRTUAL_SQUIT', [target, text])
|
||||
|
||||
if squit_data and squit_data.get('nicks'):
|
||||
self.call_hooks([source, 'CLIENTBOT_SQUIT', squit_data])
|
||||
|
||||
def part(self, source, channel, reason=''):
|
||||
"""STUB: Parts a user from a channel."""
|
||||
if self.pseudoclient and source == self.pseudoclient.uid:
|
||||
raise NotImplementedError("Explicitly leaving channels is not supported here.")
|
||||
self._channels[channel].remove_user(source)
|
||||
self.users[source].channels.discard(channel)
|
||||
self.call_hooks([source, 'CLIENTBOT_PART', {'channel': channel, 'text': reason}])
|
||||
|
||||
def quit(self, source, reason):
|
||||
"""STUB: Quits a client."""
|
||||
userdata = self._remove_client(source)
|
||||
self.call_hooks([source, 'CLIENTBOT_QUIT', {'text': reason, 'userdata': userdata}])
|
||||
|
||||
def _stub(self, *args):
|
||||
"""Stub outgoing command function (does nothing)."""
|
||||
return
|
||||
# Note: invite() and mode() are implemented in ClientbotWrapperProtocol below
|
||||
invite = mode = topic = topic_burst = _stub # XXX: incomplete
|
||||
|
||||
def _stub_raise(self, *args):
|
||||
"""Stub outgoing command function (raises an error)."""
|
||||
raise NotImplementedError("Not supported on Clientbot")
|
||||
kill = knock = numeric = _stub_raise
|
||||
|
||||
def update_client(self, target, field, text):
|
||||
"""Updates the known ident, host, or realname of a client."""
|
||||
# Note: unlike other protocol modules, this function is also called as a helper to
|
||||
# update data for external clients.
|
||||
# Following this, we only want to send hook payloads if the target is an external client.
|
||||
if target not in self.users:
|
||||
log.warning("(%s) Unknown target %s for update_client()", self.name, target)
|
||||
return
|
||||
|
||||
u = self.users[target]
|
||||
|
||||
if field == 'IDENT' and u.ident != text:
|
||||
u.ident = text
|
||||
if not self.is_internal_client(target):
|
||||
self.call_hooks([self.sid, 'CHGIDENT',
|
||||
{'target': target, 'newident': text}])
|
||||
elif field == 'HOST' and u.host != text:
|
||||
u.host = text
|
||||
if not self.is_internal_client(target):
|
||||
self.call_hooks([self.sid, 'CHGHOST',
|
||||
{'target': target, 'newhost': text}])
|
||||
elif field in ('REALNAME', 'GECOS') and u.realname != text:
|
||||
u.realname = text
|
||||
if not self.is_internal_client(target):
|
||||
self.call_hooks([self.sid, 'CHGNAME',
|
||||
{'target': target, 'newgecos': text}])
|
||||
else:
|
||||
return # Nothing changed
|
||||
|
||||
class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.protocol_caps |= {'ssl-should-verify'}
|
||||
|
||||
self.has_eob = False
|
||||
|
||||
# This is just a fallback. Actual casemapping is fetched by handle_005()
|
||||
self.casemapping = 'ascii'
|
||||
|
||||
@ -292,6 +109,52 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
ident=ident, realname=realname, host=self.hostname())
|
||||
self.users[self.pseudoclient.uid] = self.pseudoclient
|
||||
|
||||
# Note: clientbot clients are initialized with umode +i by default
|
||||
def spawn_client(self, nick, ident='unknown', host='unknown.host', realhost=None, modes={('i', None)},
|
||||
server=None, ip='0.0.0.0', realname='', ts=None, opertype=None,
|
||||
manipulatable=False):
|
||||
"""
|
||||
STUB: Pretends to spawn a new client with a subset of the given options.
|
||||
"""
|
||||
|
||||
server = server or self.sid
|
||||
uid = self.uidgen.next_uid(prefix=nick)
|
||||
|
||||
ts = ts or int(time.time())
|
||||
|
||||
log.debug('(%s) spawn_client stub called, saving nick %s as PUID %s', self.name, nick, uid)
|
||||
u = self.users[uid] = User(self, nick, ts, uid, server, ident=ident, host=host, realname=realname,
|
||||
manipulatable=manipulatable, realhost=realhost, ip=ip)
|
||||
self.servers[server].users.add(uid)
|
||||
|
||||
self.apply_modes(uid, modes)
|
||||
|
||||
return u
|
||||
|
||||
def spawn_server(self, name, sid=None, uplink=None, desc=None, internal=True):
|
||||
"""
|
||||
STUB: Pretends to spawn a new server with a subset of the given options.
|
||||
"""
|
||||
if internal:
|
||||
# Use a custom pseudo-SID format for internal servers to prevent any server name clashes
|
||||
sid = self.sidgen.next_sid(prefix=name)
|
||||
else:
|
||||
# For others servers, just use the server name as the SID.
|
||||
sid = name
|
||||
|
||||
self.servers[sid] = Server(self, uplink, name, internal=internal)
|
||||
return sid
|
||||
|
||||
def away(self, source, text):
|
||||
"""STUB: sets away messages for clients internally."""
|
||||
log.debug('(%s) away: target is %s, internal client? %s', self.name, source, self.is_internal_client(source))
|
||||
|
||||
if self.users[source].away != text:
|
||||
if not self.is_internal_client(source):
|
||||
log.debug('(%s) away: sending AWAY hook from %s with text %r', self.name, source, text)
|
||||
self.call_hooks([source, 'AWAY', {'text': text}])
|
||||
|
||||
self.users[source].away = text
|
||||
|
||||
def invite(self, client, target, channel):
|
||||
"""Invites a user to a channel."""
|
||||
@ -306,8 +169,12 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
if self.pseudoclient and client == self.pseudoclient.uid:
|
||||
self.send('JOIN %s' % channel)
|
||||
else:
|
||||
# Pass on a virtual JOIN as a hook
|
||||
super().join(client, channel)
|
||||
self._channels[channel].users.add(client)
|
||||
self.users[client].channels.add(channel)
|
||||
|
||||
log.debug('(%s) join: faking JOIN of client %s/%s to %s', self.name, client,
|
||||
self.get_friendly_name(client), channel)
|
||||
self.call_hooks([client, 'CLIENTBOT_JOIN', {'channel': channel}])
|
||||
|
||||
def kick(self, source, channel, target, reason=''):
|
||||
"""Sends channel kicks."""
|
||||
@ -315,7 +182,7 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
log.debug('(%s) kick: checking if target %s (nick: %s) is an internal client? %s',
|
||||
self.name, target, self.get_friendly_name(target),
|
||||
self.is_internal_client(target))
|
||||
if self.is_internal_client(target) and (self.pseudoclient and source != self.pseudoclient.uid):
|
||||
if self.is_internal_client(target):
|
||||
# Target was one of our virtual clients. Just remove them from the state.
|
||||
self.handle_part(target, 'KICK', [channel, reason])
|
||||
|
||||
@ -348,8 +215,7 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
if self.pseudoclient and self.pseudoclient.uid == source:
|
||||
self.send('%s %s :%s' % (command, self._expandPUID(target), text))
|
||||
else:
|
||||
# Pass the message on as a hook
|
||||
super().message(source, target, text, notice=notice)
|
||||
self.call_hooks([source, 'CLIENTBOT_MESSAGE', {'target': target, 'is_notice': notice, 'text': text}])
|
||||
|
||||
def mode(self, source, channel, modes, ts=None):
|
||||
"""Sends channel MODE changes."""
|
||||
@ -378,7 +244,7 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
log.debug('(%s) mode: filtered modes for %s: %s', self.name, channel, extmodes)
|
||||
if extmodes:
|
||||
bufsize = self.S2S_BUFSIZE - len(':%s MODE %s ' % (self.get_hostmask(self.pseudoclient.uid), channel))
|
||||
for msg in self.wrap_modes(extmodes, bufsize, max_modes_per_msg=int(self._caps.get('MODES') or 0)):
|
||||
for msg in self.wrap_modes(extmodes, bufsize, max_modes_per_msg=int(self._caps.get('MODES', 0))):
|
||||
self.send('MODE %s %s' % (channel, msg))
|
||||
# Don't update the state here: the IRCd sill respond with a MODE reply if successful.
|
||||
|
||||
@ -388,7 +254,20 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
self.send('NICK :%s' % newnick)
|
||||
# No state update here: the IRCd will respond with a NICK acknowledgement if the change succeeds.
|
||||
else:
|
||||
super().nick(source, newnick)
|
||||
assert source, "No source given?"
|
||||
# Check that the new nick exists and isn't the same client as the sender
|
||||
# (for changing nick case)
|
||||
nick_uid = self.nick_to_uid(newnick)
|
||||
if nick_uid and nick_uid != source:
|
||||
log.warning('(%s) Blocking attempt from virtual client %s to change nick to %s (nick in use)', self.name, source, newnick)
|
||||
return
|
||||
self.call_hooks([source, 'CLIENTBOT_NICK', {'newnick': newnick}])
|
||||
self.users[source].nick = newnick
|
||||
|
||||
def notice(self, source, target, text):
|
||||
"""Sends notices to the target."""
|
||||
# Wrap around message(), which does all the text formatting for us.
|
||||
self.message(source, target, text, notice=True)
|
||||
|
||||
def _ping_uplink(self):
|
||||
"""
|
||||
@ -401,13 +280,6 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
for channel in self.pseudoclient.channels:
|
||||
self._send_who(channel)
|
||||
|
||||
# Join persistent channels if always_autorejoin is enabled and there are any we're not in
|
||||
if self.serverdata.get('always_autorejoin') and self.has_cap('can-manage-bot-channels'):
|
||||
for channel in world.services['pylink'].get_persistent_channels(self):
|
||||
if channel not in self.pseudoclient.channels:
|
||||
log.info('(%s) Attempting to rejoin %s', self.name, channel)
|
||||
self.join(self.pseudoclient.uid, channel)
|
||||
|
||||
def part(self, source, channel, reason=''):
|
||||
"""STUB: Parts a user from a channel."""
|
||||
self._channels[channel].remove_user(source)
|
||||
@ -418,7 +290,13 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
self._channels[channel]._clientbot_part_requested = True
|
||||
self.send('PART %s :%s' % (channel, reason))
|
||||
else:
|
||||
super().part(source, channel, reason=reason)
|
||||
self.call_hooks([source, 'CLIENTBOT_PART', {'channel': channel, 'text': reason}])
|
||||
|
||||
def quit(self, source, reason):
|
||||
"""STUB: Quits a client."""
|
||||
userdata = self.users[source]
|
||||
self._remove_client(source)
|
||||
self.call_hooks([source, 'CLIENTBOT_QUIT', {'text': reason, 'userdata': userdata}])
|
||||
|
||||
def sjoin(self, server, channel, users, ts=None, modes=set()):
|
||||
"""STUB: bursts joins from a server."""
|
||||
@ -437,6 +315,99 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
nicks = {self.get_friendly_name(u) for u in puids}
|
||||
self.call_hooks([server, 'CLIENTBOT_SJOIN', {'channel': channel, 'nicks': nicks}])
|
||||
|
||||
def squit(self, source, target, text):
|
||||
"""STUB: SQUITs a server."""
|
||||
# What this actually does is just handle the SQUIT internally: i.e.
|
||||
# Removing pseudoclients and pseudoservers.
|
||||
squit_data = self._squit(source, 'CLIENTBOT_VIRTUAL_SQUIT', [target, text])
|
||||
|
||||
if squit_data and squit_data.get('nicks'):
|
||||
self.call_hooks([source, 'CLIENTBOT_SQUIT', squit_data])
|
||||
|
||||
def _stub(self, *args):
|
||||
"""Stub outgoing command function (does nothing)."""
|
||||
return
|
||||
topic = topic_burst = _stub # XXX: incomplete
|
||||
|
||||
def _stub_raise(self, *args):
|
||||
"""Stub outgoing command function (raises an error)."""
|
||||
raise NotImplementedError("Not supported on Clientbot")
|
||||
kill = knock = numeric = _stub_raise
|
||||
|
||||
def update_client(self, target, field, text):
|
||||
"""Updates the known ident, host, or realname of a client."""
|
||||
if target not in self.users:
|
||||
log.warning("(%s) Unknown target %s for update_client()", self.name, target)
|
||||
return
|
||||
|
||||
u = self.users[target]
|
||||
|
||||
if field == 'IDENT' and u.ident != text:
|
||||
u.ident = text
|
||||
if not self.is_internal_client(target):
|
||||
# We're updating the host of an external client in our state, so send the appropriate
|
||||
# hook payloads.
|
||||
self.call_hooks([self.sid, 'CHGIDENT',
|
||||
{'target': target, 'newident': text}])
|
||||
elif field == 'HOST' and u.host != text:
|
||||
u.host = text
|
||||
if not self.is_internal_client(target):
|
||||
self.call_hooks([self.sid, 'CHGHOST',
|
||||
{'target': target, 'newhost': text}])
|
||||
elif field in ('REALNAME', 'GECOS') and u.realname != text:
|
||||
u.realname = text
|
||||
if not self.is_internal_client(target):
|
||||
self.call_hooks([self.sid, 'CHGNAME',
|
||||
{'target': target, 'newgecos': text}])
|
||||
else:
|
||||
return # Nothing changed
|
||||
|
||||
def _get_UID(self, nick, ident=None, host=None):
|
||||
"""
|
||||
Fetches the UID for the given nick, creating one if it does not already exist.
|
||||
|
||||
Limited (internal) nick collision checking is done here to prevent Clientbot users from
|
||||
being confused with virtual clients, and vice versa."""
|
||||
self._check_puid_collision(nick)
|
||||
|
||||
idsource = self.nick_to_uid(nick)
|
||||
|
||||
if self.is_internal_client(idsource) and self.pseudoclient and idsource != self.pseudoclient.uid:
|
||||
# We got a message from a client with the same nick as an internal client.
|
||||
# Fire a virtual nick collision to prevent mixing senders.
|
||||
log.debug('(%s) Nick-colliding virtual client %s/%s', self.name, idsource, nick)
|
||||
self.call_hooks([self.sid, 'SAVE', {'target': idsource}])
|
||||
|
||||
# Clear the UID for this nick and spawn a new client for the nick that was just freed.
|
||||
idsource = None
|
||||
|
||||
if idsource is None:
|
||||
# If this sender doesn't already exist, spawn a new client.
|
||||
idsource = self.spawn_client(nick, ident or 'unknown', host or 'unknown',
|
||||
server=self.uplink, realname=FALLBACK_REALNAME).uid
|
||||
return idsource
|
||||
|
||||
def parse_message_tags(self, data):
|
||||
"""
|
||||
Parses a message with IRC v3.2 message tags, as described at http://ircv3.net/specs/core/message-tags-3.2.html
|
||||
"""
|
||||
# Example query:
|
||||
# @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello
|
||||
if data[0].startswith('@'):
|
||||
tagdata = data[0].lstrip('@').split(';')
|
||||
for idx, tag in enumerate(tagdata):
|
||||
tag = tag.replace(r'\s', ' ')
|
||||
tag = tag.replace(r'\\', '\\')
|
||||
tag = tag.replace(r'\r', '\r')
|
||||
tag = tag.replace(r'\n', '\n')
|
||||
tag = tag.replace(r'\:', ';')
|
||||
tagdata[idx] = tag
|
||||
|
||||
results = self.parse_isupport(tagdata, fallback=None)
|
||||
log.debug('(%s) parsed message tags %s', self.name, results)
|
||||
return results
|
||||
return {}
|
||||
|
||||
def _set_account_name(self, uid, account):
|
||||
"""
|
||||
Updates the user's account metadata.
|
||||
@ -487,7 +458,7 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
except ValueError:
|
||||
ident = host = None # Set ident and host as null for now.
|
||||
nick = sender # Treat the sender prefix we received as a nick.
|
||||
idsource = self._get_UID(nick, ident, host, spawn_new=True)
|
||||
idsource = self._get_UID(nick, ident, host)
|
||||
|
||||
if idsource in self.users:
|
||||
# Handle IRCv3.2 account-tag.
|
||||
@ -698,7 +669,7 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
"""
|
||||
Handles 353 / RPL_NAMREPLY.
|
||||
"""
|
||||
# <- :charybdis.midnight.vpn 353 ice = #test :ice @jlu5
|
||||
# <- :charybdis.midnight.vpn 353 ice = #test :ice @GL
|
||||
|
||||
# Mark "@"-type channels as secret automatically, per RFC2812.
|
||||
channel = args[2]
|
||||
@ -710,26 +681,18 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
prefix_to_mode = {v:k for k, v in self.prefixmodes.items()}
|
||||
prefixes = ''.join(self.prefixmodes.values())
|
||||
|
||||
# N.B. only split on spaces because of color in hosts nonsense...
|
||||
# str.split() by default treats \x1f as whitespace
|
||||
for name in args[-1].strip().split(' '):
|
||||
for name in args[-1].split():
|
||||
nick = name.lstrip(prefixes)
|
||||
|
||||
# Handle userhost-in-names where available.
|
||||
ident = host = None
|
||||
if 'userhost-in-names' in self.ircv3_caps:
|
||||
try:
|
||||
nick, ident, host = utils.split_hostmask(nick)
|
||||
except ValueError:
|
||||
log.exception('(%s) Failed to split hostmask %r from /names reply on %s; args=%s', self.name, nick, channel, args)
|
||||
# If error, leave nick unsplit
|
||||
|
||||
if not nick:
|
||||
continue
|
||||
else:
|
||||
ident = host = None
|
||||
|
||||
# Get the PUID for the given nick. If one doesn't exist, spawn
|
||||
# a new virtual user.
|
||||
idsource = self._get_UID(nick, ident=ident, host=host, spawn_new=True)
|
||||
idsource = self._get_UID(nick, ident=ident, host=host)
|
||||
|
||||
# Queue these virtual users to be joined if they're not already in the channel,
|
||||
# or we're waiting for a kick acknowledgment for them.
|
||||
@ -757,12 +720,19 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
# Send JOIN hook payloads only for users that we know the ident@host of already.
|
||||
# This is mostly used to resync kicked Clientbot users that can't actually be kicked
|
||||
# after a delay.
|
||||
if names and hasattr(self.channels[channel], '_clientbot_initial_who_received'):
|
||||
if names and hasattr(self.irc.channels[channel], '_clientbot_initial_who_received'):
|
||||
log.debug('(%s) handle_353: sending JOIN hook because /WHO was already received for %s',
|
||||
self.name, channel)
|
||||
self.irc.name, channel)
|
||||
return {'channel': channel, 'users': names, 'modes': self._channels[channel].modes,
|
||||
'parse_as': "JOIN"}
|
||||
|
||||
def _check_puid_collision(self, nick):
|
||||
"""
|
||||
Checks to make sure a nick doesn't clash with a PUID.
|
||||
"""
|
||||
if nick in self.users or nick in self.servers:
|
||||
raise ProtocolError("Got bad nick %s from IRC which clashes with a PUID. Is someone trying to spoof users?" % nick)
|
||||
|
||||
def _send_who(self, channel):
|
||||
"""Sends /WHO to a channel, with WHOX args if that is supported."""
|
||||
# Note: %% = escaped %
|
||||
@ -779,10 +749,10 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
"""
|
||||
# parameter count: 0 1 2 3 4 5 6 7(-1)
|
||||
# <- :charybdis.midnight.vpn 352 ice #test ~pylink 127.0.0.1 charybdis.midnight.vpn ice H+ :0 PyLink
|
||||
# <- :charybdis.midnight.vpn 352 ice #test ~jlu5 127.0.0.1 charybdis.midnight.vpn jlu5 H*@ :0 realname
|
||||
# <- :charybdis.midnight.vpn 352 ice #test ~gl 127.0.0.1 charybdis.midnight.vpn GL H*@ :0 realname
|
||||
# with WHO %cuhsnfar (WHOX) - note, hopcount and realname are separate!
|
||||
# 0 1 2 3 4 5 6 7 8(-1)
|
||||
# <- :charybdis.midnight.vpn 354 ice #test ~jlu5 localhost charybdis.midnight.vpn jlu5 H*@ jlu5 :realname
|
||||
# <- :charybdis.midnight.vpn 354 ice #test ~gl localhost charybdis.midnight.vpn GL H*@ GL :realname
|
||||
channel = args[1]
|
||||
ident = args[2]
|
||||
host = args[3]
|
||||
@ -794,7 +764,8 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
if command == '352':
|
||||
realname = realname.split(' ', 1)[-1]
|
||||
|
||||
uid = self._get_UID(nick, spawn_new=False)
|
||||
self._check_puid_collision(nick)
|
||||
uid = self.nick_to_uid(nick)
|
||||
|
||||
if uid is None:
|
||||
log.debug("(%s) Ignoring extraneous /WHO info for %s", self.name, nick)
|
||||
@ -907,9 +878,9 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
Handles incoming JOINs, as well as JOIN acknowledgements for us.
|
||||
"""
|
||||
# Classic format:
|
||||
# <- :jlu5|!~jlu5@127.0.0.1 JOIN #whatever
|
||||
# <- :GL|!~GL@127.0.0.1 JOIN #whatever
|
||||
# With extended-join:
|
||||
# <- :jlu5|!~jlu5@127.0.0.1 JOIN #whatever accountname :realname
|
||||
# <- :GL|!~GL@127.0.0.1 JOIN #whatever accountname :realname
|
||||
channel = args[0]
|
||||
self._channels[channel].users.add(source)
|
||||
self.users[source].channels.add(channel)
|
||||
@ -940,9 +911,9 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
"""
|
||||
Handles incoming KICKs.
|
||||
"""
|
||||
# <- :jlu5!~jlu5@127.0.0.1 KICK #whatever jlu5| :xd
|
||||
# <- :GL!~gl@127.0.0.1 KICK #whatever GL| :xd
|
||||
channel = args[0]
|
||||
target = self._get_UID(args[1], spawn_new=False)
|
||||
target = self.nick_to_uid(args[1])
|
||||
|
||||
try:
|
||||
reason = args[2]
|
||||
@ -967,21 +938,19 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Don't repeat hooks if we're the kicker, unless we're also the target.
|
||||
if self.is_internal_client(source) or self.is_internal_server(source):
|
||||
if self.pseudoclient and target != self.pseudoclient.uid:
|
||||
return
|
||||
if (not self.is_internal_client(source)) and not self.is_internal_server(source):
|
||||
# Don't repeat hooks if we're the kicker.
|
||||
return {'channel': channel, 'target': target, 'text': reason}
|
||||
|
||||
def handle_mode(self, source, command, args):
|
||||
"""Handles MODE changes."""
|
||||
# <- :jlu5!~jlu5@127.0.0.1 MODE #dev +v ice
|
||||
# <- :GL!~gl@127.0.0.1 MODE #dev +v ice
|
||||
# <- :ice MODE ice :+Zi
|
||||
target = args[0]
|
||||
if self.is_channel(target):
|
||||
oldobj = self._channels[target].deepcopy()
|
||||
else:
|
||||
target = self._get_UID(target, spawn_new=False)
|
||||
target = self.nick_to_uid(target)
|
||||
oldobj = None
|
||||
modes = args[1:]
|
||||
changedmodes = self.parse_modes(target, modes)
|
||||
@ -1000,8 +969,8 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
def handle_324(self, source, command, args):
|
||||
"""Handles MODE announcements via RPL_CHANNELMODEIS (i.e. the response to /mode #channel)"""
|
||||
# -> MODE #test
|
||||
# <- :midnight.vpn 324 jlu5 #test +nt
|
||||
# <- :midnight.vpn 329 jlu5 #test 1491773459
|
||||
# <- :midnight.vpn 324 GL #test +nt
|
||||
# <- :midnight.vpn 329 GL #test 1491773459
|
||||
channel = args[1]
|
||||
modes = args[2:]
|
||||
log.debug('(%s) Got RPL_CHANNELMODEIS (324) modes %s for %s', self.name, modes, channel)
|
||||
@ -1037,7 +1006,7 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
|
||||
def handle_nick(self, source, command, args):
|
||||
"""Handles NICK changes."""
|
||||
# <- :jlu5|!~jlu5@127.0.0.1 NICK :jlu5_
|
||||
# <- :GL|!~GL@127.0.0.1 NICK :GL_
|
||||
newnick = args[0]
|
||||
|
||||
if not self.connected.is_set():
|
||||
@ -1052,6 +1021,8 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
|
||||
oldnick = self.users[source].nick
|
||||
|
||||
# Check for any nick collisions with existing virtual clients.
|
||||
self._check_nick_collision(newnick)
|
||||
self.users[source].nick = newnick
|
||||
|
||||
return {'newnick': newnick, 'oldnick': oldnick}
|
||||
@ -1060,7 +1031,7 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
"""
|
||||
Handles incoming PARTs.
|
||||
"""
|
||||
# <- :jlu5|!~jlu5@127.0.0.1 PART #whatever
|
||||
# <- :GL|!~GL@127.0.0.1 PART #whatever
|
||||
channels = args[0].split(',')
|
||||
try:
|
||||
reason = args[1]
|
||||
@ -1105,7 +1076,7 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
|
||||
real_target = target.lstrip(''.join(self.prefixmodes.values()))
|
||||
if not self.is_channel(real_target):
|
||||
target = self._get_UID(target, spawn_new=False)
|
||||
target = self.nick_to_uid(target)
|
||||
|
||||
if target:
|
||||
return {'target': target, 'text': args[1]}
|
||||
@ -1116,13 +1087,9 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
if self.pseudoclient and source == self.pseudoclient.uid:
|
||||
# Someone faked a quit from us? We should abort.
|
||||
raise ProtocolError("Received QUIT from uplink (%s)" % args[0])
|
||||
elif source not in self.users:
|
||||
log.debug('(%s) Ignoring QUIT on non-existent user %s', self.name, source)
|
||||
return
|
||||
|
||||
userdata = self.users[source]
|
||||
self.quit(source, args[0])
|
||||
return {'text': args[0], 'userdata': userdata}
|
||||
return {'text': args[0]}
|
||||
|
||||
def handle_404(self, source, command, args):
|
||||
"""
|
||||
@ -1157,7 +1124,7 @@ class ClientbotWrapperProtocol(ClientbotBaseProtocol, IRCCommonProtocol):
|
||||
channel = args[1]
|
||||
target = args[2]
|
||||
if channel not in self.channels:
|
||||
log.warning('(%s) got ban mode +%s %s on unknown channel %s?', self.name, banmode, target, channel)
|
||||
log.warning('(%s) got ban mode +%s %s on unknown channel %s?', self.name, banmode, target)
|
||||
else:
|
||||
# Just apply the mode; we send out a mode hook only when the corresponding ban list has finished sending.
|
||||
self.apply_modes(channel, [('+%s' % banmode, target)])
|
||||
|
@ -4,13 +4,10 @@ hybrid.py: IRCD-Hybrid protocol module for PyLink.
|
||||
|
||||
import time
|
||||
|
||||
from pylinkirc import conf
|
||||
from pylinkirc.classes import *
|
||||
from pylinkirc import utils, conf
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.protocols.ts6 import TS6Protocol
|
||||
|
||||
__all__ = ['HybridProtocol']
|
||||
|
||||
from pylinkirc.classes import *
|
||||
from pylinkirc.protocols.ts6 import *
|
||||
|
||||
# This protocol module inherits from the TS6 protocol.
|
||||
class HybridProtocol(TS6Protocol):
|
||||
@ -131,8 +128,8 @@ class HybridProtocol(TS6Protocol):
|
||||
"""Updates the ident, host, or realname of a PyLink client."""
|
||||
# https://github.com/ircd-hybrid/ircd-hybrid/blob/58323b8/modules/m_svsmode.c#L40-L103
|
||||
# parv[0] = command
|
||||
# parv[1] = nickname <-- UID works too -jlu5
|
||||
# parv[2] = TS <-- Of the user, not the current time. -jlu5
|
||||
# parv[1] = nickname <-- UID works too -GLolol
|
||||
# parv[2] = TS <-- Of the user, not the current time. -GLolol
|
||||
# parv[3] = mode
|
||||
# parv[4] = optional argument (services account, vhost)
|
||||
field = field.upper()
|
||||
@ -142,17 +139,11 @@ class HybridProtocol(TS6Protocol):
|
||||
if field == 'HOST':
|
||||
self.users[target].host = text
|
||||
# On Hybrid, it appears that host changing is actually just forcing umode
|
||||
# "+x <hostname>" on the target. -jlu5
|
||||
# "+x <hostname>" on the target. -GLolol
|
||||
self._send_with_prefix(self.sid, 'SVSMODE %s %s +x %s' % (target, ts, text))
|
||||
else:
|
||||
raise NotImplementedError("Changing field %r of a client is unsupported by this protocol." % field)
|
||||
|
||||
def oper_notice(self, source, text):
|
||||
"""
|
||||
Send a message to all opers.
|
||||
"""
|
||||
self._send_with_prefix(source, 'GLOBOPS :%s' % text)
|
||||
|
||||
def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'):
|
||||
"""
|
||||
Sets a server ban.
|
||||
@ -217,14 +208,11 @@ class HybridProtocol(TS6Protocol):
|
||||
# Call the OPERED UP hook if +o is being added to the mode list.
|
||||
self._check_oper_status_change(uid, parsedmodes)
|
||||
|
||||
# Track SSL/TLS status
|
||||
has_ssl = self.users[uid].ssl = ('+S', None) in parsedmodes
|
||||
|
||||
# Set the account name if present
|
||||
if account:
|
||||
self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
||||
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realname': realname, 'host': host, 'ident': ident, 'ip': ip, 'secure': has_ssl}
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realname': realname, 'host': host, 'ident': ident, 'ip': ip}
|
||||
|
||||
def handle_tburst(self, numeric, command, args):
|
||||
"""Handles incoming topic burst (TBURST) commands."""
|
||||
@ -264,7 +252,7 @@ class HybridProtocol(TS6Protocol):
|
||||
# Login sequence (tested with Anope 2.0.4-git):
|
||||
# A mode change +d accountname is used to propagate logins,
|
||||
# before setting umode +r on the target.
|
||||
# <- :5ANAAAAAG SVSMODE 5HYAAAAAA 1460175209 +d jlu5
|
||||
# <- :5ANAAAAAG SVSMODE 5HYAAAAAA 1460175209 +d GL
|
||||
# <- :5ANAAAAAG SVSMODE 5HYAAAAAA 1460175209 +r
|
||||
|
||||
# Logout sequence:
|
||||
|
@ -1,32 +1,25 @@
|
||||
"""
|
||||
inspircd.py: InspIRCd 2.0, 3.x protocol module for PyLink.
|
||||
inspircd.py: InspIRCd 2.x protocol module for PyLink.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
import threading
|
||||
|
||||
from pylinkirc import conf
|
||||
from pylinkirc import utils, conf
|
||||
from pylinkirc.classes import *
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.protocols.ts6_common import TS6BaseProtocol
|
||||
|
||||
__all__ = ['InspIRCdProtocol']
|
||||
|
||||
from pylinkirc.protocols.ts6_common import *
|
||||
|
||||
class InspIRCdProtocol(TS6BaseProtocol):
|
||||
|
||||
S2S_BUFSIZE = 0 # InspIRCd allows infinitely long S2S messages, so set bufsize to infinite
|
||||
SUPPORTED_IRCDS = ['insp20', 'insp3']
|
||||
DEFAULT_IRCD = SUPPORTED_IRCDS[1]
|
||||
|
||||
MAX_PROTO_VER = 1205 # anything above this warns (not officially supported)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.protocol_caps |= {'slash-in-nicks', 'slash-in-hosts', 'underscore-in-hosts'}
|
||||
|
||||
# This is only the default value - on InspIRCd 3 it will be negotiated on connect in CAPAB CAPABILITIES
|
||||
# Set our case mapping (rfc1459 maps "\" and "|" together, for example).
|
||||
self.casemapping = 'rfc1459'
|
||||
|
||||
# Raw commands sent from servers vary from protocol to protocol. Here, we map
|
||||
@ -35,19 +28,11 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
self.hook_map = {'FJOIN': 'JOIN', 'RSQUIT': 'SQUIT', 'FMODE': 'MODE',
|
||||
'FTOPIC': 'TOPIC', 'OPERTYPE': 'MODE', 'FHOST': 'CHGHOST',
|
||||
'FIDENT': 'CHGIDENT', 'FNAME': 'CHGNAME', 'SVSTOPIC': 'TOPIC',
|
||||
'SAKICK': 'KICK', 'IJOIN': 'JOIN'}
|
||||
'SAKICK': 'KICK'}
|
||||
|
||||
ircd_target = self.serverdata.get('target_version', self.DEFAULT_IRCD).lower()
|
||||
if ircd_target == 'insp20':
|
||||
self.min_proto_ver = 1202
|
||||
self.proto_ver = 1202
|
||||
elif ircd_target == 'insp3':
|
||||
self.proto_ver = 1205
|
||||
else:
|
||||
raise ProtocolError("Unsupported target_version %r: supported values include %s" % (ircd_target, self.SUPPORTED_IRCDS))
|
||||
log.debug('(%s) inspircd: using protocol version %s for target_version %r', self.name, self.proto_ver, ircd_target)
|
||||
|
||||
# Track prefix mode levels on InspIRCd 3
|
||||
self._prefix_levels = {}
|
||||
self.max_proto_ver = 1202 # Anything above should warn (not officially supported)
|
||||
|
||||
# Track the modules supported by the uplink.
|
||||
self._modsupport = set()
|
||||
@ -202,8 +187,10 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
self.name, target)
|
||||
userobj.opertype = otype
|
||||
|
||||
# InspIRCd 2.x uses _ in OPERTYPE to denote spaces, while InspIRCd 3.x does not.
|
||||
# This is one of the few things not fixed by 2.0/3.0 link compat, so here's a workaround
|
||||
# InspIRCd 2.x uses _ in OPERTYPE to denote spaces, while InspIRCd 3.x does not. This is not
|
||||
# backwards compatible: spaces in InspIRCd 2.x will cause the oper type to get cut off at
|
||||
# the first word, while underscores in InspIRCd 3.x are shown literally as _.
|
||||
# We can do the underscore fixing based on the version of our uplink:
|
||||
if self.remote_proto_ver < 1205:
|
||||
otype = otype.replace(" ", "_")
|
||||
else:
|
||||
@ -252,29 +239,13 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
|
||||
self._remove_client(target)
|
||||
|
||||
def topic(self, source, target, text):
|
||||
"""Sends a topic change from a PyLink client."""
|
||||
if not self.is_internal_client(source):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
if self.proto_ver >= 1205:
|
||||
self._send_with_prefix(source, 'FTOPIC %s %s %s :%s' % (target, self._channels[target].ts, int(time.time()), text))
|
||||
else:
|
||||
return super().topic(source, target, text)
|
||||
|
||||
def topic_burst(self, source, target, text):
|
||||
def topic_burst(self, numeric, target, text):
|
||||
"""Sends a topic change from a PyLink server. This is usually used on burst."""
|
||||
if not self.is_internal_server(source):
|
||||
if not self.is_internal_server(numeric):
|
||||
raise LookupError('No such PyLink server exists.')
|
||||
|
||||
topic_ts = int(time.time())
|
||||
servername = self.servers[source].name
|
||||
|
||||
if self.proto_ver >= 1205:
|
||||
self._send_with_prefix(source, 'FTOPIC %s %s %s %s :%s' % (target, self._channels[target].ts, topic_ts, servername, text))
|
||||
else:
|
||||
self._send_with_prefix(source, 'FTOPIC %s %s %s :%s' % (target, topic_ts, servername, text))
|
||||
|
||||
ts = int(time.time())
|
||||
servername = self.servers[numeric].name
|
||||
self._send_with_prefix(numeric, 'FTOPIC %s %s %s :%s' % (target, ts, servername, text))
|
||||
self._channels[target].topic = text
|
||||
self._channels[target].topicset = True
|
||||
|
||||
@ -334,12 +305,6 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
|
||||
self.call_hooks([self.sid, 'CHGNAME',
|
||||
{'target': target, 'newgecos': text}])
|
||||
def oper_notice(self, source, text):
|
||||
"""
|
||||
Send a message to all opers.
|
||||
"""
|
||||
# <- :70M SNONOTICE G :From jlu5: aaaaaa
|
||||
self._send_with_prefix(self.sid, 'SNONOTICE G :From %s: %s' % (self.get_friendly_name(source), text))
|
||||
|
||||
def numeric(self, source, numeric, target, text):
|
||||
"""Sends raw numerics from a server to a remote client."""
|
||||
@ -348,25 +313,12 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
# given user.
|
||||
# <- :70M PUSH 0ALAAAAAA ::midnight.vpn 422 PyLink-devel :Message of the day file is missing.
|
||||
|
||||
# InspIRCd 3 uses a new NUM command in this format:
|
||||
# -> NUM <numeric source sid> <target uuid> <numeric ID> <params>
|
||||
if self.proto_ver >= 1205:
|
||||
self._send('NUM %s %s %s %s' % (source, target, numeric, text))
|
||||
else:
|
||||
# Note: InspIRCd 2.2 uses a new NUM command in this format:
|
||||
# :<sid> NUM <numeric source sid> <target uuid> <3 digit number> <params>
|
||||
# Take this into consideration if we ever target InspIRCd 2.2, even though m_spanningtree
|
||||
# does provide backwards compatibility for commands like this. -GLolol
|
||||
self._send_with_prefix(self.sid, 'PUSH %s ::%s %s %s %s' % (target, source, numeric, target, text))
|
||||
|
||||
def invite(self, source, target, channel):
|
||||
"""Sends an INVITE from a PyLink client."""
|
||||
if not self.is_internal_client(source):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
if self.proto_ver >= 1205: # insp3
|
||||
# Note: insp3 supports optionally sending an invite expiration (after the TS argument),
|
||||
# but we don't use / expose that feature yet.
|
||||
self._send_with_prefix(source, 'INVITE %s %s %d' % (target, channel, self._channels[channel].ts))
|
||||
else: # insp2
|
||||
self._send_with_prefix(source, 'INVITE %s %s' % (target, channel))
|
||||
|
||||
def away(self, source, text):
|
||||
"""Sends an AWAY message from a PyLink client. <text> can be an empty string
|
||||
to unset AWAY status."""
|
||||
@ -412,11 +364,6 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
raise ValueError('Invalid server name %r' % name)
|
||||
|
||||
self.servers[sid] = Server(self, uplink, name, internal=True, desc=desc)
|
||||
if self.proto_ver >= 1205:
|
||||
# <- :3IN SERVER services.abc.local 0SV :Some server
|
||||
self._send_with_prefix(uplink, 'SERVER %s %s :%s' % (name, sid, desc))
|
||||
else:
|
||||
# <- :00A SERVER test.server * 1 00C :test
|
||||
self._send_with_prefix(uplink, 'SERVER %s * %s %s :%s' % (name, self.servers[sid].hopcount, sid, desc))
|
||||
|
||||
# Endburst delay clutter
|
||||
@ -472,14 +419,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
sdesc=self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']))
|
||||
|
||||
self._send_with_prefix(self.sid, 'BURST %s' % ts)
|
||||
|
||||
# InspIRCd sends VERSION data on link, instead of when requested by a client.
|
||||
if self.proto_ver >= 1205:
|
||||
verstr = self.version()
|
||||
for version_type in {'version', 'rawversion'}:
|
||||
self._send_with_prefix(self.sid, 'SINFO %s :%s' % (version_type, verstr.split(' ', 1)[0]))
|
||||
self._send_with_prefix(self.sid, 'SINFO fullversion :%s' % verstr)
|
||||
else:
|
||||
# InspIRCd sends VERSION data on link, instead of whenever requested by a client.
|
||||
self._send_with_prefix(self.sid, 'VERSION :%s' % self.version())
|
||||
self._send_with_prefix(self.sid, 'ENDBURST')
|
||||
|
||||
@ -499,42 +439,12 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
Handles the CAPAB command, used for capability negotiation with our
|
||||
uplink.
|
||||
"""
|
||||
# 5 CAPAB subcommands are usually sent on connect (excluding START and END):
|
||||
# CAPAB MODULES, MODSUPPORT, CHANMODES, USERMODES, and CAPABILITIES
|
||||
# We check just about everything except MODULES
|
||||
# 6 CAPAB commands are usually sent on connect: CAPAB START, MODULES,
|
||||
# MODSUPPORT, CHANMODES, USERMODES, and CAPABILITIES.
|
||||
# The only ones of interest to us are CHANMODES, USERMODES,
|
||||
# CAPABILITIES, and MODSUPPORT.
|
||||
|
||||
if args[0] == 'START':
|
||||
# Check the protocol version
|
||||
# insp20:
|
||||
# <- CAPAB START 1202
|
||||
# insp3:
|
||||
# <- CAPAB START 1205
|
||||
self.remote_proto_ver = protocol_version = int(args[1])
|
||||
|
||||
log.debug("(%s) handle_capab: got remote protocol version %s", self.name, protocol_version)
|
||||
if protocol_version < self.proto_ver:
|
||||
raise ProtocolError("Remote protocol version is too old! "
|
||||
"At least %s is needed. (got %s)" %
|
||||
(self.proto_ver, protocol_version))
|
||||
elif protocol_version > self.MAX_PROTO_VER:
|
||||
log.warning("(%s) PyLink support for InspIRCd > 3.x is experimental, "
|
||||
"and should not be relied upon for anything important.",
|
||||
self.name)
|
||||
elif protocol_version >= 1205 > self.proto_ver:
|
||||
log.warning("(%s) PyLink 3.0 introduces native support for InspIRCd 3. "
|
||||
"You should enable this by setting the 'target_version' option in your "
|
||||
"InspIRCd server block to 'insp3'. Otherwise, some features will not "
|
||||
"work correctly!", self.name)
|
||||
log.warning("(%s) Falling back to InspIRCd 2.0 (compatibility) mode.", self.name)
|
||||
|
||||
if self.proto_ver >= 1205:
|
||||
# Clear mode lists, they will be negotiated during burst
|
||||
self.cmodes = {'*A': '', '*B': '', '*C': '', '*D': ''}
|
||||
self.umodes = {'*A': '', '*B': '', '*C': '', '*D': ''}
|
||||
self.prefixmodes.clear()
|
||||
|
||||
if args[0] in {'CHANMODES', 'USERMODES'}:
|
||||
# insp20:
|
||||
if args[0] == 'CHANMODES':
|
||||
# <- CAPAB CHANMODES :admin=&a allowinvite=A autoop=w ban=b
|
||||
# banexception=e blockcolor=c c_registered=r exemptchanops=X
|
||||
# filter=g flood=f halfop=%h history=H invex=I inviteonly=i
|
||||
@ -543,73 +453,23 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
# official-join=!Y op=@o operonly=O opmoderated=U owner=~q
|
||||
# permanent=P private=p redirect=L reginvite=R regmoderated=M
|
||||
# secret=s sslonly=z stripcolor=S topiclock=t voice=+v
|
||||
# <- CAPAB USERMODES :bot=B callerid=g cloak=x deaf_commonchan=c
|
||||
# helpop=h hidechans=I hideoper=H invisible=i oper=o regdeaf=R
|
||||
# servprotect=k showwhois=W snomask=s u_registered=r u_stripcolor=S
|
||||
# wallops=w
|
||||
|
||||
# insp3:
|
||||
# <- CAPAB CHANMODES :list:autoop=w list:ban=b list:banexception=e list:filter=g list:invex=I
|
||||
# list:namebase=Z param-set:anticaps=B param-set:flood=f param-set:joinflood=j param-set:kicknorejoin=J
|
||||
# param-set:limit=l param-set:nickflood=F param-set:redirect=L param:key=k prefix:10000:voice=+v
|
||||
# prefix:20000:halfop=%h prefix:30000:op=@o prefix:40000:admin=&a prefix:50000:founder=~q
|
||||
# prefix:9000000:official-join=!Y simple:allowinvite=A simple:auditorium=u simple:blockcolor=c
|
||||
# simple:c_registered=r simple:censor=G simple:inviteonly=i simple:moderated=m simple:noctcp=C
|
||||
# simple:noextmsg=n simple:nokick=Q simple:noknock=K simple:nonick=N simple:nonotice=T
|
||||
# simple:operonly=O simple:permanent=P simple:private=p simple:reginvite=R simple:regmoderated=M
|
||||
# simple:secret=s simple:sslonly=z simple:stripcolor=S simple:topiclock=t
|
||||
# <- CAPAB USERMODES :param-set:snomask=s simple:antiredirect=L simple:bot=B simple:callerid=g simple:cloak=x
|
||||
# simple:deaf_commonchan=c simple:helpop=h simple:hidechans=I simple:hideoper=H simple:invisible=i
|
||||
# simple:oper=o simple:regdeaf=R simple:u_censor=G simple:u_registered=r simple:u_stripcolor=S
|
||||
# simple:wallops=w
|
||||
|
||||
mydict = self.cmodes if args[0] == 'CHANMODES' else self.umodes
|
||||
|
||||
# Named modes are essential for a cross-protocol IRC service. We
|
||||
# can use InspIRCd as a model here and assign a similar mode map to
|
||||
# our cmodes list.
|
||||
for modepair in args[-1].split():
|
||||
name, char = modepair.rsplit('=', 1)
|
||||
name, char = modepair.split('=')
|
||||
|
||||
if self.proto_ver >= 1205:
|
||||
# Detect mode types from the mode type tag
|
||||
parts = name.split(':')
|
||||
modetype = parts[0]
|
||||
name = parts[-1]
|
||||
|
||||
# Modes are divided into A, B, C, and D classes
|
||||
# See http://www.irc.org/tech_docs/005.html
|
||||
if modetype == 'simple': # No parameter
|
||||
mydict['*D'] += char
|
||||
elif modetype == 'param-set': # Only parameter when setting (e.g. cmode +l)
|
||||
mydict['*C'] += char
|
||||
elif modetype == 'param': # Always has parameter (e.g. cmode +k)
|
||||
mydict['*B'] += char
|
||||
elif modetype == 'list': # List modes like ban, except, invex, ...
|
||||
mydict['*A'] += char
|
||||
elif modetype == 'prefix': # prefix:30000:op=@o
|
||||
if args[0] != 'CHANMODES': # This should never happen...
|
||||
log.warning("(%s) Possible desync? Got a prefix type modepair %r but not for channel modes", self.name, modepair)
|
||||
else:
|
||||
# We don't do anything with prefix levels yet, let's just store them for future use
|
||||
self._prefix_levels[name] = int(parts[1])
|
||||
|
||||
# Map mode names to their prefixes
|
||||
self.prefixmodes[char[-1]] = char[0]
|
||||
|
||||
# Strip c_, u_ prefixes to be consistent with other protocols.
|
||||
if name.startswith(('c_', 'u_')):
|
||||
name = name[2:]
|
||||
# Strip c_ prefixes to be consistent with other protocols.
|
||||
name = name.lstrip('c_')
|
||||
|
||||
if name == 'reginvite': # Reginvite? That's an odd name.
|
||||
name = 'regonly'
|
||||
|
||||
if name == 'antiredirect': # User mode +L
|
||||
name = 'noforward'
|
||||
|
||||
if name == 'founder': # Channel mode +q
|
||||
# Founder, owner; same thing. m_customprefix allows you to name it anything you like,
|
||||
# but PyLink uses the latter in its definitions
|
||||
# Founder, owner; same thing. m_customprefix allows you to
|
||||
# name it anything you like. The former is config default,
|
||||
# but I personally prefer the latter.
|
||||
name = 'owner'
|
||||
|
||||
if name in ('repeat', 'kicknorejoin'):
|
||||
@ -617,84 +477,82 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
# be safely relayed.
|
||||
name += '_insp'
|
||||
|
||||
# Add the mode char to our table
|
||||
mydict[name] = char[-1]
|
||||
# We don't care about mode prefixes; just the mode char.
|
||||
self.cmodes[name] = char[-1]
|
||||
|
||||
|
||||
elif args[0] == 'USERMODES':
|
||||
# <- CAPAB USERMODES :bot=B callerid=g cloak=x deaf_commonchan=c
|
||||
# helpop=h hidechans=I hideoper=H invisible=i oper=o regdeaf=R
|
||||
# servprotect=k showwhois=W snomask=s u_registered=r u_stripcolor=S
|
||||
# wallops=w
|
||||
|
||||
# Ditto above.
|
||||
for modepair in args[-1].split():
|
||||
name, char = modepair.split('=')
|
||||
# Strip u_ prefixes to be consistent with other protocols.
|
||||
name = name.lstrip('u_')
|
||||
self.umodes[name] = char
|
||||
|
||||
elif args[0] == 'CAPABILITIES':
|
||||
# insp20:
|
||||
# <- CAPAB CAPABILITIES :NICKMAX=21 CHANMAX=64 MAXMODES=20
|
||||
# IDENTMAX=11 MAXQUIT=255 MAXTOPIC=307 MAXKICK=255 MAXGECOS=128
|
||||
# MAXAWAY=200 IP6SUPPORT=1 PROTOCOL=1202 PREFIX=(Yqaohv)!~&@%+
|
||||
# CHANMODES=IXbegw,k,FHJLfjl,ACKMNOPQRSTUcimnprstz
|
||||
# USERMODES=,,s,BHIRSWcghikorwx GLOBOPS=1 SVSPART=1
|
||||
|
||||
# insp3:
|
||||
# CAPAB CAPABILITIES :NICKMAX=30 CHANMAX=64 MAXMODES=20 IDENTMAX=10 MAXQUIT=255 MAXTOPIC=307
|
||||
# MAXKICK=255 MAXREAL=128 MAXAWAY=200 MAXHOST=64 CHALLENGE=xxxxxxxxx CASEMAPPING=ascii GLOBOPS=1
|
||||
|
||||
# First, turn the arguments into a dict
|
||||
caps = self.parse_isupport(args[-1])
|
||||
log.debug("(%s) handle_capab: capabilities list is %s", self.name, caps)
|
||||
log.debug("(%s) capabilities list: %s", self.name, caps)
|
||||
|
||||
# Check the protocol version
|
||||
self.remote_proto_ver = protocol_version = int(caps['PROTOCOL'])
|
||||
|
||||
if protocol_version < self.min_proto_ver:
|
||||
raise ProtocolError("Remote protocol version is too old! "
|
||||
"At least %s (InspIRCd 2.0.x) is "
|
||||
"needed. (got %s)" % (self.min_proto_ver,
|
||||
protocol_version))
|
||||
elif protocol_version > self.max_proto_ver:
|
||||
log.warning("(%s) PyLink support for InspIRCd 2.2+ is experimental, "
|
||||
"and should not be relied upon for anything important.",
|
||||
self.name)
|
||||
|
||||
# Store the max nick and channel lengths
|
||||
if 'NICKMAX' in caps:
|
||||
self.maxnicklen = int(caps['NICKMAX'])
|
||||
if 'CHANMAX' in caps:
|
||||
self.maxchanlen = int(caps['CHANMAX'])
|
||||
# Casemapping - this is only sent in InspIRCd 3.x
|
||||
if 'CASEMAPPING' in caps:
|
||||
self.casemapping = caps['CASEMAPPING']
|
||||
log.debug('(%s) handle_capab: updated casemapping to %s', self.name, self.casemapping)
|
||||
|
||||
# InspIRCd 2 only: mode & prefix definitions are sent as CAPAB CAPABILITIES CHANMODES/USERMODES/PREFIX
|
||||
if self.proto_ver < 1205:
|
||||
if 'CHANMODES' in caps:
|
||||
# Modes are divided into A, B, C, and D classes
|
||||
# See http://www.irc.org/tech_docs/005.html
|
||||
|
||||
# FIXME: Find a neater way to assign/store this.
|
||||
self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D'] \
|
||||
= caps['CHANMODES'].split(',')
|
||||
if 'USERMODES' in caps:
|
||||
self.umodes['*A'], self.umodes['*B'], self.umodes['*C'], self.umodes['*D'] \
|
||||
= caps['USERMODES'].split(',')
|
||||
if 'PREFIX' in caps:
|
||||
|
||||
# Separate the prefixes field (e.g. "(Yqaohv)!~&@%+") into a
|
||||
# dict mapping mode characters to mode prefixes.
|
||||
self.prefixmodes = self.parse_isupport_prefixes(caps['PREFIX'])
|
||||
log.debug('(%s) handle_capab: self.prefixmodes set to %r', self.name,
|
||||
log.debug('(%s) self.prefixmodes set to %r', self.name,
|
||||
self.prefixmodes)
|
||||
|
||||
elif args[0] == 'MODSUPPORT':
|
||||
# <- CAPAB MODSUPPORT :m_alltime.so m_check.so m_chghost.so m_chgident.so m_chgname.so m_fullversion.so m_gecosban.so m_knock.so m_muteban.so m_nicklock.so m_nopartmsg.so m_opmoderated.so m_sajoin.so m_sanick.so m_sapart.so m_serverban.so m_services_account.so m_showwhois.so m_silence.so m_swhois.so m_uninvite.so m_watch.so
|
||||
self._modsupport |= set(args[-1].split())
|
||||
|
||||
def handle_kick(self, source, command, args):
|
||||
"""Handles incoming KICKs."""
|
||||
# InspIRCD 3 adds membership IDs to KICK messages when forwarding across servers
|
||||
# <- :3INAAAAAA KICK #endlessvoid 3INAAAAAA :test (local)
|
||||
# <- :3INAAAAAA KICK #endlessvoid 7PYAAAAAA 0 :test (remote)
|
||||
if self.proto_ver >= 1205 and len(args) > 3:
|
||||
del args[2]
|
||||
|
||||
return super().handle_kick(source, command, args)
|
||||
|
||||
def handle_ping(self, source, command, args):
|
||||
"""Handles incoming PING commands, so we don't time out."""
|
||||
# InspIRCd 2:
|
||||
# <- :70M PING 70M 0AL
|
||||
# -> :0AL PONG 0AL 70M
|
||||
|
||||
# InspIRCd 3:
|
||||
# <- :3IN PING 808
|
||||
# -> :808 PONG 3IN
|
||||
if len(args) >= 2:
|
||||
self._send_with_prefix(args[1], 'PONG %s %s' % (args[1], source), queue=False)
|
||||
else:
|
||||
self._send_with_prefix(args[0], 'PONG %s' % source, queue=False)
|
||||
self._send_with_prefix(self.sid, 'PONG %s' % source, queue=False)
|
||||
|
||||
def handle_fjoin(self, servernumeric, command, args):
|
||||
"""Handles incoming FJOIN commands (InspIRCd equivalent of JOIN/SJOIN)."""
|
||||
# insp2:
|
||||
# <- :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :o,1SRAABIT4 v,1IOAAF53R <...>
|
||||
# insp3:
|
||||
# <- :3IN FJOIN #test 1556842195 +nt :o,3INAAAAAA:4
|
||||
# :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :o,1SRAABIT4 v,1IOAAF53R <...>
|
||||
channel = args[0]
|
||||
chandata = self._channels[channel].deepcopy()
|
||||
# InspIRCd sends each channel's users in the form of 'modeprefix(es),UID'
|
||||
@ -710,10 +568,6 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
for user in userlist:
|
||||
modeprefix, user = user.split(',', 1)
|
||||
|
||||
if self.proto_ver >= 1205:
|
||||
# XXX: we don't handle membership IDs yet
|
||||
user = user.split(':', 1)[0]
|
||||
|
||||
# Don't crash when we get an invalid UID.
|
||||
if user not in self.users:
|
||||
log.debug('(%s) handle_fjoin: tried to introduce user %s not in our user list, ignoring...',
|
||||
@ -741,34 +595,9 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts,
|
||||
'channeldata': chandata}
|
||||
|
||||
def handle_ijoin(self, source, command, args):
|
||||
"""Handles InspIRCd 3 joins with membership ID."""
|
||||
# insp3:
|
||||
# EX: regular /join on an existing channel
|
||||
# <- :3INAAAAAA IJOIN #valhalla 6
|
||||
|
||||
# EX: /ojoin on an existing channel
|
||||
# <- :3INAAAAAA IJOIN #valhalla 7 1559348434 Yo
|
||||
|
||||
# From insp3 source:
|
||||
# <- :<uid> IJOIN <chan> <membid> [<ts> [<flags>]]
|
||||
# args idx: 0 1 2 3
|
||||
|
||||
# For now we don't care about the membership ID
|
||||
channel = args[0]
|
||||
self.users[source].channels.add(channel)
|
||||
self._channels[channel].users.add(source)
|
||||
|
||||
# Apply prefix modes if they exist and the TS check passes
|
||||
if len(args) >= 4 and int(args[2]) <= self._channels[channel].ts:
|
||||
self.apply_modes(source, {('+%s' % mode, source) for mode in args[3]})
|
||||
|
||||
return {'channel': channel, 'users': [source], 'modes':
|
||||
self._channels[channel].modes}
|
||||
|
||||
def handle_uid(self, numeric, command, args):
|
||||
"""Handles incoming UID commands (user introduction)."""
|
||||
# :70M UID 70MAAAAAB 1429934638 jlu5 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP jlu5 0::1 1429934638 +Wioswx +ACGKNOQXacfgklnoqvx :realname
|
||||
# :70M UID 70MAAAAAB 1429934638 GL 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 +Wioswx +ACGKNOQXacfgklnoqvx :realname
|
||||
uid, ts, nick, realhost, host, ident, ip = args[0:7]
|
||||
|
||||
ts = int(ts)
|
||||
@ -783,41 +612,34 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
self._check_oper_status_change(uid, parsedmodes)
|
||||
|
||||
self.servers[numeric].users.add(uid)
|
||||
# InspIRCd sends SSL status in the metadata command, so the info is not known at this point
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip, 'secure': None}
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
|
||||
|
||||
def handle_server(self, source, command, args):
|
||||
def handle_server(self, numeric, command, args):
|
||||
"""Handles incoming SERVER commands (introduction of servers)."""
|
||||
|
||||
# Initial SERVER command on connect.
|
||||
if self.uplink is None:
|
||||
# <- SERVER whatever.net abcdefgh 0 10X :some server description
|
||||
servername = args[0].lower()
|
||||
source = args[3]
|
||||
numeric = args[3]
|
||||
|
||||
if args[1] != self.serverdata['recvpass']:
|
||||
# Check if recvpass is correct
|
||||
raise ProtocolError('recvpass from uplink server %s does not match configuration!' % servername)
|
||||
|
||||
sdesc = args[-1]
|
||||
self.servers[source] = Server(self, None, servername, desc=sdesc)
|
||||
self.uplink = source
|
||||
log.debug('(%s) inspircd: found uplink %s', self.name, self.uplink)
|
||||
self.servers[numeric] = Server(self, None, servername, desc=sdesc)
|
||||
self.uplink = numeric
|
||||
return
|
||||
|
||||
# Other server introductions.
|
||||
# insp20:
|
||||
# <- :00A SERVER test.server * 1 00C :testing raw message syntax
|
||||
# insp3:
|
||||
# <- :3IN SERVER services.abc.local 0SV :Some server
|
||||
servername = args[0].lower()
|
||||
if self.proto_ver >= 1205:
|
||||
sid = args[1] # insp3
|
||||
else:
|
||||
sid = args[3] # insp20
|
||||
sid = args[3]
|
||||
sdesc = args[-1]
|
||||
self.servers[sid] = Server(self, source, servername, desc=sdesc)
|
||||
self.servers[sid] = Server(self, numeric, servername, desc=sdesc)
|
||||
|
||||
return {'name': servername, 'sid': sid, 'text': sdesc}
|
||||
return {'name': servername, 'sid': args[3], 'text': sdesc}
|
||||
|
||||
def handle_fmode(self, numeric, command, args):
|
||||
"""Handles the FMODE command, used for channel mode changes."""
|
||||
@ -850,36 +672,18 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
# First arg = source, second = signon time, third = idle time
|
||||
self._send_with_prefix(target, 'IDLE %s %s 0' % (source, start_time))
|
||||
|
||||
def handle_ftopic(self, source, command, args):
|
||||
"""Handles incoming topic changes."""
|
||||
# insp2 (only used for server senders):
|
||||
# <- :70M FTOPIC #channel 1434510754 jlu5!jlu5@escape.the.dreamland.ca :Some channel topic
|
||||
|
||||
# insp3 (used for server AND user senders):
|
||||
# <- :3IN FTOPIC #qwerty 1556828864 1556844505 jlu5!jlu5@midnight-umk.of4.0.127.IP :1234abcd
|
||||
# <- :3INAAAAAA FTOPIC #qwerty 1556828864 1556844248 :topic text
|
||||
# chan creation time ^ ^ topic set time (the one we want)
|
||||
|
||||
# <- :00A SVSTOPIC #channel 1538402416 SomeUser :test
|
||||
def handle_ftopic(self, numeric, command, args):
|
||||
"""Handles incoming FTOPIC (sets topic on burst)."""
|
||||
# <- :70M FTOPIC #channel 1434510754 GLo|o|!GLolol@escape.the.dreamland.ca :Some channel topic
|
||||
channel = args[0]
|
||||
|
||||
if self.proto_ver >= 1205 and command == 'FTOPIC':
|
||||
ts = args[2]
|
||||
if source in self.users:
|
||||
setter = source
|
||||
else:
|
||||
setter = args[3]
|
||||
else:
|
||||
ts = args[1]
|
||||
setter = args[2]
|
||||
ts = int(ts)
|
||||
|
||||
topic = args[-1]
|
||||
self._channels[channel].topic = topic
|
||||
self._channels[channel].topicset = True
|
||||
return {'channel': channel, 'setter': setter, 'ts': ts, 'text': topic}
|
||||
|
||||
# SVSTOPIC is used by InspIRCd module m_topiclock - its arguments are the same as insp2 FTOPIC
|
||||
# SVSTOPIC is used by InspIRCd module m_topiclock - its arguments are the same as FTOPIC
|
||||
handle_svstopic = handle_ftopic
|
||||
|
||||
def handle_opertype(self, target, command, args):
|
||||
@ -969,7 +773,8 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
|
||||
def handle_metadata(self, numeric, command, args):
|
||||
"""
|
||||
Handles the METADATA command, used by servers to send metadata for various objects.
|
||||
Handles the METADATA command, used by servers to send metadata (services
|
||||
login name, certfp data, etc.) for clients.
|
||||
"""
|
||||
uid = args[0]
|
||||
|
||||
@ -977,8 +782,8 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
# <- :00A METADATA 1MLAAAJET accountname :
|
||||
# <- :00A METADATA 1MLAAAJET accountname :tester
|
||||
# Sets the services login name of the client.
|
||||
self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': args[-1]}])
|
||||
|
||||
self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': args[-1]}])
|
||||
elif args[1] == 'modules' and numeric == self.uplink:
|
||||
# Note: only handle METADATA from our uplink; otherwise leaf servers unloading modules
|
||||
# while shutting down will corrupt the state.
|
||||
@ -993,8 +798,6 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
||||
self._modsupport.add(module[1:])
|
||||
else:
|
||||
log.warning('(%s) Got unknown METADATA modules string: %r', self.name, args[-1])
|
||||
elif args[1] == 'ssl_cert' and uid in self.users:
|
||||
self.users[uid].ssl = True
|
||||
|
||||
def handle_version(self, numeric, command, args):
|
||||
"""
|
||||
|
@ -2,41 +2,54 @@
|
||||
ircs2s_common.py: Common base protocol class with functions shared by TS6 and P10-based protocols.
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
from pylinkirc import conf
|
||||
from pylinkirc.classes import IRCNetwork, ProtocolError
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc import utils, conf
|
||||
|
||||
__all__ = ['UIDGenerator', 'IRCCommonProtocol', 'IRCS2SProtocol']
|
||||
|
||||
class UIDGenerator():
|
||||
class IncrementalUIDGenerator():
|
||||
"""
|
||||
Generate UIDs for IRC S2S.
|
||||
Incremental UID Generator module, adapted from InspIRCd source:
|
||||
https://github.com/inspircd/inspircd/blob/f449c6b296ab/src/server.cpp#L85-L156
|
||||
"""
|
||||
|
||||
def __init__(self, uidchars, length, sid):
|
||||
self.uidchars = uidchars # corpus of characters to choose from
|
||||
self.length = length # desired length of uid part, padded with uidchars[0]
|
||||
self.sid = str(sid) # server id (prefixed to every result)
|
||||
self.counter = 0
|
||||
def __init__(self, sid):
|
||||
if not (hasattr(self, 'allowedchars') and hasattr(self, 'length')):
|
||||
raise RuntimeError("Allowed characters list not defined. Subclass "
|
||||
"%s by defining self.allowedchars and self.length "
|
||||
"and then calling super().__init__()." % self.__class__.__name__)
|
||||
self.uidchars = [self.allowedchars[0]]*self.length
|
||||
self.sid = str(sid)
|
||||
|
||||
def increment(self, pos=None):
|
||||
"""
|
||||
Increments the UID generator to the next available UID.
|
||||
"""
|
||||
# Position starts at 1 less than the UID length.
|
||||
if pos is None:
|
||||
pos = self.length - 1
|
||||
|
||||
# If we're at the last character in the list of allowed ones, reset
|
||||
# and increment the next level above.
|
||||
if self.uidchars[pos] == self.allowedchars[-1]:
|
||||
self.uidchars[pos] = self.allowedchars[0]
|
||||
self.increment(pos-1)
|
||||
else:
|
||||
# Find what position in the allowed characters list we're currently
|
||||
# on, and add one.
|
||||
idx = self.allowedchars.find(self.uidchars[pos])
|
||||
self.uidchars[pos] = self.allowedchars[idx+1]
|
||||
|
||||
def next_uid(self):
|
||||
"""
|
||||
Returns the next unused UID for the server.
|
||||
"""
|
||||
uid = ''
|
||||
num = self.counter
|
||||
if num >= (len(self.uidchars) ** self.length):
|
||||
raise RuntimeError("UID overflowed")
|
||||
while num > 0:
|
||||
num, index = divmod(num, len(self.uidchars))
|
||||
uid = self.uidchars[index] + uid
|
||||
|
||||
self.counter += 1
|
||||
uid = uid.rjust(self.length, self.uidchars[0])
|
||||
return self.sid + uid
|
||||
uid = self.sid + ''.join(self.uidchars)
|
||||
self.increment()
|
||||
return uid
|
||||
|
||||
class IRCCommonProtocol(IRCNetwork):
|
||||
|
||||
@ -47,7 +60,6 @@ class IRCCommonProtocol(IRCNetwork):
|
||||
|
||||
self._caps = {}
|
||||
self._use_builtin_005_handling = False # Disabled by default for greater security
|
||||
self.protocol_caps |= {'has-irc-modes', 'can-manage-bot-channels'}
|
||||
|
||||
def post_connect(self):
|
||||
self._caps.clear()
|
||||
@ -65,6 +77,30 @@ class IRCCommonProtocol(IRCNetwork):
|
||||
"Invalid port %r for network %s"
|
||||
% (port, self.name))
|
||||
|
||||
# TODO: these wrappers really need to be standardized
|
||||
def _get_SID(self, sname):
|
||||
"""Returns the SID of a server with the given name, if present."""
|
||||
name = sname.lower()
|
||||
|
||||
if name in self.servers:
|
||||
return name
|
||||
|
||||
for k, v in self.servers.items():
|
||||
if v.name.lower() == name:
|
||||
return k
|
||||
else:
|
||||
return sname # Fall back to given text instead of None
|
||||
|
||||
def _get_UID(self, target):
|
||||
"""Converts a nick argument to its matching UID. This differs from irc.nick_to_uid()
|
||||
in that it returns the original text instead of None, if no matching nick is found."""
|
||||
|
||||
if target in self.users:
|
||||
return target
|
||||
|
||||
target = self.nick_to_uid(target) or target
|
||||
return target
|
||||
|
||||
@staticmethod
|
||||
def parse_args(args):
|
||||
"""
|
||||
@ -82,7 +118,6 @@ class IRCCommonProtocol(IRCNetwork):
|
||||
joined_arg = ' '.join(args[idx:])[1:] # Cut off the leading : as well
|
||||
real_args.append(joined_arg)
|
||||
break
|
||||
elif arg.strip(' '): # Skip empty args that aren't part of the multi-word arg
|
||||
real_args.append(arg)
|
||||
|
||||
return real_args
|
||||
@ -95,6 +130,63 @@ class IRCCommonProtocol(IRCNetwork):
|
||||
args[0] = args[0].split(':', 1)[1]
|
||||
return args
|
||||
|
||||
def _squit(self, numeric, command, args):
|
||||
"""Handles incoming SQUITs."""
|
||||
|
||||
split_server = self._get_SID(args[0])
|
||||
|
||||
# Normally we'd only need to check for our SID as the SQUIT target, but Nefarious
|
||||
# actually uses the uplink server as the SQUIT target.
|
||||
# <- ABAAE SQ nefarious.midnight.vpn 0 :test
|
||||
if split_server in (self.sid, self.uplink):
|
||||
raise ProtocolError('SQUIT received: (reason: %s)' % args[-1])
|
||||
|
||||
affected_users = []
|
||||
affected_nicks = defaultdict(list)
|
||||
log.debug('(%s) Splitting server %s (reason: %s)', self.name, split_server, args[-1])
|
||||
|
||||
if split_server not in self.servers:
|
||||
log.warning("(%s) Tried to split a server (%s) that didn't exist!", self.name, split_server)
|
||||
return
|
||||
|
||||
# Prevent RuntimeError: dictionary changed size during iteration
|
||||
old_servers = self.servers.copy()
|
||||
old_channels = self._channels.copy()
|
||||
|
||||
# Cycle through our list of servers. If any server's uplink is the one that is being SQUIT,
|
||||
# remove them and all their users too.
|
||||
for sid, data in old_servers.items():
|
||||
if data.uplink == split_server:
|
||||
log.debug('Server %s also hosts server %s, removing those users too...', split_server, sid)
|
||||
# Recursively run SQUIT on any other hubs this server may have been connected to.
|
||||
args = self._squit(sid, 'SQUIT', [sid, "0",
|
||||
"PyLink: Automatically splitting leaf servers of %s" % sid])
|
||||
affected_users += args['users']
|
||||
|
||||
for user in self.servers[split_server].users.copy():
|
||||
affected_users.append(user)
|
||||
nick = self.users[user].nick
|
||||
|
||||
# Nicks affected is channel specific for SQUIT:. This makes Clientbot's SQUIT relaying
|
||||
# much easier to implement.
|
||||
for name, cdata in old_channels.items():
|
||||
if user in cdata.users:
|
||||
affected_nicks[name].append(nick)
|
||||
|
||||
log.debug('Removing client %s (%s)', user, nick)
|
||||
self._remove_client(user)
|
||||
|
||||
serverdata = self.servers[split_server]
|
||||
sname = serverdata.name
|
||||
uplink = serverdata.uplink
|
||||
|
||||
del self.servers[split_server]
|
||||
log.debug('(%s) Netsplit affected users: %s', self.name, affected_users)
|
||||
|
||||
return {'target': split_server, 'users': affected_users, 'name': sname,
|
||||
'uplink': uplink, 'nicks': affected_nicks, 'serverdata': serverdata,
|
||||
'channeldata': old_channels}
|
||||
|
||||
@staticmethod
|
||||
def parse_isupport(args, fallback=''):
|
||||
"""
|
||||
@ -125,33 +217,6 @@ class IRCCommonProtocol(IRCNetwork):
|
||||
prefixsearch = re.search(r'\(([A-Za-z]+)\)(.*)', args)
|
||||
return dict(zip(prefixsearch.group(1), prefixsearch.group(2)))
|
||||
|
||||
@classmethod
|
||||
def parse_message_tags(cls, data):
|
||||
"""
|
||||
Parses IRCv3.2 message tags from a message, as described at http://ircv3.net/specs/core/message-tags-3.2.html
|
||||
|
||||
data is a list of command arguments, split by spaces.
|
||||
"""
|
||||
# Example query:
|
||||
# @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello
|
||||
if data[0].startswith('@'):
|
||||
tagdata = data[0].lstrip('@').split(';')
|
||||
for idx, tag in enumerate(tagdata):
|
||||
tag = tag.replace('\\s', ' ')
|
||||
tag = tag.replace('\\r', '\r')
|
||||
tag = tag.replace('\\n', '\n')
|
||||
tag = tag.replace('\\:', ';')
|
||||
|
||||
# We want to drop lone \'s but keep \\ as \ ...
|
||||
tag = tag.replace('\\\\', '\x00')
|
||||
tag = tag.replace('\\', '')
|
||||
tag = tag.replace('\x00', '\\')
|
||||
tagdata[idx] = tag
|
||||
|
||||
results = cls.parse_isupport(tagdata, fallback='')
|
||||
return results
|
||||
return {}
|
||||
|
||||
def handle_away(self, source, command, args):
|
||||
"""Handles incoming AWAY messages."""
|
||||
# TS6:
|
||||
@ -268,7 +333,7 @@ class IRCS2SProtocol(IRCCommonProtocol):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.protocol_caps |= {'can-spawn-clients', 'has-ts', 'can-host-relay',
|
||||
self.protocol_caps = {'can-spawn-clients', 'has-ts', 'can-host-relay',
|
||||
'can-track-servers'}
|
||||
|
||||
# Alias
|
||||
@ -286,27 +351,22 @@ class IRCS2SProtocol(IRCCommonProtocol):
|
||||
the SID of the uplink server.
|
||||
"""
|
||||
data = data.split(" ")
|
||||
|
||||
tags = self.parse_message_tags(data)
|
||||
if tags:
|
||||
# If we have message tags, split off the first argument.
|
||||
data = data[1:]
|
||||
|
||||
args = self.parse_args(data)
|
||||
|
||||
sender = args[0]
|
||||
if sender.startswith(':'):
|
||||
sender = sender[1:]
|
||||
sender = sender.lstrip(':')
|
||||
|
||||
# If the sender isn't in numeric format, try to convert it automatically.
|
||||
sender_sid = self._get_SID(sender)
|
||||
sender_uid = self._get_UID(sender)
|
||||
|
||||
if sender_sid in self.servers:
|
||||
# Sender is a server (converting from name to SID gave a valid result).
|
||||
sender = sender_sid
|
||||
elif sender_uid in self.users:
|
||||
# Sender is a user (converting from name to UID gave a valid result).
|
||||
sender = sender_uid
|
||||
else:
|
||||
elif not (args[0].startswith(':')):
|
||||
# No sender prefix; treat as coming from uplink IRCd.
|
||||
sender = self.uplink
|
||||
args.insert(0, sender)
|
||||
@ -327,7 +387,7 @@ class IRCS2SProtocol(IRCCommonProtocol):
|
||||
|
||||
if command == 'ENCAP':
|
||||
# Special case for TS6 encapsulated commands (ENCAP), in forms like this:
|
||||
# <- :00A ENCAP * SU 42XAAAAAC :jlu5
|
||||
# <- :00A ENCAP * SU 42XAAAAAC :GLolol
|
||||
command = args[1]
|
||||
args = args[2:]
|
||||
log.debug("(%s) Rewriting incoming ENCAP to command %s (args: %s)", self.name, command, args)
|
||||
@ -339,12 +399,10 @@ class IRCS2SProtocol(IRCCommonProtocol):
|
||||
else:
|
||||
parsed_args = func(sender, command, args)
|
||||
if parsed_args is not None:
|
||||
if tags:
|
||||
parsed_args['tags'] = tags # Add message tags to this hook payload.
|
||||
return [sender, command, parsed_args]
|
||||
|
||||
def invite(self, source, target, channel):
|
||||
"""Sends an INVITE from a PyLink client."""
|
||||
"""Sends an INVITE from a PyLink client.."""
|
||||
if not self.is_internal_client(source):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
@ -370,12 +428,6 @@ class IRCS2SProtocol(IRCCommonProtocol):
|
||||
# handle_part() does that just fine.
|
||||
self.handle_part(target, 'KICK', [channel])
|
||||
|
||||
def oper_notice(self, source, text):
|
||||
"""
|
||||
Send a message to all opers.
|
||||
"""
|
||||
self._send_with_prefix(source, 'WALLOPS :%s' % text)
|
||||
|
||||
def numeric(self, source, numeric, target, text):
|
||||
"""Sends raw numerics from a server to a remote client. This is used for WHOIS replies."""
|
||||
# Mangle the target for IRCds that require it.
|
||||
@ -399,7 +451,7 @@ class IRCS2SProtocol(IRCCommonProtocol):
|
||||
|
||||
This is mostly used by PyLink internals to check whether the remote link is up."""
|
||||
if self.sid and self.connected.is_set():
|
||||
self._send_with_prefix(self.sid, 'PING %s' % self._expandPUID(self.uplink))
|
||||
self._send_with_prefix(self.sid, 'PING %s' % self._expandPUID(self.sid))
|
||||
|
||||
def quit(self, numeric, reason):
|
||||
"""Quits a PyLink client."""
|
||||
@ -486,18 +538,17 @@ class IRCS2SProtocol(IRCCommonProtocol):
|
||||
def handle_kill(self, source, command, args):
|
||||
"""Handles incoming KILLs."""
|
||||
killed = self._get_UID(args[0])
|
||||
# Some IRCds send explicit QUIT messages for their killed clients in addition to KILL,
|
||||
# meaning that our target client may have been removed already. If this is the case,
|
||||
# don't bother forwarding this message on.
|
||||
# Generally, we only need to distinguish between KILL and QUIT if the target is
|
||||
# one of our clients, in which case the above statement isn't really applicable.
|
||||
if killed in self.users:
|
||||
userdata = self._remove_client(killed)
|
||||
else:
|
||||
return
|
||||
# Depending on whether the IRCd sends explicit QUIT messages for
|
||||
# killed clients, the user may or may not have automatically been
|
||||
# removed from our user list.
|
||||
# If not, we have to assume that KILL = QUIT and remove them
|
||||
# ourselves.
|
||||
data = self.users.get(killed)
|
||||
if data:
|
||||
self._remove_client(killed)
|
||||
|
||||
# TS6-style kills look something like this:
|
||||
# <- :jlu5 KILL 38QAAAAAA :hidden-1C620195!jlu5 (test)
|
||||
# <- :GL KILL 38QAAAAAA :hidden-1C620195!GL (test)
|
||||
# What we actually want is to format a pretty kill message, in the form
|
||||
# "Killed (killername (reason))".
|
||||
|
||||
@ -513,21 +564,21 @@ class IRCS2SProtocol(IRCCommonProtocol):
|
||||
# Get the reason, which is enclosed in brackets.
|
||||
killmsg = ' '.join(args[1].split(" ")[1:])[1:-1]
|
||||
if not killmsg:
|
||||
log.warning('(%s) Failed to extract kill reason: %r', self.name, args)
|
||||
killmsg = args[1]
|
||||
log.warning('(%s) Failed to extract kill reason: %r', irc.name, args)
|
||||
killmsg = '<No reason given>'
|
||||
else:
|
||||
# We already have a preformatted kill, so just pass it on as is.
|
||||
# XXX: this does create a convoluted kill string if we want to forward kills
|
||||
# over relay.
|
||||
# InspIRCd:
|
||||
# <- :1MLAAAAA1 KILL 0ALAAAAAC :Killed (jlu5 (test))
|
||||
# <- :1MLAAAAA1 KILL 0ALAAAAAC :Killed (GL (test))
|
||||
# ngIRCd:
|
||||
# <- :jlu5 KILL PyLink-devel :KILLed by jlu5: ?
|
||||
# <- :GL KILL PyLink-devel :KILLed by GL: ?
|
||||
killmsg = args[1]
|
||||
|
||||
return {'target': killed, 'text': killmsg, 'userdata': userdata}
|
||||
return {'target': killed, 'text': killmsg, 'userdata': data}
|
||||
|
||||
def _check_cloak_change(self, uid): # Stub by default
|
||||
def _check_cloak_change(self, uid):
|
||||
return
|
||||
|
||||
def _check_umode_away_change(self, uid):
|
||||
@ -564,7 +615,7 @@ class IRCS2SProtocol(IRCCommonProtocol):
|
||||
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc
|
||||
|
||||
# P10:
|
||||
# <- ABAAA M jlu5 -w
|
||||
# <- ABAAA M GL -w
|
||||
# <- ABAAA M #test +v ABAAB 1460747615
|
||||
# <- ABAAA OM #test +h ABAAA
|
||||
target = self._get_UID(args[0])
|
||||
@ -674,14 +725,9 @@ class IRCS2SProtocol(IRCCommonProtocol):
|
||||
# TS6:
|
||||
# <- :1SRAAGB4T QUIT :Quit: quit message goes here
|
||||
# P10:
|
||||
# <- ABAAB Q :Killed (jlu5_ (bangbang))
|
||||
userdata = self._remove_client(numeric)
|
||||
if userdata:
|
||||
try:
|
||||
reason = args[0]
|
||||
except IndexError:
|
||||
reason = ''
|
||||
return {'text': reason, 'userdata': userdata}
|
||||
# <- ABAAB Q :Killed (GL_ (bangbang))
|
||||
self._remove_client(numeric)
|
||||
return {'text': args[0]}
|
||||
|
||||
def handle_stats(self, numeric, command, args):
|
||||
"""Handles the IRC STATS command."""
|
||||
|
@ -3,10 +3,7 @@ nefarious.py: Migration stub to the new P10 protocol module.
|
||||
"""
|
||||
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.protocols.p10 import P10Protocol
|
||||
|
||||
__all__ = ['NefariousProtocol']
|
||||
|
||||
from pylinkirc.protocols.p10 import *
|
||||
|
||||
class NefariousProtocol(P10Protocol):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -7,17 +7,14 @@ ngircd.py: PyLink protocol module for ngIRCd.
|
||||
# and https://tools.ietf.org/html/rfc2813
|
||||
##
|
||||
|
||||
import re
|
||||
import time
|
||||
import re
|
||||
|
||||
from pylinkirc import __version__, conf, utils
|
||||
from pylinkirc import utils, conf, __version__
|
||||
from pylinkirc.classes import *
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.protocols.ircs2s_common import *
|
||||
|
||||
__all__ = ['NgIRCdProtocol']
|
||||
|
||||
|
||||
class NgIRCdProtocol(IRCS2SProtocol):
|
||||
def __init__(self, irc):
|
||||
super().__init__(irc)
|
||||
@ -61,31 +58,15 @@ class NgIRCdProtocol(IRCS2SProtocol):
|
||||
self._caps.clear()
|
||||
|
||||
self.cmodes.update({
|
||||
'banexception': 'e',
|
||||
'invex': 'I',
|
||||
'noinvite': 'V',
|
||||
'nokick': 'Q',
|
||||
'nonick': 'N',
|
||||
'operonly': 'O',
|
||||
'permanent': 'P',
|
||||
'registered': 'r',
|
||||
'regmoderated': 'M',
|
||||
'regonly': 'R',
|
||||
'sslonly': 'z'
|
||||
'banexception': 'e', 'invex': 'I', 'regmoderated': 'M', 'nonick': 'N',
|
||||
'operonly': 'O', 'permanent': 'P', 'nokick': 'Q', 'registered': 'r',
|
||||
'regonly': 'R', 'noinvite': 'V', 'sslonly': 'z'
|
||||
})
|
||||
|
||||
self.umodes.update({
|
||||
'away': 'a',
|
||||
'bot': 'B',
|
||||
'cloak': 'x',
|
||||
'deaf_commonchan': 'C',
|
||||
'floodexempt': 'F',
|
||||
'hidechans': 'I',
|
||||
'privdeaf': 'b',
|
||||
'registered': 'R',
|
||||
'restricted': 'r',
|
||||
'servprotect': 'q',
|
||||
'sno_clientconnections': 'c'
|
||||
'away': 'a', 'deaf': 'b', 'bot': 'B', 'sno_clientconnections': 'c',
|
||||
'deaf_commonchan': 'C', 'floodexempt': 'f', 'hidechans': 'I',
|
||||
'servprotect': 'q', 'restricted': 'r', 'registered': 'R', 'cloak': 'x'
|
||||
})
|
||||
|
||||
def spawn_client(self, nick, ident='null', host='null', realhost=None, modes=set(),
|
||||
@ -117,7 +98,7 @@ class NgIRCdProtocol(IRCS2SProtocol):
|
||||
|
||||
# Grab our server token; this is used instead of server name to denote where the client is.
|
||||
server_token = server.rsplit('@')[-1]
|
||||
# <- :ngircd.midnight.local NICK jlu5 1 ~jlu5 localhost 1 +io :realname
|
||||
# <- :ngircd.midnight.local NICK GL 1 ~gl localhost 1 +io :realname
|
||||
self._send_with_prefix(server, 'NICK %s %s %s %s %s %s :%s' % (nick, self.servers[server].hopcount,
|
||||
ident, host, server_token, self.join_modes(modes), realname))
|
||||
return userobj
|
||||
@ -285,7 +266,7 @@ class NgIRCdProtocol(IRCS2SProtocol):
|
||||
"""
|
||||
Sets a server ban.
|
||||
"""
|
||||
# <- :jlu5 GLINE *!*@bad.user 3d :test
|
||||
# <- :GL GLINE *!*@bad.user 3d :test
|
||||
assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*"
|
||||
self._send_with_prefix(source, 'GLINE *!%s@%s %s :%s' % (user, host, duration, reason))
|
||||
|
||||
@ -380,8 +361,8 @@ class NgIRCdProtocol(IRCS2SProtocol):
|
||||
def handle_join(self, source, command, args):
|
||||
# RFC 2813 is odd to say the least... https://tools.ietf.org/html/rfc2813#section-4.2.1
|
||||
# Basically, we expect messages of the forms:
|
||||
# <- :jlu5 JOIN #test\x07o
|
||||
# <- :jlu5 JOIN #moretest
|
||||
# <- :GL JOIN #test\x07o
|
||||
# <- :GL JOIN #moretest
|
||||
for chanpair in args[0].split(','):
|
||||
# Normalize channel case.
|
||||
try:
|
||||
@ -426,7 +407,7 @@ class NgIRCdProtocol(IRCS2SProtocol):
|
||||
|
||||
def handle_metadata(self, source, command, args):
|
||||
"""Handles various user metadata for ngIRCd (cloaked host, account name, etc.)"""
|
||||
# <- :ngircd.midnight.local METADATA jlu5 cloakhost :hidden-3a2a739e.ngircd.midnight.local
|
||||
# <- :ngircd.midnight.local METADATA GL cloakhost :hidden-3a2a739e.ngircd.midnight.local
|
||||
target = self._get_UID(args[0])
|
||||
|
||||
if target not in self.users:
|
||||
@ -461,7 +442,7 @@ class NgIRCdProtocol(IRCS2SProtocol):
|
||||
"""
|
||||
if len(args) >= 2:
|
||||
# User introduction:
|
||||
# <- :ngircd.midnight.local NICK jlu5 1 ~jlu5 localhost 1 +io :realname
|
||||
# <- :ngircd.midnight.local NICK GL 1 ~gl localhost 1 +io :realname
|
||||
nick = args[0]
|
||||
assert source in self.servers, "Server %r tried to introduce nick %r but isn't in the servers index?" % (source, nick)
|
||||
self._check_nick_collision(nick)
|
||||
@ -488,13 +469,13 @@ class NgIRCdProtocol(IRCS2SProtocol):
|
||||
'parse_as': 'UID', 'ip': '0.0.0.0'}
|
||||
else:
|
||||
# Nick changes:
|
||||
# <- :jlu5 NICK :jlu5_
|
||||
# <- :GL NICK :GL_
|
||||
oldnick = self.users[source].nick
|
||||
newnick = self.users[source].nick = args[0]
|
||||
return {'newnick': newnick, 'oldnick': oldnick}
|
||||
|
||||
def handle_njoin(self, source, command, args):
|
||||
# <- :ngircd.midnight.local NJOIN #test :tester,@%jlu5
|
||||
# <- :ngircd.midnight.local NJOIN #test :tester,@%GL
|
||||
|
||||
channel = args[0]
|
||||
chandata = self._channels[channel].deepcopy()
|
||||
@ -529,8 +510,7 @@ class NgIRCdProtocol(IRCS2SProtocol):
|
||||
if recvpass != self.serverdata['recvpass']:
|
||||
raise ProtocolError("RECVPASS from uplink does not match configuration!")
|
||||
|
||||
if 'IRC+' not in args[1]:
|
||||
raise ProtocolError("Linking to non-ngIRCd server using this protocol module is not supported")
|
||||
assert 'IRC+' in args[1], "Linking to non-ngIRCd server using this protocol module is not supported"
|
||||
|
||||
def handle_ping(self, source, command, args):
|
||||
"""
|
||||
|
@ -3,27 +3,22 @@ p10.py: P10 protocol module for PyLink, supporting Nefarious IRCu and others.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import socket
|
||||
import string
|
||||
import struct
|
||||
import time
|
||||
from ipaddress import ip_address
|
||||
import time
|
||||
|
||||
from pylinkirc import conf, structures, utils
|
||||
from pylinkirc import utils, structures, conf
|
||||
from pylinkirc.classes import *
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.protocols.ircs2s_common import *
|
||||
|
||||
__all__ = ['P10Protocol']
|
||||
|
||||
|
||||
class P10UIDGenerator(UIDGenerator):
|
||||
"""Implements a P10 UID Generator."""
|
||||
class P10UIDGenerator(IncrementalUIDGenerator):
|
||||
"""Implements an incremental P10 UID Generator."""
|
||||
|
||||
def __init__(self, sid):
|
||||
uidchars = string.ascii_uppercase + string.ascii_lowercase + string.digits + '[]'
|
||||
length = 3
|
||||
super().__init__(uidchars, length, sid)
|
||||
self.allowedchars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789[]'
|
||||
self.length = 3
|
||||
super().__init__(sid)
|
||||
|
||||
def p10b64encode(num, length=2):
|
||||
"""
|
||||
@ -199,7 +194,7 @@ class P10Protocol(IRCS2SProtocol):
|
||||
@staticmethod
|
||||
def decode_p10_ip(ip):
|
||||
"""Decodes a P10 IP."""
|
||||
# Many thanks to Jobe @ evilnet for the code on what to do here. :) -jlu5
|
||||
# Many thanks to Jobe @ evilnet for the code on what to do here. :) -GL
|
||||
|
||||
if len(ip) == 6: # IPv4
|
||||
# Pad the characters with two \x00's (represented in P10 B64 as AA)
|
||||
@ -407,7 +402,7 @@ class P10Protocol(IRCS2SProtocol):
|
||||
|
||||
def kill(self, numeric, target, reason):
|
||||
"""Sends a kill from a PyLink client/server."""
|
||||
# <- ABAAA D AyAAA :nefarious.midnight.vpn!jlu5 (test)
|
||||
# <- ABAAA D AyAAA :nefarious.midnight.vpn!GL (test)
|
||||
|
||||
if (not self.is_internal_client(numeric)) and \
|
||||
(not self.is_internal_server(numeric)):
|
||||
@ -464,7 +459,7 @@ class P10Protocol(IRCS2SProtocol):
|
||||
|
||||
def mode(self, numeric, target, modes, ts=None):
|
||||
"""Sends mode changes from a PyLink client/server."""
|
||||
# <- ABAAA M jlu5 -w
|
||||
# <- ABAAA M GL -w
|
||||
# <- ABAAA M #test +v ABAAB 1460747615
|
||||
|
||||
if (not self.is_internal_client(numeric)) and \
|
||||
@ -495,7 +490,7 @@ class P10Protocol(IRCS2SProtocol):
|
||||
real_target = target
|
||||
else:
|
||||
assert target in self.users, "Unknown mode target %s" % target
|
||||
# P10 uses nicks in user MODE targets, NOT UIDs. ~jlu5
|
||||
# P10 uses nicks in user MODE targets, NOT UIDs. ~GL
|
||||
real_target = self.users[target].nick
|
||||
|
||||
self.apply_modes(target, modes)
|
||||
@ -511,7 +506,7 @@ class P10Protocol(IRCS2SProtocol):
|
||||
|
||||
def nick(self, numeric, newnick):
|
||||
"""Changes the nick of a PyLink client."""
|
||||
# <- ABAAA N jlu5_ 1460753763
|
||||
# <- ABAAA N GL_ 1460753763
|
||||
if not self.is_internal_client(numeric):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
@ -524,15 +519,9 @@ class P10Protocol(IRCS2SProtocol):
|
||||
def numeric(self, source, numeric, target, text):
|
||||
"""Sends raw numerics from a server to a remote client. This is used for WHOIS
|
||||
replies."""
|
||||
# <- AB 311 AyAAA jlu5 ~jlu5 nefarious.midnight.vpn * :realname
|
||||
# <- AB 311 AyAAA GL ~gl nefarious.midnight.vpn * :realname
|
||||
self._send_with_prefix(source, '%s %s %s' % (numeric, target, text))
|
||||
|
||||
def oper_notice(self, source, text):
|
||||
"""
|
||||
Send a message to all opers.
|
||||
"""
|
||||
self._send_with_prefix(source, 'WA :%s' % text)
|
||||
|
||||
def part(self, client, channel, reason=None):
|
||||
"""Sends a part from a PyLink client."""
|
||||
|
||||
@ -565,7 +554,7 @@ class P10Protocol(IRCS2SProtocol):
|
||||
assert not (user == host == '*'), "Refusing to set ridiculous ban on *@*"
|
||||
|
||||
# https://github.com/evilnet/nefarious2/blob/master/doc/p10.txt#L535
|
||||
# <- ABAAA jlu5 * +test@test.host 30 1500300185 1500300215 :haha, you're banned now!!!!1
|
||||
# <- ABAAA GL * +test@test.host 30 1500300185 1500300215 :haha, you're banned now!!!!1
|
||||
currtime = int(time.time())
|
||||
|
||||
if duration == 0 or duration > GLINE_MAX_EXPIRE:
|
||||
@ -754,7 +743,7 @@ class P10Protocol(IRCS2SProtocol):
|
||||
|
||||
def topic(self, source, target, text):
|
||||
"""Sends a TOPIC change from a PyLink client or server."""
|
||||
# <- ABAAA T #test jlu5!~jlu5@nefarious.midnight.vpn 1460852591 1460855795 :blah
|
||||
# <- ABAAA T #test GL!~gl@nefarious.midnight.vpn 1460852591 1460855795 :blah
|
||||
# First timestamp is channel creation time, second is current time,
|
||||
if (not self.is_internal_client(source)) and (not self.is_internal_server(source)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
@ -822,16 +811,6 @@ class P10Protocol(IRCS2SProtocol):
|
||||
|
||||
### HANDLERS
|
||||
|
||||
def handle_events(self, data):
|
||||
"""
|
||||
Events handler for the P10 protocol. This is mostly the same as RFC1459, with extra handling
|
||||
for the fact that P10 does not send source numerics prefixed with a ":".
|
||||
"""
|
||||
# After the initial PASS & SERVER message, every following message should be prefixed
|
||||
if self.uplink and not data.startswith(":"):
|
||||
data = ':' + data
|
||||
return super().handle_events(data)
|
||||
|
||||
def post_connect(self):
|
||||
"""Initializes a connection to a server."""
|
||||
ts = self.start_ts
|
||||
@ -846,7 +825,7 @@ class P10Protocol(IRCS2SProtocol):
|
||||
# 4 <link TS>
|
||||
# 5 <protocol>
|
||||
# 6 <numeric of new server><max client numeric>
|
||||
# 7 <flags> <-- Mark ourselves as a service with IPv6 support (+s & +6) -jlu5
|
||||
# 7 <flags> <-- Mark ourselves as a service with IPv6 support (+s & +6) -GLolol
|
||||
# -1 <description of new server>
|
||||
|
||||
name = self.serverdata["hostname"]
|
||||
@ -935,7 +914,7 @@ class P10Protocol(IRCS2SProtocol):
|
||||
def handle_nick(self, source, command, args):
|
||||
"""Handles the NICK command, used for user introductions and nick changes."""
|
||||
if len(args) > 2:
|
||||
# <- AB N jlu5 1 1460673049 ~jlu5 nefarious.midnight.vpn +iw B]AAAB ABAAA :realname
|
||||
# <- AB N GL 1 1460673049 ~gl nefarious.midnight.vpn +iw B]AAAB ABAAA :realname
|
||||
|
||||
nick = args[0]
|
||||
self._check_nick_collision(nick)
|
||||
@ -952,7 +931,6 @@ class P10Protocol(IRCS2SProtocol):
|
||||
ident, host, realname, realhost, ip)
|
||||
|
||||
uobj = self.users[uid] = User(self, nick, ts, uid, source, ident, host, realname, realhost, ip)
|
||||
uobj.ssl = False
|
||||
self.servers[source].users.add(uid)
|
||||
|
||||
# https://github.com/evilnet/nefarious2/blob/master/doc/p10.txt#L708
|
||||
@ -970,18 +948,15 @@ class P10Protocol(IRCS2SProtocol):
|
||||
accountname = modepair[1].split(':', 1)[0]
|
||||
self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}])
|
||||
|
||||
elif modepair[0][-1] == self.umodes.get('ssl'): # track SSL status where available
|
||||
uobj.ssl = True
|
||||
|
||||
# Call the OPERED UP hook if +o is being added to the mode list.
|
||||
self._check_oper_status_change(uid, parsedmodes)
|
||||
|
||||
self._check_cloak_change(uid)
|
||||
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip, 'parse_as': 'UID', 'secure': uobj.ssl}
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip, 'parse_as': 'UID'}
|
||||
|
||||
else:
|
||||
# <- ABAAA N jlu5_ 1460753763
|
||||
# <- ABAAA N GL_ 1460753763
|
||||
oldnick = self.users[source].nick
|
||||
newnick = self.users[source].nick = args[0]
|
||||
|
||||
@ -1061,7 +1036,7 @@ class P10Protocol(IRCS2SProtocol):
|
||||
# -> X3 Z Channels.CollectiveIRC.Net 1460745823.89510 0 1460745823.089840
|
||||
# Arguments of a PONG: our server hostname, the original TS of PING,
|
||||
# difference between PING and PONG in seconds, the current TS.
|
||||
# Why is this the way it is? I don't know... -jlu5
|
||||
# Why is this the way it is? I don't know... -GL
|
||||
|
||||
target = args[1]
|
||||
sid = self._get_SID(target)
|
||||
@ -1237,7 +1212,7 @@ class P10Protocol(IRCS2SProtocol):
|
||||
|
||||
def handle_topic(self, source, command, args):
|
||||
"""Handles TOPIC changes."""
|
||||
# <- ABAAA T #test jlu5!~jlu5@nefarious.midnight.vpn 1460852591 1460855795 :blah
|
||||
# <- ABAAA T #test GL!~gl@nefarious.midnight.vpn 1460852591 1460855795 :blah
|
||||
channel = args[0]
|
||||
topic = args[-1]
|
||||
|
||||
@ -1290,7 +1265,7 @@ class P10Protocol(IRCS2SProtocol):
|
||||
target = args[0]
|
||||
|
||||
if self.serverdata.get('use_extended_accounts'):
|
||||
# Registration: <- AA AC ABAAA R jlu5 1459019072
|
||||
# Registration: <- AA AC ABAAA R GL 1459019072
|
||||
# Logout: <- AA AC ABAAA U
|
||||
|
||||
# 1 <target user numeric>
|
||||
@ -1299,16 +1274,11 @@ class P10Protocol(IRCS2SProtocol):
|
||||
|
||||
# Any other subcommands listed at https://github.com/evilnet/nefarious2/blob/master/doc/p10.txt#L354
|
||||
# shouldn't apply to us.
|
||||
|
||||
if args[1] in ('R', 'M'):
|
||||
accountname = args[2]
|
||||
elif args[1] == 'U':
|
||||
accountname = '' # logout
|
||||
elif len(args[1]) > 1:
|
||||
log.warning('(%s) Got subcommand %r for %s in ACCOUNT message, is use_extended_accounts set correctly?',
|
||||
self.name, args[1], target)
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
else:
|
||||
# ircu or nefarious with F:EXTENDED_ACCOUNTS = FALSE
|
||||
|
@ -2,16 +2,13 @@
|
||||
ts6.py: PyLink protocol module for TS6-based IRCds (charybdis, elemental-ircd).
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
import re
|
||||
|
||||
from pylinkirc import conf, utils
|
||||
from pylinkirc import utils, conf
|
||||
from pylinkirc.classes import *
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.protocols.ts6_common import TS6BaseProtocol
|
||||
|
||||
__all__ = ['TS6Protocol']
|
||||
|
||||
from pylinkirc.protocols.ts6_common import *
|
||||
|
||||
class TS6Protocol(TS6BaseProtocol):
|
||||
|
||||
@ -23,7 +20,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
else 'charybdis')
|
||||
self._ircd = self._ircd.lower()
|
||||
if self._ircd not in self.SUPPORTED_IRCDS:
|
||||
log.warning("(%s) Unsupported IRCd %r; falling back to 'charybdis' instead", self.name, self._ircd)
|
||||
log.warning("(%s) Unsupported IRCd %r; falling back to 'charybdis' instead", self.name, target_ircd)
|
||||
self._ircd = 'charybdis'
|
||||
|
||||
self._can_chghost = False
|
||||
@ -93,19 +90,6 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
|
||||
return u
|
||||
|
||||
def spawn_server(self, name, sid=None, uplink=None, desc=None):
|
||||
"""
|
||||
Spawns a server off a PyLink server. desc (server description)
|
||||
defaults to the one in the config. uplink defaults to the main PyLink
|
||||
server, and sid (the server ID) is automatically generated if not
|
||||
given.
|
||||
"""
|
||||
desc = desc or self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']
|
||||
if self.serverdata.get('hidden', False):
|
||||
desc = '(H) ' + desc
|
||||
|
||||
return super().spawn_server(name, sid=sid, uplink=uplink, desc=desc)
|
||||
|
||||
def join(self, client, channel):
|
||||
"""Joins a PyLink client to a channel."""
|
||||
# JOIN:
|
||||
@ -117,15 +101,6 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
self._channels[channel].users.add(client)
|
||||
self.users[client].channels.add(channel)
|
||||
|
||||
def oper_notice(self, source, text):
|
||||
"""
|
||||
Send a message to all opers.
|
||||
"""
|
||||
if self.is_internal_server(source):
|
||||
# Charybdis TS6 only allows OPERWALL from users
|
||||
source = self.pseudoclient.uid
|
||||
self._send_with_prefix(source, 'OPERWALL :%s' % text)
|
||||
|
||||
def sjoin(self, server, channel, users, ts=None, modes=set()):
|
||||
"""Sends an SJOIN for a group of users to a channel.
|
||||
|
||||
@ -326,19 +301,19 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
'quiet': 'q', 'redirect': 'f', 'freetarget': 'F',
|
||||
'joinflood': 'j', 'largebanlist': 'L', 'permanent': 'P',
|
||||
'noforwards': 'Q', 'stripcolor': 'c', 'allowinvite':
|
||||
'g', 'opmoderated': 'z', 'noctcp': 'C',
|
||||
'g', 'opmoderated': 'z', 'noctcp': 'C', 'ssl': 'Z',
|
||||
# charybdis modes provided by extensions
|
||||
'operonly': 'O', 'adminonly': 'A', 'sslonly': 'S',
|
||||
'nonotice': 'T',
|
||||
'*A': 'beIq', '*B': 'k', '*C': 'lfj', '*D': 'mnprstFLPQcgzCOAST'
|
||||
'*A': 'beIq', '*B': 'k', '*C': 'lfj', '*D': 'mnprstFLPQcgzCZOAST'
|
||||
})
|
||||
self.umodes.update({
|
||||
'deaf': 'D', 'servprotect': 'S', 'admin': 'a',
|
||||
'invisible': 'i', 'oper': 'o', 'wallops': 'w',
|
||||
'snomask': 's', 'noforward': 'Q', 'regdeaf': 'R',
|
||||
'callerid': 'g', 'operwall': 'z', 'locops': 'l',
|
||||
'cloak': 'x', 'override': 'p', 'ssl': 'Z',
|
||||
'*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzlxpZ'
|
||||
'cloak': 'x', 'override': 'p',
|
||||
'*A': '', '*B': '', '*C': '', '*D': 'DSaiowsQRgzlxp'
|
||||
})
|
||||
|
||||
# Charybdis extbans
|
||||
@ -386,7 +361,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
self.cmodes.update(chatircd_cmodes)
|
||||
self.cmodes['*D'] += ''.join(chatircd_cmodes.values())
|
||||
|
||||
chatircd_umodes = {'netadmin': 'n', 'bot': 'B', 'sslonlymsg': 't'}
|
||||
chatircd_umodes = {'netadmin': 'n', 'bot': 'B', 'callerid_sslonly': 't'}
|
||||
self.umodes.update(chatircd_umodes)
|
||||
self.umodes['*D'] += ''.join(chatircd_umodes.values())
|
||||
|
||||
@ -422,10 +397,8 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
# KLN: supports remote KLINEs
|
||||
f('CAPAB :QS ENCAP EX CHW IE KNOCK SAVE SERVICES TB EUID RSFNC EOPMOD SAVETS_100 KLN')
|
||||
|
||||
sdesc = self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']
|
||||
if self.serverdata.get('hidden', False):
|
||||
sdesc = '(H) ' + sdesc
|
||||
f('SERVER %s 0 :%s' % (self.serverdata["hostname"], sdesc))
|
||||
f('SERVER %s 0 :%s' % (self.serverdata["hostname"],
|
||||
self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']))
|
||||
|
||||
# Finally, end all the initialization with a PING - that's Charybdis'
|
||||
# way of saying end-of-burst :)
|
||||
@ -440,7 +413,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
|
||||
if args[0] != self.serverdata['recvpass']:
|
||||
# Check if recvpass is correct
|
||||
raise ProtocolError('Recvpass from uplink server %r does not match configuration!' % numeric)
|
||||
raise ProtocolError('Recvpass from uplink server %s does not match configuration!' % servername)
|
||||
|
||||
if args[1] != 'TS' and args[2] != '6':
|
||||
raise ProtocolError("Remote protocol version is too old! Is this even TS6?")
|
||||
@ -584,7 +557,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
|
||||
def handle_euid(self, numeric, command, args):
|
||||
"""Handles incoming EUID commands (user introduction)."""
|
||||
# <- :42X EUID jlu5 1 1437505322 +ailoswz ~jlu5 127.0.0.1 127.0.0.1 42XAAAAAB * * :realname
|
||||
# <- :42X EUID GL 1 1437505322 +ailoswz ~gl 127.0.0.1 127.0.0.1 42XAAAAAB * * :realname
|
||||
nick = args[0]
|
||||
self._check_nick_collision(nick)
|
||||
ts, modes, ident, host, ip, uid, realhost, accountname, realname = args[2:11]
|
||||
@ -614,11 +587,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
if accountname != "*":
|
||||
self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}])
|
||||
|
||||
# charybdis and derivatives have a usermode (+Z) to mark SSL connections
|
||||
# ratbox doesn't appear to have this
|
||||
has_ssl = self.users[uid].ssl = ('+%s' % self.umodes.get('ssl'), None) in parsedmodes
|
||||
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip, 'secure': has_ssl}
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
|
||||
|
||||
def handle_uid(self, numeric, command, args):
|
||||
"""Handles legacy user introductions (UID)."""
|
||||
@ -657,7 +626,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
|
||||
return
|
||||
|
||||
# <- :services.int SERVER a.bc 2 :(H) [jlu5] a
|
||||
# <- :services.int SERVER a.bc 2 :(H) [GL] a
|
||||
return super().handle_server(numeric, command, args)
|
||||
|
||||
def handle_tmode(self, numeric, command, args):
|
||||
@ -675,7 +644,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
|
||||
def handle_tb(self, numeric, command, args):
|
||||
"""Handles incoming topic burst (TB) commands."""
|
||||
# <- :42X TB #chat 1467427448 jlu5!~jlu5@127.0.0.1 :test
|
||||
# <- :42X TB #chat 1467427448 GL!~gl@127.0.0.1 :test
|
||||
channel = args[0]
|
||||
ts = args[1]
|
||||
setter = args[2]
|
||||
@ -686,7 +655,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
|
||||
def handle_etb(self, numeric, command, args):
|
||||
"""Handles extended topic burst (ETB)."""
|
||||
# <- :00AAAAAAC ETB 0 #test 1470021157 jlu5 :test | abcd
|
||||
# <- :00AAAAAAC ETB 0 #test 1470021157 GL :test | abcd
|
||||
# Same as TB, with extra TS and extensions arguments.
|
||||
channel = args[1]
|
||||
ts = args[2]
|
||||
@ -723,7 +692,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
the administrator that certain extensions should be loaded for the best
|
||||
compatibility.
|
||||
"""
|
||||
# <- :charybdis.midnight.vpn 472 jlu5|devel O :is an unknown mode char to me
|
||||
# <- :charybdis.midnight.vpn 472 GL|devel O :is an unknown mode char to me
|
||||
badmode = args[1]
|
||||
reason = args[-1]
|
||||
setter = args[0]
|
||||
@ -740,7 +709,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
"""
|
||||
Handles SU, which is used for setting login information.
|
||||
"""
|
||||
# <- :00A ENCAP * SU 42XAAAAAC :jlu5
|
||||
# <- :00A ENCAP * SU 42XAAAAAC :GLolol
|
||||
# <- :00A ENCAP * SU 42XAAAAAC
|
||||
try:
|
||||
account = args[1] # Account name is being set
|
||||
@ -759,7 +728,7 @@ class TS6Protocol(TS6BaseProtocol):
|
||||
|
||||
def handle_realhost(self, uid, command, args):
|
||||
"""Handles real host propagation."""
|
||||
log.debug('(%s) Got REALHOST %s for %s', self.name, args[0], uid)
|
||||
log.debug('(%s) Got REALHOST %s for %s', args[0], uid)
|
||||
self.users[uid].realhost = args[0]
|
||||
|
||||
def handle_login(self, uid, command, args):
|
||||
|
@ -5,14 +5,11 @@ ts6_common.py: Common base protocol class with functions shared by the UnrealIRC
|
||||
import string
|
||||
import time
|
||||
|
||||
from pylinkirc import conf, structures
|
||||
from pylinkirc import utils, structures, conf
|
||||
from pylinkirc.classes import *
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.protocols.ircs2s_common import *
|
||||
|
||||
__all__ = ['TS6BaseProtocol']
|
||||
|
||||
|
||||
class TS6SIDGenerator():
|
||||
"""
|
||||
TS6 SID Generator. <query> is a 3 character string with any combination of
|
||||
@ -87,16 +84,18 @@ class TS6SIDGenerator():
|
||||
sid = ''.join(self.output)
|
||||
return sid
|
||||
|
||||
class TS6UIDGenerator(UIDGenerator):
|
||||
class TS6UIDGenerator(IncrementalUIDGenerator):
|
||||
"""Implements an incremental TS6 UID Generator."""
|
||||
|
||||
def __init__(self, sid):
|
||||
# Define the options for IncrementalUIDGenerator, and then
|
||||
# initialize its functions.
|
||||
# TS6 UIDs are 6 characters in length (9 including the SID).
|
||||
# They go from ABCDEFGHIJKLMNOPQRSTUVWXYZ -> 0123456789 -> wrap around:
|
||||
# e.g. AAAAAA, AAAAAB ..., AAAAA8, AAAAA9, AAAABA, etc.
|
||||
uidchars = string.ascii_uppercase + string.digits
|
||||
length = 6
|
||||
super().__init__(uidchars, length, sid)
|
||||
self.allowedchars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456879'
|
||||
self.length = 6
|
||||
super().__init__(sid)
|
||||
|
||||
class TS6BaseProtocol(IRCS2SProtocol):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -216,7 +215,7 @@ class TS6BaseProtocol(IRCS2SProtocol):
|
||||
|
||||
def handle_nick(self, numeric, command, args):
|
||||
"""Handles incoming NICK changes."""
|
||||
# <- :70MAAAAAA NICK jlu5-devel 1434744242
|
||||
# <- :70MAAAAAA NICK GL-devel 1434744242
|
||||
oldnick = self.users[numeric].nick
|
||||
newnick = self.users[numeric].nick = args[0]
|
||||
|
||||
@ -247,7 +246,7 @@ class TS6BaseProtocol(IRCS2SProtocol):
|
||||
|
||||
def handle_server(self, numeric, command, args):
|
||||
"""Handles the SERVER command, used for introducing older (TS5) servers."""
|
||||
# <- :services.int SERVER a.bc 2 :(H) [jlu5] test jupe
|
||||
# <- :services.int SERVER a.bc 2 :(H) [GL] test jupe
|
||||
servername = args[0].lower()
|
||||
sdesc = args[-1]
|
||||
self.servers[servername] = Server(self, numeric, servername, desc=sdesc)
|
||||
@ -270,5 +269,5 @@ class TS6BaseProtocol(IRCS2SProtocol):
|
||||
# This is rewritten to SVSNICK with args ['902AAAAAB', 'Guest53593', '1468299404']
|
||||
|
||||
# UnrealIRCd:
|
||||
# <- :services.midnight.vpn SVSNICK jlu5 Guest87795 1468303726
|
||||
# <- :services.midnight.vpn SVSNICK GL Guest87795 1468303726
|
||||
return {'target': self._get_UID(args[0]), 'newnick': args[1]}
|
||||
|
@ -1,19 +1,16 @@
|
||||
"""
|
||||
unreal.py: UnrealIRCd 4.x-5.x protocol module for PyLink.
|
||||
unreal.py: UnrealIRCd 4.x protocol module for PyLink.
|
||||
"""
|
||||
|
||||
import codecs
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
import codecs
|
||||
import socket
|
||||
import re
|
||||
|
||||
from pylinkirc import conf, utils
|
||||
from pylinkirc import utils, conf
|
||||
from pylinkirc.classes import *
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.protocols.ts6_common import TS6BaseProtocol
|
||||
|
||||
__all__ = ['UnrealProtocol']
|
||||
|
||||
from pylinkirc.protocols.ts6_common import *
|
||||
|
||||
SJOIN_PREFIXES = {'q': '*', 'a': '~', 'o': '@', 'h': '%', 'v': '+', 'b': '&', 'e': '"', 'I': "'"}
|
||||
|
||||
@ -21,70 +18,16 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# I'm not sure what the real limit is, but the text posted at
|
||||
# https://github.com/jlu5/PyLink/issues/378 suggests 427 characters.
|
||||
# https://github.com/unrealircd/unrealircd/blob/4cad9cb/src/modules/m_server.c#L1260 may
|
||||
# also help. (but why BUFSIZE-*80*?) -jlu5
|
||||
# also help. (but why BUFSIZE-*80*?) -GL
|
||||
S2S_BUFSIZE = 427
|
||||
_KNOWN_CMODES = {'ban': 'b',
|
||||
'banexception': 'e',
|
||||
'blockcolor': 'c',
|
||||
'censor': 'G',
|
||||
'delayjoin': 'D',
|
||||
'flood_unreal': 'f',
|
||||
'invex': 'I',
|
||||
'inviteonly': 'i',
|
||||
'issecure': 'Z',
|
||||
'key': 'k',
|
||||
'limit': 'l',
|
||||
'moderated': 'm',
|
||||
'noctcp': 'C',
|
||||
'noextmsg': 'n',
|
||||
'noinvite': 'V',
|
||||
'nokick': 'Q',
|
||||
'noknock': 'K',
|
||||
'nonick': 'N',
|
||||
'nonotice': 'T',
|
||||
'op': 'o',
|
||||
'operonly': 'O',
|
||||
'permanent': 'P',
|
||||
'private': 'p',
|
||||
'registered': 'r',
|
||||
'regmoderated': 'M',
|
||||
'regonly': 'R',
|
||||
'secret': 's',
|
||||
'sslonly': 'z',
|
||||
'stripcolor': 'S',
|
||||
'topiclock': 't',
|
||||
'voice': 'v'}
|
||||
_KNOWN_UMODES = {'bot': 'B',
|
||||
'censor': 'G',
|
||||
'cloak': 'x',
|
||||
'deaf': 'd',
|
||||
'filter': 'G',
|
||||
'hidechans': 'p',
|
||||
'hideidle': 'I',
|
||||
'hideoper': 'H',
|
||||
'invisible': 'i',
|
||||
'noctcp': 'T',
|
||||
'protected': 'q',
|
||||
'regdeaf': 'R',
|
||||
'registered': 'r',
|
||||
'sslonlymsg': 'Z',
|
||||
'servprotect': 'S',
|
||||
'showwhois': 'W',
|
||||
'snomask': 's',
|
||||
'ssl': 'z',
|
||||
'vhost': 't',
|
||||
'wallops': 'w'}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.protocol_caps |= {'slash-in-nicks', 'underscore-in-hosts', 'slash-in-hosts'}
|
||||
self.protocol_caps |= {'slash-in-nicks', 'underscore-in-hosts'}
|
||||
# Set our case mapping (rfc1459 maps "\" and "|" together, for example)
|
||||
self.casemapping = 'ascii'
|
||||
|
||||
# Unreal protocol version
|
||||
self.proto_ver = 4203
|
||||
self.proto_ver = 4017
|
||||
self.min_proto_ver = 4000
|
||||
|
||||
self.hook_map = {'UMODE2': 'MODE', 'SVSKILL': 'KILL', 'SVSMODE': 'MODE',
|
||||
'SVS2MODE': 'MODE', 'SJOIN': 'JOIN', 'SETHOST': 'CHGHOST',
|
||||
'SETIDENT': 'CHGIDENT', 'SETNAME': 'CHGNAME',
|
||||
@ -149,7 +92,7 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# Now, strip the trailing \n and decode into a string again.
|
||||
encoded_ip = encoded_ip.strip().decode()
|
||||
|
||||
# <- :001 UID jlu5 0 1441306929 jlu5 localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname
|
||||
# <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname
|
||||
self._send_with_prefix(server, "UID {nick} {hopcount} {ts} {ident} {realhost} {uid} 0 {modes} "
|
||||
"{host} * {ip} :{realname}".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
@ -163,14 +106,9 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
"""Joins a PyLink client to a channel."""
|
||||
if not self.is_internal_client(client):
|
||||
raise LookupError('No such PyLink client exists.')
|
||||
|
||||
# Forward this on to SJOIN, as using JOIN in Unreal S2S seems to cause TS corruption bugs.
|
||||
# This seems to be what Unreal itself does anyways.
|
||||
if channel not in self.channels:
|
||||
prefix = 'o' # Create new channels with the first joiner as op
|
||||
else:
|
||||
prefix = ''
|
||||
self.sjoin(self.sid, channel, [(prefix, client)])
|
||||
self._send_with_prefix(client, "JOIN %s" % channel)
|
||||
self._channels[channel].users.add(client)
|
||||
self.users[client].channels.add(channel)
|
||||
|
||||
def sjoin(self, server, channel, users, ts=None, modes=set()):
|
||||
"""Sends an SJOIN for a group of users to a channel.
|
||||
@ -259,7 +197,7 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
Sends mode changes from a PyLink client/server. The mode list should be
|
||||
a list of (mode, arg) tuples, i.e. the format of utils.parse_modes() output.
|
||||
"""
|
||||
# <- :unreal.midnight.vpn MODE #test +ntCo jlu5 1444361345
|
||||
# <- :unreal.midnight.vpn MODE #test +ntCo GL 1444361345
|
||||
|
||||
if (not self.is_internal_client(numeric)) and \
|
||||
(not self.is_internal_server(numeric)):
|
||||
@ -268,8 +206,12 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
self.apply_modes(target, modes)
|
||||
|
||||
if self.is_channel(target):
|
||||
modes = list(modes) # Needed for indexing
|
||||
|
||||
# Fix assignment TypeError in the expandPUID bit (sets can't be
|
||||
# assigned to by index).
|
||||
modes = list(modes)
|
||||
|
||||
modes = list(modes) # Needed for indexing
|
||||
# Make sure we expand any PUIDs when sending outgoing modes...
|
||||
for idx, mode in enumerate(modes):
|
||||
if mode[0][-1] in self.prefixmodes:
|
||||
@ -312,12 +254,6 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
joinedmodes = self.join_modes(modes)
|
||||
self._send_with_prefix(target, 'UMODE2 %s' % joinedmodes)
|
||||
|
||||
def oper_notice(self, source, text):
|
||||
"""
|
||||
Send a message to all opers.
|
||||
"""
|
||||
self._send_with_prefix(source, 'GLOBOPS :%s' % text)
|
||||
|
||||
def set_server_ban(self, source, duration, user='*', host='*', reason='User banned'):
|
||||
"""
|
||||
Sets a server ban.
|
||||
@ -381,21 +317,11 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
self.call_hooks([self.sid, 'CHGNAME',
|
||||
{'target': target, 'newgecos': text}])
|
||||
|
||||
def kill(self, source, target, reason):
|
||||
"""Sends a kill from a PyLink client or server."""
|
||||
|
||||
if (not self.is_internal_client(source)) and \
|
||||
(not self.is_internal_server(source)):
|
||||
raise LookupError('No such PyLink client/server exists.')
|
||||
|
||||
self._send_with_prefix(source, 'KILL %s :%s' % (target, reason))
|
||||
self._remove_client(target)
|
||||
|
||||
def knock(self, numeric, target, text):
|
||||
"""Sends a KNOCK from a PyLink client."""
|
||||
# KNOCKs in UnrealIRCd are actually just specially formatted NOTICEs,
|
||||
# sent to all ops in a channel.
|
||||
# <- :unreal.midnight.vpn NOTICE @#test :[Knock] by jlu5|!jlu5@hidden-1C620195 (test)
|
||||
# <- :unreal.midnight.vpn NOTICE @#test :[Knock] by GL|!gl@hidden-1C620195 (test)
|
||||
assert self.is_channel(target), "Can only knock on channels!"
|
||||
sender = self.get_server(numeric)
|
||||
s = '[Knock] by %s (%s)' % (self.get_hostmask(numeric), text)
|
||||
@ -411,6 +337,15 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# Track usages of legacy (Unreal 3.2) nicks.
|
||||
self.legacy_uidgen = PUIDGenerator('U32user')
|
||||
|
||||
self.umodes.update({'deaf': 'd', 'invisible': 'i', 'hidechans': 'p',
|
||||
'protected': 'q', 'registered': 'r',
|
||||
'snomask': 's', 'vhost': 't', 'wallops': 'w',
|
||||
'bot': 'B', 'cloak': 'x', 'ssl': 'z',
|
||||
'filter': 'G', 'hideoper': 'H', 'hideidle': 'I',
|
||||
'regdeaf': 'R', 'servprotect': 'S',
|
||||
'noctcp': 'T', 'showwhois': 'W',
|
||||
'*A': '', '*B': '', '*C': '', '*D': 'dipqrstwBxzGHIRSTW'})
|
||||
|
||||
f = self.send
|
||||
host = self.serverdata["hostname"]
|
||||
|
||||
@ -433,32 +368,17 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# not work for any UnrealIRCd 3.2 users.
|
||||
# ESVID - Supports account names in services stamps instead of just the signon time.
|
||||
# AFAIK this doesn't actually affect services' behaviour?
|
||||
# EXTSWHOIS - support multiple SWHOIS lines (purely informational for us)
|
||||
f('PROTOCTL SJOIN SJ3 NOQUIT NICKv2 VL UMODE2 PROTOCTL NICKIP EAUTH=%s SID=%s VHP ESVID EXTSWHOIS' % (self.serverdata["hostname"], self.sid))
|
||||
f('PROTOCTL SJOIN SJ3 NOQUIT NICKv2 VL UMODE2 PROTOCTL NICKIP EAUTH=%s SID=%s VHP ESVID' % (self.serverdata["hostname"], self.sid))
|
||||
sdesc = self.serverdata.get('serverdesc') or conf.conf['pylink']['serverdesc']
|
||||
f('SERVER %s 1 U%s-h6e-%s :%s' % (host, self.proto_ver, self.sid, sdesc))
|
||||
|
||||
f('NETINFO 1 %s %s * 0 0 0 :%s' % (self.start_ts, self.proto_ver, self.serverdata.get("netname", self.name)))
|
||||
self._send_with_prefix(self.sid, 'EOS')
|
||||
|
||||
# Extban definitions
|
||||
self.extbans_acting = {'quiet': '~q:',
|
||||
'ban_nonick': '~n:',
|
||||
'ban_nojoins': '~j:',
|
||||
'filter': '~T:block:',
|
||||
'filter_censor': '~T:censor:',
|
||||
'msgbypass_external': '~m:external:',
|
||||
'msgbypass_censor': '~m:censor:',
|
||||
'msgbypass_moderated': '~m:moderated:',
|
||||
# These two sort of map to InspIRCd +e S: and +e T:
|
||||
'ban_stripcolor': '~m:color:',
|
||||
'ban_nonotice': '~m:notice:',
|
||||
'timedban_unreal': '~t:'}
|
||||
self.extbans_matching = {'ban_account': '~a:',
|
||||
'ban_inchannel': '~c:',
|
||||
'ban_opertype': '~O:',
|
||||
'ban_realname': '~r:',
|
||||
'ban_account_legacy': '~R:',
|
||||
'ban_certfp': '~S:'}
|
||||
self.extbans_acting = {'quiet': '~q:', 'ban_nonick': '~n:', 'ban_nojoins': '~j:',
|
||||
'filter': '~T:block:', 'filter_censor': '~T:censor:'}
|
||||
self.extbans_matching = {'ban_account': '~a:', 'ban_inchannel': '~c:', 'ban_opertype': '~O:',
|
||||
'ban_realname': '~r:', 'ban_account_legacy': '~R:', 'ban_certfp': '~S:'}
|
||||
|
||||
def handle_eos(self, numeric, command, args):
|
||||
"""EOS is used to denote end of burst."""
|
||||
@ -468,8 +388,8 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
return {}
|
||||
|
||||
def handle_uid(self, numeric, command, args):
|
||||
# <- :001 UID jlu5 0 1441306929 jlu5 localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname
|
||||
# <- :001 UID jlu5| 0 1441389007 jlu5 10.120.0.6 001ZO8F03 0 +iwx * 391A9CB9.26A16454.D9847B69.IP CngABg== :realname
|
||||
# <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * midnight-1C620195 fwAAAQ== :realname
|
||||
# <- :001 UID GL| 0 1441389007 gl 10.120.0.6 001ZO8F03 0 +iwx * 391A9CB9.26A16454.D9847B69.IP CngABg== :realname
|
||||
# arguments: nick, hopcount?, ts, ident, real-host, UID, services account (0 if none), modes,
|
||||
# displayed host, cloaked (+x) host, base64-encoded IP, and realname
|
||||
nick = args[0]
|
||||
@ -524,16 +444,13 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
if ('+r', None) in parsedmodes and accountname.isdigit():
|
||||
accountname = nick
|
||||
|
||||
# Track SSL/TLS status
|
||||
has_ssl = self.users[uid].ssl = ('+z', None) in parsedmodes
|
||||
|
||||
if not accountname.isdigit():
|
||||
self.call_hooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': accountname}])
|
||||
|
||||
# parse_as is used here to prevent legacy user introduction from being confused
|
||||
# with a nick change.
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host,
|
||||
'ident': ident, 'ip': ip, 'parse_as': 'UID', 'secure': has_ssl}
|
||||
'ident': ident, 'ip': ip, 'parse_as': 'UID'}
|
||||
|
||||
def handle_pass(self, numeric, command, args):
|
||||
# <- PASS :abcdefg
|
||||
@ -559,7 +476,7 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
|
||||
sdesc = args[-1].split(" ", 1)
|
||||
# Get our protocol version. I really don't know why the version and the server
|
||||
# description aren't two arguments instead of one... -jlu5
|
||||
# description aren't two arguments instead of one... -GLolol
|
||||
vline = sdesc[0].split('-', 1)
|
||||
sdesc = " ".join(sdesc[1:])
|
||||
|
||||
@ -575,30 +492,28 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
"(Unreal 4.x), got %s)" % (self.min_proto_ver, protover))
|
||||
self.servers[numeric] = Server(self, None, sname, desc=sdesc)
|
||||
|
||||
# Prior to 4203, Unreal did not send PROTOCTL USERMODES (see handle_protoctl() )
|
||||
if protover < 4203:
|
||||
self.umodes.update(self._KNOWN_UMODES)
|
||||
self.umodes['*D'] = ''.join(self._KNOWN_UMODES.values())
|
||||
else:
|
||||
# Legacy (non-SID) servers can still be introduced using the SERVER command.
|
||||
# <- :services.int SERVER a.bc 2 :(H) [jlu5] a
|
||||
# <- :services.int SERVER a.bc 2 :(H) [GL] a
|
||||
return super().handle_server(numeric, command, args)
|
||||
|
||||
def handle_protoctl(self, numeric, command, args):
|
||||
"""Handles protocol negotiation."""
|
||||
|
||||
cmodes = {'noknock': 'K', 'limit': 'l', 'registered': 'r', 'flood_unreal': 'f',
|
||||
'censor': 'G', 'noextmsg': 'n', 'invex': 'I', 'permanent': 'P',
|
||||
'sslonly': 'z', 'operonly': 'O', 'moderated': 'm', 'blockcolor': 'c',
|
||||
'regmoderated': 'M', 'noctcp': 'C', 'secret': 's', 'ban': 'b',
|
||||
'nokick': 'Q', 'private': 'p', 'stripcolor': 'S', 'key': 'k',
|
||||
'op': 'o', 'voice': 'v', 'regonly': 'R', 'noinvite': 'V',
|
||||
'banexception': 'e', 'nonick': 'N', 'issecure': 'Z', 'topiclock': 't',
|
||||
'nonotice': 'T', 'delayjoin': 'D', 'inviteonly': 'i'}
|
||||
|
||||
# Make a list of all our capability names.
|
||||
self.caps += [arg.split('=')[0] for arg in args]
|
||||
|
||||
# Unreal 4.0.x:
|
||||
# <- PROTOCTL NOQUIT NICKv2 SJOIN SJOIN2 UMODE2 VL SJ3 TKLEXT TKLEXT2 NICKIP ESVID
|
||||
# <- PROTOCTL CHANMODES=beI,k,l,psmntirzMQNRTOVKDdGPZSCc NICKCHARS= SID=001 MLOCK TS=1441314501 EXTSWHOIS
|
||||
# Unreal 4.2.x:
|
||||
# <- PROTOCTL NOQUIT NICKv2 SJOIN SJOIN2 UMODE2 VL SJ3 TKLEXT TKLEXT2 NICKIP ESVID SJSBY
|
||||
# <- PROTOCTL CHANMODES=beI,kLf,l,psmntirzMQNRTOVKDdGPZSCc USERMODES=iowrsxzdHtIDZRqpWGTSB BOOTED=1574014839 PREFIX=(qaohv)~&@%+ NICKCHARS= SID=001 MLOCK TS=1574020869 EXTSWHOIS
|
||||
# Unreal 5.0.0-rc1:
|
||||
# <- PROTOCTL NOQUIT NICKv2 SJOIN SJOIN2 UMODE2 VL SJ3 TKLEXT TKLEXT2 NICKIP ESVID SJSBY MTAGS
|
||||
# <- PROTOCTL CHANMODES=beI,kLf,lH,psmntirzMQNRTOVKDdGPZSCc USERMODES=iowrsxzdHtIDZRqpWGTSB BOOTED=1574020755 PREFIX=(qaohv)~&@%+ SID=001 MLOCK TS=1574020823 EXTSWHOIS
|
||||
# <- PROTOCTL NICKCHARS= CHANNELCHARS=utf8
|
||||
for cap in args:
|
||||
if cap.startswith('SID'):
|
||||
self.uplink = cap.split('=', 1)[1]
|
||||
@ -606,14 +521,10 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# Parse all the supported channel modes.
|
||||
supported_cmodes = cap.split('=', 1)[1]
|
||||
self.cmodes['*A'], self.cmodes['*B'], self.cmodes['*C'], self.cmodes['*D'] = supported_cmodes.split(',')
|
||||
for namedmode, modechar in self._KNOWN_CMODES.items():
|
||||
for namedmode, modechar in cmodes.items():
|
||||
if modechar in supported_cmodes:
|
||||
self.cmodes[namedmode] = modechar
|
||||
elif cap.startswith('USERMODES'): # Only for protover >= 4203
|
||||
self.umodes['*D'] = supported_umodes = cap.split('=', 1)[1]
|
||||
for namedmode, modechar in self._KNOWN_UMODES.items():
|
||||
if modechar in supported_umodes:
|
||||
self.umodes[namedmode] = modechar
|
||||
self.cmodes['*B'] += 'f' # Add +f to the list too, dunno why it isn't there.
|
||||
|
||||
# Add in the supported prefix modes.
|
||||
self.cmodes.update({'halfop': 'h', 'admin': 'a', 'owner': 'q',
|
||||
@ -621,7 +532,7 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
|
||||
def handle_join(self, numeric, command, args):
|
||||
"""Handles the UnrealIRCd JOIN command."""
|
||||
# <- :jlu5 JOIN #pylink,#test
|
||||
# <- :GL JOIN #pylink,#test
|
||||
if args[0] == '0':
|
||||
# /join 0; part the user from all channels
|
||||
oldchans = self.users[numeric].channels.copy()
|
||||
@ -692,11 +603,6 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
continue
|
||||
|
||||
user = self._get_UID(user) # Normalize nicks to UIDs for Unreal 3.2 links
|
||||
if user not in self.users:
|
||||
# Work around a potential race when sending kills on join
|
||||
log.debug("(%s) Ignoring user %s in SJOIN to %s, they don't exist anymore", self.name, user, channel)
|
||||
continue
|
||||
|
||||
# Unreal uses slightly different prefixes in SJOIN. +q is * instead of ~,
|
||||
# and +a is ~ instead of &.
|
||||
modeprefix = (r.group(1) or '').replace("~", "&").replace("*", "~")
|
||||
@ -735,9 +641,9 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# <- NICK Global 3 1456843578 services novernet.com services.novernet.com 0 +ioS * :Global Noticer
|
||||
# & nick hopcount timestamp username hostname server service-identifier-token :realname
|
||||
# With NICKIP and VHP enabled:
|
||||
# <- NICK legacy32 2 1470699865 jlu5 localhost unreal32.midnight.vpn jlu5 +iowx hidden-1C620195 AAAAAAAAAAAAAAAAAAAAAQ== :realname
|
||||
# <- NICK GL32 2 1470699865 gl localhost unreal32.midnight.vpn GL +iowx hidden-1C620195 AAAAAAAAAAAAAAAAAAAAAQ== :realname
|
||||
# to this:
|
||||
# <- :001 UID jlu5 0 1441306929 jlu5 localhost 0018S7901 0 +iowx * hidden-1C620195 fwAAAQ== :realname
|
||||
# <- :001 UID GL 0 1441306929 gl localhost 0018S7901 0 +iowx * hidden-1C620195 fwAAAQ== :realname
|
||||
log.debug('(%s) got legacy NICK args: %s', self.name, ' '.join(args))
|
||||
|
||||
new_args = args[:] # Clone the old args list
|
||||
@ -759,15 +665,15 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
return self.handle_uid(servername, 'UID_LEGACY', new_args)
|
||||
else:
|
||||
# Normal NICK change, just let ts6_common handle it.
|
||||
# :70MAAAAAA NICK jlu5-devel 1434744242
|
||||
# :70MAAAAAA NICK GL-devel 1434744242
|
||||
return super().handle_nick(numeric, command, args)
|
||||
|
||||
def handle_mode(self, numeric, command, args):
|
||||
# <- :unreal.midnight.vpn MODE #test +bb test!*@* *!*@bad.net
|
||||
# <- :unreal.midnight.vpn MODE #test +q jlu5 1444361345
|
||||
# <- :unreal.midnight.vpn MODE #test +ntCo jlu5 1444361345
|
||||
# <- :unreal.midnight.vpn MODE #test +mntClfo 5 [10t]:5 jlu5 1444361345
|
||||
# <- :jlu5 MODE #services +v jlu5
|
||||
# <- :unreal.midnight.vpn MODE #test +q GL 1444361345
|
||||
# <- :unreal.midnight.vpn MODE #test +ntCo GL 1444361345
|
||||
# <- :unreal.midnight.vpn MODE #test +mntClfo 5 [10t]:5 GL 1444361345
|
||||
# <- :GL MODE #services +v GL
|
||||
|
||||
# This seems pretty relatively inconsistent - why do some commands have a TS at the end while others don't?
|
||||
# Answer: the first syntax (MODE sent by SERVER) is used for channel bursts - according to Unreal 3.2 docs,
|
||||
@ -856,30 +762,30 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
# which is supported by atheme and Anope 2.x).
|
||||
|
||||
# Logging in (with account info, atheme):
|
||||
# <- :NickServ SVS2MODE jlu5 +rd jlu5
|
||||
# <- :NickServ SVS2MODE GL +rd GL
|
||||
|
||||
# Logging in (without account info, anope 2.0?):
|
||||
# <- :NickServ SVS2MODE 001WCO6YK +r
|
||||
|
||||
# Logging in (without account info, anope 1.8):
|
||||
# Note: ignore the timestamp.
|
||||
# <- :services.abc.net SVS2MODE jlu5 +rd 1470696723
|
||||
# <- :services.abc.net SVS2MODE GLolol +rd 1470696723
|
||||
|
||||
# Logging out (atheme):
|
||||
# <- :NickServ SVS2MODE jlu5 -r+d 0
|
||||
# <- :NickServ SVS2MODE GL -r+d 0
|
||||
|
||||
# Logging out (anope 1.8):
|
||||
# <- :services.abc.net SVS2MODE jlu5 -r+d 1
|
||||
# <- :services.abc.net SVS2MODE GLolol -r+d 1
|
||||
|
||||
# Logging out (anope 2.0):
|
||||
# <- :NickServ SVS2MODE 009EWLA03 -r
|
||||
|
||||
# Logging in to account from a different nick (atheme):
|
||||
# Note: no +r is being set.
|
||||
# <- :NickServ SVS2MODE somenick +d jlu5
|
||||
# <- :NickServ SVS2MODE somenick +d GL
|
||||
|
||||
# Logging in to account from a different nick (anope):
|
||||
# <- :NickServ SVS2MODE 001SALZ01 +d jlu5
|
||||
# <- :NickServ SVS2MODE 001SALZ01 +d GL
|
||||
# <- :NickServ SVS2MODE 001SALZ01 +r
|
||||
|
||||
target = self._get_UID(args[0])
|
||||
@ -937,13 +843,13 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
|
||||
def handle_umode2(self, source, command, args):
|
||||
"""Handles UMODE2, used to set user modes on oneself."""
|
||||
# <- :jlu5 UMODE2 +W
|
||||
# <- :GL UMODE2 +W
|
||||
target = self._get_UID(source)
|
||||
return self._handle_umode(target, self.parse_modes(target, args))
|
||||
|
||||
def handle_topic(self, numeric, command, args):
|
||||
"""Handles the TOPIC command."""
|
||||
# <- jlu5 TOPIC #services jlu5 1444699395 :weeee
|
||||
# <- GL TOPIC #services GL 1444699395 :weeee
|
||||
# <- TOPIC #services devel.relay 1452399682 :test
|
||||
channel = args[0]
|
||||
topic = args[-1]
|
||||
@ -980,32 +886,17 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
self.users[numeric].realname = newgecos = args[0]
|
||||
return {'target': numeric, 'newgecos': newgecos}
|
||||
|
||||
def handle_chgident(self, source, command, args):
|
||||
def handle_chgident(self, numeric, command, args):
|
||||
"""Handles CHGIDENT, used for denoting ident changes."""
|
||||
# <- :jlu5 CHGIDENT jlu5 test
|
||||
# <- :GL CHGIDENT GL test
|
||||
target = self._get_UID(args[0])
|
||||
|
||||
# Bounce attempts to change fields of protected PyLink clients
|
||||
if self.is_internal_client(target):
|
||||
log.warning("(%s) Bouncing attempt from %s to change ident of PyLink client %s",
|
||||
self.name, self.get_friendly_name(source), self.get_friendly_name(target))
|
||||
self.update_client(target, 'IDENT', self.users[target].ident)
|
||||
return
|
||||
|
||||
self.users[target].ident = newident = args[1]
|
||||
return {'target': target, 'newident': newident}
|
||||
|
||||
def handle_chghost(self, source, command, args):
|
||||
def handle_chghost(self, numeric, command, args):
|
||||
"""Handles CHGHOST, used for denoting hostname changes."""
|
||||
# <- :jlu5 CHGHOST jlu5 some.host
|
||||
# <- :GL CHGHOST GL some.host
|
||||
target = self._get_UID(args[0])
|
||||
# Bounce attempts to change fields of protected PyLink clients
|
||||
if self.is_internal_client(target):
|
||||
log.warning("(%s) Bouncing attempt from %s to change host of PyLink client %s",
|
||||
self.name, self.get_friendly_name(source), self.get_friendly_name(target))
|
||||
self.update_client(target, 'HOST', self.users[target].host)
|
||||
return
|
||||
|
||||
self.users[target].host = newhost = args[1]
|
||||
|
||||
# When SETHOST or CHGHOST is used, modes +xt are implicitly set on the
|
||||
@ -1014,23 +905,16 @@ class UnrealProtocol(TS6BaseProtocol):
|
||||
|
||||
return {'target': target, 'newhost': newhost}
|
||||
|
||||
def handle_chgname(self, source, command, args):
|
||||
def handle_chgname(self, numeric, command, args):
|
||||
"""Handles CHGNAME, used for denoting real name/gecos changes."""
|
||||
# <- :jlu5 CHGNAME jlu5 :afdsafasf
|
||||
# <- :GL CHGNAME GL :afdsafasf
|
||||
target = self._get_UID(args[0])
|
||||
# Bounce attempts to change fields of protected PyLink clients
|
||||
if self.is_internal_client(target):
|
||||
log.warning("(%s) Bouncing attempt from %s to change gecos of PyLink client %s",
|
||||
self.name, self.get_friendly_name(source), self.get_friendly_name(target))
|
||||
self.update_client(target, 'REALNAME', self.users[target].realname)
|
||||
return
|
||||
|
||||
self.users[target].realname = newgecos = args[1]
|
||||
return {'target': target, 'newgecos': newgecos}
|
||||
|
||||
def handle_tsctl(self, source, command, args):
|
||||
"""Handles /TSCTL alltime requests."""
|
||||
# <- :jlu5 TSCTL alltime
|
||||
# <- :GL TSCTL alltime
|
||||
|
||||
if args[0] == 'alltime':
|
||||
self._send_with_prefix(self.sid, 'NOTICE %s :*** Server=%s time()=%d' % (source, self.hostname(), time.time()))
|
||||
|
1
pylink
1
pylink
@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
try:
|
||||
from pylinkirc import launcher
|
||||
except ImportError:
|
||||
|
@ -3,9 +3,8 @@
|
||||
Password hashing utility for PyLink IRC Services.
|
||||
"""
|
||||
|
||||
import getpass
|
||||
|
||||
from pylinkirc.coremods.login import pwd_context
|
||||
import getpass
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
@ -24,4 +23,4 @@ if __name__ == '__main__':
|
||||
|
||||
password = password.strip()
|
||||
assert password, "Password cannot be empty!"
|
||||
print(pwd_context.hash(password))
|
||||
print(pwd_context.encrypt(password))
|
||||
|
@ -1,4 +0,0 @@
|
||||
cachetools
|
||||
passlib
|
||||
pyyaml
|
||||
setuptools
|
@ -9,9 +9,6 @@ import threading
|
||||
from pylinkirc import world
|
||||
from pylinkirc.log import log
|
||||
|
||||
__all__ = ['register', 'unregister', 'start']
|
||||
|
||||
|
||||
SELECT_TIMEOUT = 0.5
|
||||
|
||||
selector = selectors.DefaultSelector()
|
||||
|
31
setup.py
31
setup.py
@ -1,16 +1,16 @@
|
||||
"""Setup module for PyLink IRC Services."""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from codecs import open
|
||||
|
||||
if sys.version_info < (3, 7):
|
||||
raise RuntimeError("PyLink requires Python 3.7 or higher.")
|
||||
if sys.version_info < (3, 4):
|
||||
raise RuntimeError("PyLink requires Python 3.4 or higher.")
|
||||
|
||||
try:
|
||||
from setuptools import setup, find_packages
|
||||
except ImportError:
|
||||
raise ImportError("Please install Setuptools and try again.")
|
||||
from codecs import open
|
||||
import subprocess
|
||||
|
||||
with open('VERSION', encoding='utf-8') as f:
|
||||
version = f.read().strip()
|
||||
@ -30,8 +30,14 @@ with open('__init__.py', 'w') as f:
|
||||
f.write('real_version = %r\n' % real_version)
|
||||
|
||||
try:
|
||||
if sys.version_info >= (3, 5):
|
||||
with open('README.md') as f:
|
||||
long_description = f.read()
|
||||
else:
|
||||
# Work around "TypeError: a bytes-like object is required, not 'str'" errors on Python 3.4
|
||||
# when the README has Unicode characters (error in distutils.util.rfc822_escape)
|
||||
import codecs
|
||||
long_description = codecs.open('README.md', encoding='utf-8').read()
|
||||
except OSError:
|
||||
print('WARNING: Failed to read readme, skipping writing long_description')
|
||||
long_description = None
|
||||
@ -70,21 +76,24 @@ setup(
|
||||
'Natural Language :: English',
|
||||
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
],
|
||||
|
||||
keywords='IRC services relay',
|
||||
install_requires=['pyyaml', 'cachetools'],
|
||||
install_requires=['pyyaml', 'ircmatch'],
|
||||
|
||||
extras_require={
|
||||
'password-hashing': ['passlib>=1.7.0'],
|
||||
'password-hashing': ['passlib'],
|
||||
'cron-support': ['psutil'],
|
||||
'relay-unicode': ['unidecode'],
|
||||
'servprotect': ['expiringdict>=1.1.4'],
|
||||
},
|
||||
|
||||
dependency_links=[
|
||||
"git+https://github.com/mailgun/expiringdict.git@v1.1.4#egg=expiringdict-1.1.4"
|
||||
],
|
||||
|
||||
# Folders (packages of code)
|
||||
packages=['pylinkirc', 'pylinkirc.protocols', 'pylinkirc.plugins', 'pylinkirc.coremods'],
|
||||
|
||||
|
@ -7,21 +7,14 @@ This module contains custom data structures that may be useful in various situat
|
||||
import collections
|
||||
import collections.abc
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import string
|
||||
import os
|
||||
import threading
|
||||
from copy import copy, deepcopy
|
||||
import string
|
||||
|
||||
from . import conf
|
||||
from .log import log
|
||||
|
||||
__all__ = ['KeyedDefaultdict', 'CopyWrapper', 'CaseInsensitiveFixedSet',
|
||||
'CaseInsensitiveDict', 'IRCCaseInsensitiveDict',
|
||||
'CaseInsensitiveSet', 'IRCCaseInsensitiveSet',
|
||||
'CamelCaseToSnakeCase', 'DataStore', 'JSONDataStore',
|
||||
'PickleDataStore']
|
||||
|
||||
from . import conf
|
||||
|
||||
_BLACKLISTED_COPY_TYPES = []
|
||||
|
||||
@ -32,7 +25,7 @@ class KeyedDefaultdict(collections.defaultdict):
|
||||
def __missing__(self, key):
|
||||
if self.default_factory is None:
|
||||
# If there is no default factory, just let defaultdict handle it
|
||||
super().__missing__(key)
|
||||
super().__missing__(self, key)
|
||||
else:
|
||||
value = self[key] = self.default_factory(key)
|
||||
return value
|
||||
@ -49,11 +42,11 @@ class CopyWrapper():
|
||||
def __deepcopy__(self, memo):
|
||||
"""Returns a deep copy of the channel object."""
|
||||
newobj = copy(self)
|
||||
#log.debug('CopyWrapper: _BLACKLISTED_COPY_TYPES = %s', _BLACKLISTED_COPY_TYPES)
|
||||
log.debug('CopyWrapper: _BLACKLISTED_COPY_TYPES = %s', _BLACKLISTED_COPY_TYPES)
|
||||
for attr, val in self.__dict__.items():
|
||||
# We can't pickle IRCNetwork, so just return a reference of it.
|
||||
if not isinstance(val, tuple(_BLACKLISTED_COPY_TYPES)):
|
||||
#log.debug('CopyWrapper: copying attr %r', attr)
|
||||
log.debug('CopyWrapper: copying attr %r', attr)
|
||||
setattr(newobj, attr, deepcopy(val))
|
||||
|
||||
memo[id(self)] = newobj
|
||||
@ -71,6 +64,7 @@ class CaseInsensitiveFixedSet(collections.abc.Set, CopyWrapper):
|
||||
|
||||
def __init__(self, *, data=None):
|
||||
if data is not None:
|
||||
assert isinstance(data, set)
|
||||
self._data = data
|
||||
else:
|
||||
self._data = set()
|
||||
@ -82,11 +76,6 @@ class CaseInsensitiveFixedSet(collections.abc.Set, CopyWrapper):
|
||||
return key.lower()
|
||||
return key
|
||||
|
||||
@classmethod
|
||||
def _from_iterable(cls, it):
|
||||
"""Returns a new iterable instance given the data in 'it'."""
|
||||
return cls(data=it)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, self._data)
|
||||
|
||||
@ -108,6 +97,7 @@ class CaseInsensitiveDict(collections.abc.MutableMapping, CaseInsensitiveFixedSe
|
||||
"""
|
||||
def __init__(self, *, data=None):
|
||||
if data is not None:
|
||||
assert isinstance(data, dict)
|
||||
self._data = data
|
||||
else:
|
||||
self._data = {}
|
||||
@ -137,10 +127,6 @@ class IRCCaseInsensitiveDict(CaseInsensitiveDict):
|
||||
return self._irc.to_lower(key)
|
||||
return key
|
||||
|
||||
def _from_iterable(self, it):
|
||||
"""Returns a new iterable instance given the data in 'it'."""
|
||||
return self.__class__(self._irc, data=it)
|
||||
|
||||
def __copy__(self):
|
||||
return self.__class__(self._irc, data=self._data.copy())
|
||||
|
||||
@ -169,10 +155,6 @@ class IRCCaseInsensitiveSet(CaseInsensitiveSet):
|
||||
return self._irc.to_lower(key)
|
||||
return key
|
||||
|
||||
def _from_iterable(self, it):
|
||||
"""Returns a new iterable instance given the data in 'it'."""
|
||||
return self.__class__(self._irc, data=it)
|
||||
|
||||
def __copy__(self):
|
||||
return self.__class__(self._irc, data=self._data.copy())
|
||||
|
||||
@ -208,12 +190,7 @@ class DataStore:
|
||||
Generic database class. Plugins should use a subclass of this such as JSONDataStore or
|
||||
PickleDataStore.
|
||||
"""
|
||||
def __init__(self, name, filename, save_frequency=None, default_db=None, data_dir=None):
|
||||
if data_dir is None:
|
||||
data_dir = conf.conf['pylink'].get('data_dir', '')
|
||||
|
||||
filename = os.path.join(data_dir, filename)
|
||||
|
||||
def __init__(self, name, filename, save_frequency=None, default_db=None):
|
||||
self.name = name
|
||||
self.filename = filename
|
||||
self.tmp_filename = filename + '.tmp'
|
||||
|
@ -1 +0,0 @@
|
||||
Subproject commit 26c9dd841467b9746fb325e16292a0b7a93bc802
|
@ -1,977 +0,0 @@
|
||||
"""
|
||||
A test fixture for PyLink protocol modules.
|
||||
"""
|
||||
import time
|
||||
import unittest
|
||||
import collections
|
||||
import itertools
|
||||
from unittest.mock import patch
|
||||
|
||||
from pylinkirc import conf, world
|
||||
from pylinkirc.log import log
|
||||
from pylinkirc.classes import User, Server, Channel
|
||||
|
||||
class DummySocket():
|
||||
def __init__(self):
|
||||
#self.recv_messages = collections.deque()
|
||||
self.sent_messages = collections.deque()
|
||||
|
||||
@staticmethod
|
||||
def connect(address):
|
||||
return
|
||||
'''
|
||||
def recv(bufsize, *args):
|
||||
if self.recv_messages:
|
||||
data = self.recv_messages.popleft()
|
||||
print('<-', data)
|
||||
return data
|
||||
else:
|
||||
return None
|
||||
'''
|
||||
|
||||
def recv(self, bufsize, *args):
|
||||
raise NotImplementedError
|
||||
|
||||
def send(self, data):
|
||||
print('->', data)
|
||||
self.sent_messages.append(data)
|
||||
|
||||
class BaseProtocolTest(unittest.TestCase):
|
||||
proto_class = None
|
||||
netname = 'test'
|
||||
serverdata = conf.conf['servers'][netname]
|
||||
|
||||
def setUp(self):
|
||||
if not self.proto_class:
|
||||
raise RuntimeError("Must set target protocol module in proto_class")
|
||||
self.p = self.proto_class(self.netname)
|
||||
|
||||
# Stub connect() and the socket for now...
|
||||
self.p.connect = lambda self: None
|
||||
self.p.socket = DummySocket()
|
||||
|
||||
if self.serverdata:
|
||||
self.p.serverdata = self.serverdata
|
||||
|
||||
def _make_user(self, nick, uid, ts=None, sid=None, **kwargs):
|
||||
"""
|
||||
Creates a user for testing.
|
||||
"""
|
||||
if ts is None:
|
||||
ts = int(time.time())
|
||||
userobj = User(self.p, nick, ts, uid, sid, **kwargs)
|
||||
self.p.users[uid] = userobj
|
||||
return userobj
|
||||
|
||||
### STATEKEEPING FUNCTIONS
|
||||
|
||||
def test_nick_to_uid(self):
|
||||
self.assertEqual(self.p.nick_to_uid('TestUser'), None)
|
||||
|
||||
self._make_user('TestUser', 'testuid1')
|
||||
|
||||
self.assertEqual(self.p.nick_to_uid('TestUser'), 'testuid1')
|
||||
self.assertEqual(self.p.nick_to_uid('TestUser', multi=True), ['testuid1'])
|
||||
self.assertEqual(self.p.nick_to_uid('BestUser'), None)
|
||||
self.assertEqual(self.p.nick_to_uid('RestUser', multi=True), [])
|
||||
|
||||
self._make_user('TestUser', 'testuid2')
|
||||
self.assertEqual(self.p.nick_to_uid('TestUser', multi=True), ['testuid1', 'testuid2'])
|
||||
|
||||
def test_is_internal(self):
|
||||
self.p.servers['internalserver'] = Server(self.p, None, 'internal.server', internal=True)
|
||||
self.p.sid = 'internalserver'
|
||||
self.p.servers['externalserver'] = Server(self.p, None, 'external.server', internal=False)
|
||||
|
||||
iuser = self._make_user('someone', 'uid1', sid='internalserver')
|
||||
euser = self._make_user('sometwo', 'uid2', sid='externalserver')
|
||||
|
||||
self.assertTrue(self.p.is_internal_server('internalserver'))
|
||||
self.assertFalse(self.p.is_internal_server('externalserver'))
|
||||
self.assertTrue(self.p.is_internal_client('uid1'))
|
||||
self.assertFalse(self.p.is_internal_client('uid2'))
|
||||
|
||||
def test_is_manipulatable(self):
|
||||
self.p.servers['serv1'] = Server(self.p, None, 'myserv.local', internal=True)
|
||||
iuser = self._make_user('yes', 'uid1', sid='serv1', manipulatable=True)
|
||||
euser = self._make_user('no', 'uid2', manipulatable=False)
|
||||
|
||||
self.assertTrue(self.p.is_manipulatable_client('uid1'))
|
||||
self.assertFalse(self.p.is_manipulatable_client('uid2'))
|
||||
|
||||
def test_get_service_bot(self):
|
||||
self.assertFalse(self.p.get_service_bot('nonexistent'))
|
||||
|
||||
regular_user = self._make_user('Guest12345', 'Guest12345@1')
|
||||
service_user = self._make_user('MyServ', 'MyServ@2')
|
||||
service_user.service = 'myserv'
|
||||
|
||||
self.assertFalse(self.p.get_service_bot('Guest12345@1'))
|
||||
|
||||
with patch.dict(world.services, {'myserv': 'myserv instance'}, clear=True):
|
||||
self.assertEqual(self.p.get_service_bot('MyServ@2'), 'myserv instance')
|
||||
|
||||
def test_to_lower(self):
|
||||
check = lambda inp, expected: self.assertEqual(self.p.to_lower(inp), expected)
|
||||
check_unchanged = lambda inp: self.assertEqual(self.p.to_lower(inp), inp)
|
||||
|
||||
check('BLAH!', 'blah!')
|
||||
check('BLAH!', 'blah!') # since we memoize
|
||||
check_unchanged('zabcdefghijklmnopqrstuvwxy')
|
||||
check('123Xyz !@#$%^&*()-=+', '123xyz !@#$%^&*()-=+')
|
||||
|
||||
if self.p.casemapping == 'rfc1459':
|
||||
check('hello [] {} |\\ ~^', 'hello [] [] \\\\ ^^')
|
||||
check('{Test Case}', '[test case]')
|
||||
else:
|
||||
check_unchanged('hello [] {} |\\ ~^')
|
||||
check('{Test Case}', '{test case}')
|
||||
|
||||
def test_is_nick(self):
|
||||
assertT = lambda inp: self.assertTrue(self.p.is_nick(inp))
|
||||
assertF = lambda inp: self.assertFalse(self.p.is_nick(inp))
|
||||
|
||||
assertT('test')
|
||||
assertT('PyLink')
|
||||
assertT('[bracketman]')
|
||||
assertT('{RACKETman}')
|
||||
assertT('bar|tender')
|
||||
assertT('\\bar|bender\\')
|
||||
assertT('jlu5|ovd')
|
||||
assertT('B')
|
||||
assertT('`')
|
||||
assertT('Hello123')
|
||||
assertT('test-')
|
||||
assertT('test-test')
|
||||
assertT('_jkl9')
|
||||
assertT('_-jkl9')
|
||||
|
||||
assertF('')
|
||||
assertF('?')
|
||||
assertF('nick@network')
|
||||
assertF('Space flight')
|
||||
assertF(' ')
|
||||
assertF(' Space blight')
|
||||
assertF('0')
|
||||
assertF('-test')
|
||||
assertF('#lounge')
|
||||
assertF('\\bar/bender\\')
|
||||
assertF('jlu5/ovd') # Technically not valid, but some IRCds don't care ;)
|
||||
assertF('100AAAAAC') # TS6 UID
|
||||
|
||||
self.assertFalse(self.p.is_nick('longnicklongnicklongnicklongnicklongnicklongnick', nicklen=20))
|
||||
self.assertTrue(self.p.is_nick('ninechars', nicklen=9))
|
||||
self.assertTrue(self.p.is_nick('ChanServ', nicklen=20))
|
||||
self.assertTrue(self.p.is_nick('leneight', nicklen=9))
|
||||
self.assertFalse(self.p.is_nick('bitmonster', nicklen=9))
|
||||
self.assertFalse(self.p.is_nick('ninecharsplus', nicklen=12))
|
||||
|
||||
def test_is_channel(self):
|
||||
assertT = lambda inp: self.assertTrue(self.p.is_channel(inp))
|
||||
assertF = lambda inp: self.assertFalse(self.p.is_channel(inp))
|
||||
|
||||
assertT('#test')
|
||||
assertT('#')
|
||||
assertT('#a#b#c')
|
||||
|
||||
assertF('nick!user@host')
|
||||
assertF('&channel') # we don't support these yet
|
||||
assertF('lorem ipsum')
|
||||
|
||||
def test_is_server_name(self):
|
||||
self.assertTrue(self.p.is_server_name('test.local'))
|
||||
self.assertTrue(self.p.is_server_name('IRC.example.com'))
|
||||
self.assertTrue(self.p.is_server_name('services.'))
|
||||
self.assertFalse(self.p.is_server_name('.org'))
|
||||
self.assertFalse(self.p.is_server_name('bacon'))
|
||||
|
||||
def test_is_hostmask(self):
|
||||
assertT = lambda inp: self.assertTrue(self.p.is_hostmask(inp))
|
||||
assertF = lambda inp: self.assertFalse(self.p.is_hostmask(inp))
|
||||
|
||||
assertT('nick!user@host')
|
||||
assertT('abc123!~ident@ip1-2-3-4.example.net')
|
||||
|
||||
assertF('brick!user')
|
||||
assertF('user@host')
|
||||
assertF('!@')
|
||||
assertF('!')
|
||||
assertF('@abcd')
|
||||
assertF('#channel')
|
||||
assertF('test.host')
|
||||
assertF('nick ! user @ host')
|
||||
assertF('alpha!beta@example.net#otherchan') # Janus workaround
|
||||
|
||||
def test_get_SID(self):
|
||||
self.p.servers['serv1'] = Server(self.p, None, 'myserv.local', internal=True)
|
||||
|
||||
check = lambda inp, expected: self.assertEqual(self.p._get_SID(inp), expected)
|
||||
check('myserv.local', 'serv1')
|
||||
check('MYSERV.local', 'serv1')
|
||||
check('serv1', 'serv1')
|
||||
check('other.server', 'other.server')
|
||||
|
||||
def test_get_UID(self):
|
||||
u = self._make_user('you', uid='100')
|
||||
check = lambda inp, expected: self.assertEqual(self.p._get_UID(inp), expected)
|
||||
|
||||
check('you', '100') # nick to UID
|
||||
check('YOu', '100')
|
||||
check('100', '100') # already a UID
|
||||
check('Test', 'Test') # non-existent
|
||||
|
||||
def test_get_hostmask(self):
|
||||
u = self._make_user('lorem', 'testUID', ident='ipsum', host='sit.amet')
|
||||
self.assertEqual(self.p.get_hostmask(u.uid), 'lorem!ipsum@sit.amet')
|
||||
|
||||
def test_get_friendly_name(self):
|
||||
u = self._make_user('lorem', 'testUID', ident='ipsum', host='sit.amet')
|
||||
s = self.p.servers['mySID'] = Server(self.p, None, 'irc.example.org')
|
||||
c = self.p._channels['#abc'] = Channel('#abc')
|
||||
|
||||
self.assertEqual(self.p.get_friendly_name(u.uid), 'lorem')
|
||||
self.assertEqual(self.p.get_friendly_name('#abc'), '#abc')
|
||||
self.assertEqual(self.p.get_friendly_name('mySID'), 'irc.example.org')
|
||||
|
||||
# TODO: _squit wrapper
|
||||
|
||||
### MISC UTILS
|
||||
def test_get_service_option(self):
|
||||
f = self.p.get_service_option
|
||||
self.assertEqual(f('myserv', 'myopt'), None) # No value anywhere
|
||||
self.assertEqual(f('myserv', 'myopt', default=0), 0)
|
||||
|
||||
# Define global option
|
||||
with patch.dict(conf.conf, {'myserv': {'youropt': True, 'myopt': 1234}}):
|
||||
self.assertEqual(f('myserv', 'myopt'), 1234) # Read global option
|
||||
|
||||
# Both global and local options exist
|
||||
with patch.dict(self.p.serverdata, {'myserv_myopt': 2345}):
|
||||
self.assertEqual(f('myserv', 'myopt'), 2345) # Read local option
|
||||
self.assertEqual(f('myserv', 'myopt', global_option='youropt'), 2345)
|
||||
|
||||
# Custom global_option setting
|
||||
self.assertEqual(f('myserv', 'abcdef', global_option='youropt'), True)
|
||||
|
||||
# Define local option
|
||||
with patch.dict(self.p.serverdata, {'myserv_myopt': 998877}):
|
||||
self.assertEqual(f('myserv', 'myopt'), 998877) # Read local option
|
||||
self.assertEqual(f('myserv', 'myopt', default='unused'), 998877)
|
||||
|
||||
def test_get_service_options_list(self):
|
||||
f = self.p.get_service_options
|
||||
self.assertEqual(f('myserv', 'items', list), []) # No value anywhere
|
||||
|
||||
# Define global option
|
||||
with patch.dict(conf.conf, {'myserv': {'items': [1, 10, 100], 'empty': []}}):
|
||||
self.assertEqual(f('myserv', 'items', list), [1, 10, 100]) # Global value only
|
||||
self.assertEqual(f('myserv', 'empty', list), [])
|
||||
|
||||
# Both global and local options exist
|
||||
with patch.dict(self.p.serverdata, {'myserv_items': [2, 4, 6, 8], 'empty': []}):
|
||||
self.assertEqual(f('myserv', 'items', list), [1, 10, 100, 2, 4, 6, 8])
|
||||
# Custom global_option setting
|
||||
self.assertEqual(f('myserv', 'items', list, global_option='nonexistent'), [2, 4, 6, 8])
|
||||
self.assertEqual(f('myserv', 'empty', list), [])
|
||||
|
||||
# Define local option
|
||||
with patch.dict(self.p.serverdata, {'myserv_items': [1, 0, 0, 3]}):
|
||||
self.assertEqual(f('myserv', 'items', list), [1, 0, 0, 3]) # Read local option
|
||||
|
||||
def test_get_service_options_dict(self):
|
||||
f = self.p.get_service_options
|
||||
self.assertEqual(f('chanman', 'items', dict), {}) # No value anywhere
|
||||
|
||||
# This is just mildly relevant test data, it's not actually used anywhere.
|
||||
globalopt = {'o': '@', 'v': '+', 'a': '!', 'h': '%'}
|
||||
localopt = {'a': '&', 'q': '~'}
|
||||
# Define global option
|
||||
with patch.dict(conf.conf, {'chanman': {'prefixes': globalopt, 'empty': {}}}):
|
||||
self.assertEqual(f('chanman', 'prefixes', dict), globalopt) # Global value only
|
||||
self.assertEqual(f('chanman', 'empty', dict), {})
|
||||
|
||||
# Both global and local options exist
|
||||
with patch.dict(self.p.serverdata, {'chanman_prefixes': localopt, 'empty': {}}):
|
||||
self.assertEqual(f('chanman', 'prefixes', dict), {**globalopt, **localopt})
|
||||
|
||||
self.assertEqual(f('chanman', 'items', dict), {}) # No value anywhere
|
||||
self.assertEqual(f('chanman', 'empty', dict), {})
|
||||
|
||||
# Define local option
|
||||
with patch.dict(self.p.serverdata, {'chanman_prefixes': localopt}):
|
||||
self.assertEqual(f('chanman', 'prefixes', dict), localopt) # Read local option
|
||||
|
||||
### MODE HANDLING
|
||||
def test_parse_modes_channel_rfc(self):
|
||||
# These are basic tests that only use RFC 1459 defined modes.
|
||||
# IRCds supporting more complex modes can define new test cases if needed.
|
||||
u = self._make_user('testuser', uid='100')
|
||||
|
||||
c = self.p.channels['#testruns'] = Channel(self.p, name='#testruns')
|
||||
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+m']),
|
||||
[('+m', None)]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+l', '3']),
|
||||
[('+l', '3')]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+ntl', '59']),
|
||||
[('+n', None), ('+t', None), ('+l', '59')]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+k-n', 'test']),
|
||||
[('+k', 'test'), ('-n', None)]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+o', '102']), # unknown UID
|
||||
[]
|
||||
)
|
||||
|
||||
c.users.add(u)
|
||||
u.channels.add(c)
|
||||
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+o', '100']),
|
||||
[('+o', '100')]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+vip', '100']),
|
||||
[('+v', '100'), ('+i', None), ('+p', None)]
|
||||
)
|
||||
|
||||
def test_parse_modes_channel_rfc2(self):
|
||||
# These are basic tests that only use RFC 1459 defined modes.
|
||||
# IRCds supporting more complex modes can define new test cases if needed.
|
||||
c = self.p.channels['#testruns'] = Channel(self.p, name='#testruns')
|
||||
|
||||
# Note: base case is not defined and raises AssertionError
|
||||
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+m']), # add modes
|
||||
[('+m', None)]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['-tn']), # remove modes
|
||||
[('-t', None), ('-n', None)]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#TESTRUNS', ['-tn']), # different case target
|
||||
[('-t', None), ('-n', None)]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+l', '3']), # modes w/ arguments
|
||||
[('+l', '3')]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+nlt', '59']), # combination
|
||||
[('+n', None), ('+l', '59'), ('+t', None)]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+k-n', 'test']), # swapping +/-
|
||||
[('+k', 'test'), ('-n', None)]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['n-s']), # sloppy syntax
|
||||
[('+n', None), ('-s', None)]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+bmi', '*!test@example.com']),
|
||||
[('+b', '*!test@example.com'), ('+m', None), ('+i', None)]
|
||||
)
|
||||
|
||||
def test_parse_modes_prefixmodes_rfc(self):
|
||||
c = self.p.channels['#testruns'] = Channel(self.p, name='#testruns')
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+ov', '102', '101']), # unknown UIDs are ignored
|
||||
[]
|
||||
)
|
||||
u = self._make_user('test100', uid='100')
|
||||
c.users.add(u)
|
||||
u.channels.add(c)
|
||||
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+o', '100']),
|
||||
[('+o', '100')]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+vip', '100']),
|
||||
[('+v', '100'), ('+i', None), ('+p', None)]
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['-o+bn', '100', '*!test@example.com']),
|
||||
[('-o', '100'), ('+b', '*!test@example.com'), ('+n', None)]
|
||||
)
|
||||
self.assertEqual(
|
||||
# 2nd user missing
|
||||
self.p.parse_modes('#testruns', ['+oovv', '100', '102', '100', '102']),
|
||||
[('+o', '100'), ('+v', '100')]
|
||||
)
|
||||
|
||||
u2 = self._make_user('test102', uid='102')
|
||||
c.users.add(u2)
|
||||
u2.channels.add(c)
|
||||
|
||||
self.assertEqual(
|
||||
# two users interleaved
|
||||
self.p.parse_modes('#testruns', ['+oovv', '100', '102', '100', '102']),
|
||||
[('+o', '100'), ('+o', '102'), ('+v', '100'), ('+v', '102')]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
# Mode cycle
|
||||
self.p.parse_modes('#testruns', ['+o-o', '100', '100']),
|
||||
[('+o', '100'), ('-o', '100')]
|
||||
)
|
||||
|
||||
def test_parse_modes_channel_ban_complex(self):
|
||||
c = self.p.channels['#testruns'] = Channel(self.p, name='#testruns')
|
||||
self.assertEqual(
|
||||
# add first ban, but don't remove second because it doesn't exist
|
||||
self.p.parse_modes('#testruns', ['+b-b', '*!*@test1', '*!*@test2']),
|
||||
[('+b', '*!*@test1')],
|
||||
"First ban should have been added, and second ignored"
|
||||
)
|
||||
self.p.apply_modes('#testruns', [('+b', '*!*@test1')])
|
||||
self.assertEqual(
|
||||
# remove first ban because it matches case
|
||||
self.p.parse_modes('#testruns', ['-b', '*!*@test1']),
|
||||
[('-b', '*!*@test1')],
|
||||
"First ban should have been removed (same case)"
|
||||
)
|
||||
self.assertEqual(
|
||||
# remove first ban despite different case
|
||||
self.p.parse_modes('#testruns', ['-b', '*!*@TEST1']),
|
||||
[('-b', '*!*@test1')],
|
||||
"First ban should have been removed (different case)"
|
||||
)
|
||||
self.p.apply_modes('#testruns', [('+b', '*!*@Test2')])
|
||||
self.assertEqual(
|
||||
# remove second ban despite different case
|
||||
self.p.parse_modes('#testruns', ['-b', '*!*@test2']),
|
||||
[('-b', '*!*@Test2')],
|
||||
"Second ban should have been removed (different case)"
|
||||
)
|
||||
|
||||
def test_parse_modes_channel_ban_cycle(self):
|
||||
c = self.p.channels['#testruns'] = Channel(self.p, name='#testruns')
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+b-b', '*!*@example.com', '*!*@example.com']),
|
||||
[('+b', '*!*@example.com'), ('-b', '*!*@example.com')],
|
||||
"Cycling a ban +b-b should remove it"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['-b+b', '*!*@example.com', '*!*@example.com']),
|
||||
[('+b', '*!*@example.com')],
|
||||
"Cycling a ban -b+b should add it"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+b-b', '*!*@example.com', '*!*@Example.com']),
|
||||
[('+b', '*!*@example.com'), ('-b', '*!*@example.com')],
|
||||
"Cycling a ban +b-b should remove it (different case)"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#testruns', ['+b-b', '*!*@Example.com', '*!*@example.com']),
|
||||
[('+b', '*!*@Example.com'), ('-b', '*!*@Example.com')],
|
||||
"Cycling a ban +b-b should remove it (different case)"
|
||||
)
|
||||
|
||||
def test_parse_modes_channel_prefixmode_has_nick(self):
|
||||
c = self.p.channels['#'] = Channel(self.p, name='#')
|
||||
u = self._make_user('mynick', uid='myuid')
|
||||
c.users.add(u)
|
||||
u.channels.add(c)
|
||||
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#', ['+o', 'myuid']),
|
||||
[('+o', 'myuid')],
|
||||
"+o on UID should be registered"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#', ['+o', 'mynick']),
|
||||
[('+o', 'myuid')],
|
||||
"+o on nick should be registered"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#', ['+o', 'MyUiD']),
|
||||
[],
|
||||
"+o on wrong case UID should be ignored"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#', ['+o', 'MyNick']),
|
||||
[('+o', 'myuid')],
|
||||
"+o on different case nick should be registered"
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#', ['-o', 'myuid']),
|
||||
[('-o', 'myuid')],
|
||||
"-o on UID should be registered"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('#', ['-o', 'mynick']),
|
||||
[('-o', 'myuid')],
|
||||
"-o on nick should be registered"
|
||||
)
|
||||
|
||||
# TODO: check the output of parse_modes() here too
|
||||
def test_parse_modes_channel_key(self):
|
||||
c = self.p.channels['#pylink'] = Channel(self.p, name='#pylink')
|
||||
c.modes = {('k', 'foobar')}
|
||||
|
||||
modes = self.p.parse_modes('#pylink', ['-k', 'foobar'])
|
||||
self.assertEqual(modes, [('-k', 'foobar')], "Parse result should include -k")
|
||||
|
||||
modes = self.p.parse_modes('#pylink', ['-k', 'aBcDeF'])
|
||||
self.assertEqual(modes, [], "Incorrect key removal should be ignored")
|
||||
|
||||
modes = self.p.parse_modes('#pylink', ['+k', 'aBcDeF'])
|
||||
self.assertEqual(modes, [('+k', 'aBcDeF')], "Parse result should include +k (replace key)")
|
||||
|
||||
# Mismatched case - treat this as remove
|
||||
# Some IRCds allow this (Unreal, P10), others do not (InspIRCd). However, if such a message
|
||||
# makes actually its way to us it is most likely valid.
|
||||
# Note: Charybdis and ngIRCd do -k * removal instead so this case will never happen there
|
||||
modes = self.p.parse_modes('#pylink', ['-k', 'FooBar'])
|
||||
self.assertEqual(modes, [('-k', 'foobar')], "Parse result should include -k (different case)")
|
||||
|
||||
modes = self.p.parse_modes('#pylink', ['-k', '*'])
|
||||
self.assertEqual(modes, [('-k', 'foobar')], "Parse result should include -k (-k *)")
|
||||
|
||||
def test_parse_modes_user_rfc(self):
|
||||
u = self._make_user('testuser', uid='100')
|
||||
|
||||
self.assertEqual(
|
||||
self.p.parse_modes('100', ['+i-w+x']),
|
||||
[('+i', None), ('-w', None), ('+x', None)]
|
||||
)
|
||||
self.assertEqual(
|
||||
# Sloppy syntax, but OK
|
||||
self.p.parse_modes('100', ['wx']),
|
||||
[('+w', None), ('+x', None)]
|
||||
)
|
||||
|
||||
def test_apply_modes_channel_simple(self):
|
||||
c = self.p.channels['#'] = Channel(self.p, name='#')
|
||||
|
||||
self.p.apply_modes('#', [('+m', None)])
|
||||
self.assertEqual(c.modes, {('m', None)})
|
||||
|
||||
self.p.apply_modes('#', []) # No-op
|
||||
self.assertEqual(c.modes, {('m', None)})
|
||||
|
||||
self.p.apply_modes('#', [('-m', None)])
|
||||
self.assertFalse(c.modes) # assert is empty
|
||||
|
||||
def test_apply_modes_channel_add_remove(self):
|
||||
c = self.p.channels['#'] = Channel(self.p, name='#')
|
||||
|
||||
self.p.apply_modes('#', [('+i', None)])
|
||||
self.assertEqual(c.modes, {('i', None)})
|
||||
|
||||
self.p.apply_modes('#', [('+n', None), ('-i', None)])
|
||||
self.assertEqual(c.modes, {('n', None)})
|
||||
|
||||
c = self.p.channels['#Magic'] = Channel(self.p, name='#Magic')
|
||||
self.p.apply_modes('#Magic', [('+m', None), ('+n', None), ('+i', None)])
|
||||
self.assertEqual(c.modes, {('m', None), ('n', None), ('i', None)}, "Modes should be added")
|
||||
|
||||
self.p.apply_modes('#Magic', [('-i', None), ('-n', None)])
|
||||
self.assertEqual(c.modes, {('m', None)}, "Modes should be removed")
|
||||
|
||||
def test_apply_modes_channel_remove_nonexistent(self):
|
||||
c = self.p.channels['#abc'] = Channel(self.p, name='#abc')
|
||||
self.p.apply_modes('#abc', [('+t', None)])
|
||||
self.assertEqual(c.modes, {('t', None)})
|
||||
|
||||
self.p.apply_modes('#abc', [('-n', None), ('-i', None)])
|
||||
self.assertEqual(c.modes, {('t', None)})
|
||||
|
||||
def test_apply_modes_channel_limit(self):
|
||||
c = self.p.channels['#abc'] = Channel(self.p, name='#abc')
|
||||
self.p.apply_modes('#abc', [('+t', None), ('+l', '30')])
|
||||
self.assertEqual(c.modes, {('t', None), ('l', '30')})
|
||||
|
||||
self.p.apply_modes('#abc', [('+l', '55')])
|
||||
self.assertEqual(c.modes, {('t', None), ('l', '55')})
|
||||
|
||||
self.p.apply_modes('#abc', [('-l', None)])
|
||||
self.assertEqual(c.modes, {('t', None)})
|
||||
|
||||
def test_apply_modes_channel_key(self):
|
||||
c = self.p.channels['#pylink'] = Channel(self.p, name='#pylink')
|
||||
self.p.apply_modes('#pylink', [('+k', 'password123'), ('+s', None)])
|
||||
self.assertEqual(c.modes, {('s', None), ('k', 'password123')})
|
||||
|
||||
self.p.apply_modes('#pylink', [('+k', 'qwerty')])
|
||||
self.assertEqual(c.modes, {('s', None), ('k', 'qwerty')})
|
||||
|
||||
self.p.apply_modes('#pylink', [('-k', 'abcdef')])
|
||||
# Trying to remove with wrong key is a no-op
|
||||
self.assertEqual(c.modes, {('s', None), ('k', 'qwerty')})
|
||||
|
||||
self.p.apply_modes('#pylink', [('-k', 'qwerty')])
|
||||
self.assertEqual(c.modes, {('s', None)})
|
||||
|
||||
self.p.apply_modes('#pylink', [('+k', 'qwerty')])
|
||||
self.assertEqual(c.modes, {('s', None), ('k', 'qwerty')})
|
||||
self.p.apply_modes('#pylink', [('-k', 'QWERTY')]) # Remove with different case
|
||||
self.assertEqual(c.modes, {('s', None)})
|
||||
|
||||
self.p.apply_modes('#pylink', [('+k', '12345')]) # Replace existing key
|
||||
self.assertEqual(c.modes, {('s', None), ('k', '12345')})
|
||||
|
||||
def test_apply_modes_channel_ban(self):
|
||||
c = self.p.channels['#Magic'] = Channel(self.p, name='#Magic')
|
||||
self.p.apply_modes('#Magic', [('+b', '*!*@test.host'), ('+b', '*!*@best.host')])
|
||||
self.assertEqual(c.modes, {('b', '*!*@test.host'), ('b', '*!*@best.host')}, "Bans should be added")
|
||||
|
||||
# This should be a no-op
|
||||
self.p.apply_modes('#Magic', [('-b', '*!*non-existent')])
|
||||
self.assertEqual(c.modes, {('b', '*!*@test.host'), ('b', '*!*@best.host')}, "Trying to unset non-existent ban should be no-op")
|
||||
|
||||
# Simple removal
|
||||
self.p.apply_modes('#Magic', [('-b', '*!*@test.host')])
|
||||
self.assertEqual(c.modes, {('b', '*!*@best.host')}, "Ban on *!*@test.host be removed (same case as original)")
|
||||
|
||||
# Removal but different case than original
|
||||
self.p.apply_modes('#Magic', [('-b', '*!*@BEST.HOST')])
|
||||
self.assertFalse(c.modes, "Ban on *!*@best.host should be removed (different case)")
|
||||
|
||||
def test_apply_modes_channel_ban_multiple(self):
|
||||
c = self.p.channels['#Magic'] = Channel(self.p, name='#Magic')
|
||||
self.p.apply_modes('#Magic', [('+b', '*!*@test.host'), ('+b', '*!*@best.host'), ('+b', '*!*@guest.host')])
|
||||
self.assertEqual(c.modes, {('b', '*!*@test.host'), ('b', '*!*@best.host'), ('b', '*!*@guest.host')},
|
||||
"Bans should be added")
|
||||
|
||||
self.p.apply_modes('#Magic', [('-b', '*!*@best.host'), ('-b', '*!*@guest.host'), ('-b', '*!*@test.host')])
|
||||
self.assertEqual(c.modes, set(), "Bans should be removed")
|
||||
|
||||
def test_apply_modes_channel_mode_cycle(self):
|
||||
c = self.p.channels['#Magic'] = Channel(self.p, name='#Magic')
|
||||
self.p.apply_modes('#Magic', [('+b', '*!*@example.net'), ('-b', '*!*@example.net')])
|
||||
self.assertEqual(c.modes, set(), "Ban should have been removed (same case)")
|
||||
|
||||
self.p.apply_modes('#Magic', [('+b', '*!*@example.net'), ('-b', '*!*@Example.net')])
|
||||
self.assertEqual(c.modes, set(), "Ban should have been removed (different case)")
|
||||
|
||||
u = self._make_user('nick', uid='user')
|
||||
c.users.add(u.uid)
|
||||
u.channels.add(c)
|
||||
|
||||
self.p.apply_modes('#Magic', [('+o', 'user'), ('-o', 'user')])
|
||||
self.assertEqual(c.modes, set(), "No prefixmodes should have been set")
|
||||
self.assertFalse(c.is_op('user'))
|
||||
self.assertFalse(c.get_prefix_modes('user'))
|
||||
|
||||
def test_apply_modes_channel_prefixmodes(self):
|
||||
# Make some users
|
||||
c = self.p.channels['#staff'] = Channel(self.p, name='#staff')
|
||||
u1 = self._make_user('user100', uid='100')
|
||||
u2 = self._make_user('user101', uid='101')
|
||||
c.users.add(u1.uid)
|
||||
u1.channels.add(c)
|
||||
c.users.add(u2.uid)
|
||||
u2.channels.add(c)
|
||||
|
||||
# Set modes
|
||||
self.p.apply_modes('#staff', [('+o', '100'), ('+v', '101'), ('+t', None)])
|
||||
self.assertEqual(c.modes, {('t', None)})
|
||||
|
||||
# State checks, lots of them... TODO: move this into Channel class tests
|
||||
self.assertTrue(c.is_op('100'))
|
||||
self.assertTrue(c.is_op_plus('100'))
|
||||
self.assertFalse(c.is_halfop('100'))
|
||||
self.assertTrue(c.is_halfop_plus('100'))
|
||||
self.assertFalse(c.is_voice('100'))
|
||||
self.assertTrue(c.is_voice_plus('100'))
|
||||
self.assertEqual(c.get_prefix_modes('100'), ['op'])
|
||||
|
||||
self.assertTrue(c.is_voice('101'))
|
||||
self.assertTrue(c.is_voice_plus('101'))
|
||||
self.assertFalse(c.is_halfop('101'))
|
||||
self.assertFalse(c.is_halfop_plus('101'))
|
||||
self.assertFalse(c.is_op('101'))
|
||||
self.assertFalse(c.is_op_plus('101'))
|
||||
self.assertEqual(c.get_prefix_modes('101'), ['voice'])
|
||||
|
||||
self.assertFalse(c.prefixmodes['owner'])
|
||||
self.assertFalse(c.prefixmodes['admin'])
|
||||
self.assertEqual(c.prefixmodes['op'], {'100'})
|
||||
self.assertFalse(c.prefixmodes['halfop'])
|
||||
self.assertEqual(c.prefixmodes['voice'], {'101'})
|
||||
|
||||
self.p.apply_modes('#staff', [('-o', '100')])
|
||||
self.assertEqual(c.modes, {('t', None)})
|
||||
self.assertFalse(c.get_prefix_modes('100'))
|
||||
self.assertEqual(c.get_prefix_modes('101'), ['voice'])
|
||||
|
||||
def test_apply_modes_user(self):
|
||||
u = self._make_user('nick', uid='user')
|
||||
self.p.apply_modes('user', [('+o', None), ('+w', None)])
|
||||
self.assertEqual(u.modes, {('o', None), ('w', None)})
|
||||
self.p.apply_modes('user', [('-o', None), ('+i', None)])
|
||||
self.assertEqual(u.modes, {('i', None), ('w', None)})
|
||||
|
||||
def test_reverse_modes_simple(self):
|
||||
c = self.p.channels['#foobar'] = Channel(self.p, name='#foobar')
|
||||
c.modes = {('m', None), ('n', None)}
|
||||
|
||||
# This function supports both strings and mode lists
|
||||
|
||||
# Base cases
|
||||
for inp in {'', '+', '-'}:
|
||||
self.assertEqual(self.p.reverse_modes('#foobar', inp), '+')
|
||||
out = self.p.reverse_modes('#foobar', [])
|
||||
self.assertEqual(out, [])
|
||||
|
||||
# One simple
|
||||
out = self.p.reverse_modes('#foobar', '+t')
|
||||
self.assertEqual(out, '-t')
|
||||
out = self.p.reverse_modes('#foobar', [('+t', None)])
|
||||
self.assertEqual(out, [('-t', None)])
|
||||
|
||||
out = self.p.reverse_modes('#foobar', '+m')
|
||||
self.assertEqual(out, '+', 'Calling reverse_modes() on an already set mode is a no-op')
|
||||
|
||||
out = self.p.reverse_modes('#foobar', [('+m', None)])
|
||||
self.assertEqual(out, [], 'Calling reverse_modes() on an already set mode is a no-op')
|
||||
|
||||
out = self.p.reverse_modes('#foobar', [('-i', None)])
|
||||
self.assertEqual(out, [], 'Calling reverse_modes() on non-existent mode is a no-op')
|
||||
|
||||
def test_reverse_modes_multi(self):
|
||||
c = self.p.channels['#foobar'] = Channel(self.p, name='#foobar')
|
||||
c.modes = {('t', None), ('n', None)}
|
||||
|
||||
# reverse_modes should ignore modes that were already set
|
||||
out = self.p.reverse_modes('#foobar', '+nt')
|
||||
self.assertEqual(out, '+')
|
||||
|
||||
out = self.p.reverse_modes('#foobar', [('+m', None), ('+i', None)])
|
||||
self.assertEqual(out, [('-m', None), ('-i', None)])
|
||||
|
||||
out = self.p.reverse_modes('#foobar', '+mint')
|
||||
self.assertEqual(out, '-mi') # Ignore +nt since it already exists
|
||||
|
||||
out = self.p.reverse_modes('#foobar', '+m-t')
|
||||
self.assertEqual(out, '-m+t')
|
||||
|
||||
out = self.p.reverse_modes('#foobar', '+mn-t')
|
||||
self.assertEqual(out, '-m+t')
|
||||
|
||||
out = self.p.reverse_modes('#foobar', [('+m', None), ('+n', None), ('-t', None)])
|
||||
self.assertEqual(out, [('-m', None), ('+t', None)])
|
||||
|
||||
def test_reverse_modes_bans(self):
|
||||
c = self.p.channels['#foobar'] = Channel(self.p, name='#foobar')
|
||||
c.modes = {('t', None), ('n', None), ('b', '*!*@example.com')}
|
||||
|
||||
out = self.p.reverse_modes('#foobar', [('+b', '*!*@test')])
|
||||
self.assertEqual(out, [('-b', '*!*@test')], "non-existent ban should be removed")
|
||||
out = self.p.reverse_modes('#foobar', '+b *!*@example.com')
|
||||
self.assertEqual(out, '+', "+b existing ban should be no-op")
|
||||
|
||||
out = self.p.reverse_modes('#foobar', '+b *!*@Example.com')
|
||||
self.assertEqual(out, '+', "Should ignore attempt to change case of ban mode")
|
||||
|
||||
out = self.p.reverse_modes('#foobar', '-b *!*@example.com')
|
||||
self.assertEqual(out, '+b *!*@example.com', "-b existing ban should reset it")
|
||||
|
||||
out = self.p.reverse_modes('#foobar', '-b *!*@Example.com')
|
||||
self.assertEqual(out, '+b *!*@example.com', "-b existing ban should reset it using original case")
|
||||
|
||||
out = self.p.reverse_modes('#foobar', '-b *!*@*')
|
||||
self.assertEqual(out, '+', "Removing non-existent ban is no-op")
|
||||
|
||||
out = self.p.reverse_modes('#foobar', '+bbbm 1 2 3')
|
||||
self.assertEqual(out, '-bbbm 1 2 3')
|
||||
|
||||
def test_reverse_modes_limit(self):
|
||||
c = self.p.channels['#foobar'] = Channel(self.p, name='#foobar')
|
||||
c.modes = {('t', None), ('n', None), ('l', '50')}
|
||||
|
||||
out = self.p.reverse_modes('#foobar', [('+l', '100')])
|
||||
self.assertEqual(out, [('+l', '50')], "Setting +l should reset original mode")
|
||||
|
||||
out = self.p.reverse_modes('#foobar', [('-l', None)])
|
||||
self.assertEqual(out, [('+l', '50')], "Unsetting +l should reset original mode")
|
||||
|
||||
out = self.p.reverse_modes('#foobar', [('+l', '50')])
|
||||
self.assertEqual(out, [], "Setting +l with original value is no-op")
|
||||
|
||||
c.modes.clear()
|
||||
out = self.p.reverse_modes('#foobar', [('+l', '111'), ('+m', None)])
|
||||
self.assertEqual(out, [('-l', None), ('-m', None)], "Setting +l on channel without it should remove")
|
||||
|
||||
def test_reverse_modes_prefixmodes(self):
|
||||
c = self.p.channels['#foobar'] = Channel(self.p, name='#foobar')
|
||||
c.modes = {('t', None), ('n', None)}
|
||||
u = self._make_user('nick', uid='user')
|
||||
u.channels.add(c)
|
||||
c.users.add(u)
|
||||
c.prefixmodes['op'].add(u.uid)
|
||||
|
||||
out = self.p.reverse_modes('#foobar', '-o user')
|
||||
self.assertEqual(out, '+o user')
|
||||
out = self.p.reverse_modes('#foobar', '+o user')
|
||||
self.assertEqual(out, '+')
|
||||
out = self.p.reverse_modes('#foobar', '+ov user user')
|
||||
self.assertEqual(out, '-v user') # ignore +o
|
||||
out = self.p.reverse_modes('#foobar', '-ovt user user')
|
||||
self.assertEqual(out, '+ot user') # ignore -v
|
||||
|
||||
def test_reverse_modes_cycle_simple(self):
|
||||
c = self.p.channels['#weirdstuff'] = Channel(self.p, name='#weirdstuff')
|
||||
c.modes = {('t', None), ('n', None)}
|
||||
|
||||
out = self.p.reverse_modes('#weirdstuff', '+n-n') # +- cycle existing mode
|
||||
self.assertEqual(out, '+n')
|
||||
out = self.p.reverse_modes('#weirdstuff', '-n+n') # -+ cycle existing mode
|
||||
self.assertEqual(out, '+n') # Ugly but OK
|
||||
|
||||
out = self.p.reverse_modes('#weirdstuff', '+m-m') # +- cycle non-existent mode
|
||||
self.assertEqual(out, '-m')
|
||||
out = self.p.reverse_modes('#weirdstuff', '-m+m') # -+ cycle non-existent mode
|
||||
self.assertEqual(out, '-m') # Ugly but OK
|
||||
|
||||
def test_reverse_modes_cycle_bans(self):
|
||||
c = self.p.channels['#weirdstuff'] = Channel(self.p, name='#weirdstuff')
|
||||
c.modes = {('t', None), ('n', None), ('b', '*!*@test.host')}
|
||||
|
||||
out = self.p.reverse_modes('#weirdstuff', '+b-b *!*@test.host *!*@test.host') # +- cycle existing ban
|
||||
self.assertEqual(out, '+b *!*@test.host')
|
||||
out = self.p.reverse_modes('#weirdstuff', '-b+b *!*@test.host *!*@test.host') # -+ cycle existing ban
|
||||
self.assertEqual(out, '+b *!*@test.host') # Ugly but OK
|
||||
|
||||
out = self.p.reverse_modes('#weirdstuff', '+b-b *!*@* *!*@*') # +- cycle existing ban
|
||||
self.assertEqual(out, '-b *!*@*')
|
||||
out = self.p.reverse_modes('#weirdstuff', '-b+b *!*@* *!*@*') # -+ cycle existing ban
|
||||
self.assertEqual(out, '-b *!*@*') # Ugly but OK
|
||||
|
||||
def test_reverse_modes_cycle_arguments(self):
|
||||
# All of these cases are ugly, sometimes unsetting modes that don't exist...
|
||||
c = self.p.channels['#weirdstuff'] = Channel(self.p, name='#weirdstuff')
|
||||
|
||||
out = self.p.reverse_modes('#weirdstuff', '+l-l 30')
|
||||
self.assertEqual(out, '-l')
|
||||
out = self.p.reverse_modes('#weirdstuff', '-l+l 30')
|
||||
self.assertEqual(out, '-l')
|
||||
|
||||
out = self.p.reverse_modes('#weirdstuff', '+k-k aaaaaaaaaaaa aaaaaaaaaaaa')
|
||||
self.assertEqual(out, '-k aaaaaaaaaaaa')
|
||||
out = self.p.reverse_modes('#weirdstuff', '-k+k aaaaaaaaaaaa aaaaaaaaaaaa')
|
||||
self.assertEqual(out, '-k aaaaaaaaaaaa')
|
||||
|
||||
c.modes = {('l', '555'), ('k', 'NO-PLEASE')}
|
||||
out = self.p.reverse_modes('#weirdstuff', '+l-l 30')
|
||||
self.assertEqual(out, '+l 555')
|
||||
out = self.p.reverse_modes('#weirdstuff', '-l+l 30')
|
||||
self.assertEqual(out, '+l 555')
|
||||
|
||||
out = self.p.reverse_modes('#weirdstuff', '+k-k aaaaaaaaaaaa aaaaaaaaaaaa')
|
||||
self.assertEqual(out, '+k NO-PLEASE')
|
||||
out = self.p.reverse_modes('#weirdstuff', '-k+k aaaaaaaaaaaa aaaaaaaaaaaa')
|
||||
self.assertEqual(out, '+k NO-PLEASE')
|
||||
|
||||
def test_reverse_modes_cycle_prefixmodes(self):
|
||||
# All of these cases are ugly, sometimes unsetting modes that don't exist...
|
||||
c = self.p.channels['#weirdstuff'] = Channel(self.p, name='#weirdstuff')
|
||||
u = self._make_user('nick', uid='user')
|
||||
u.channels.add(c)
|
||||
c.users.add(u)
|
||||
|
||||
# user not already opped
|
||||
out = self.p.reverse_modes('#weirdstuff', '+o-o user user')
|
||||
self.assertEqual(out, '-o user')
|
||||
out = self.p.reverse_modes('#weirdstuff', '-o+o user user')
|
||||
self.assertEqual(out, '-o user')
|
||||
|
||||
c.prefixmodes['op'].add(u.uid)
|
||||
|
||||
# user was opped
|
||||
out = self.p.reverse_modes('#weirdstuff', '+o-o user user')
|
||||
self.assertEqual(out, '+o user')
|
||||
out = self.p.reverse_modes('#weirdstuff', '-o+o user user')
|
||||
self.assertEqual(out, '+o user')
|
||||
|
||||
def test_join_modes(self):
|
||||
# join_modes operates independently of state; the input just has to be valid modepairs
|
||||
check = lambda inp, expected, sort=False: self.assertEqual(self.p.join_modes(inp, sort=sort), expected)
|
||||
|
||||
check([], '+') # base case
|
||||
|
||||
check([('+b', '*!*@test')], '+b *!*@test')
|
||||
check([('-S', None)], '-S')
|
||||
check([('+n', None), ('+t', None)], '+nt')
|
||||
check([('+t', None), ('+n', None)], '+tn')
|
||||
check([('+t', None), ('+n', None)], '+nt', sort=True)
|
||||
|
||||
check([('-n', None), ('-s', None)], '-ns')
|
||||
|
||||
check([('+q', '*'), ('-q', '*')], '+q-q * *')
|
||||
check([('+l', '5'), ('-n', None), ('+R', None)], '+l-n+R 5')
|
||||
|
||||
# Sloppy syntax: assume leading mode is + if not otherwise stated
|
||||
check([('o', '100AAAAAC'), ('m', None), ('-v', '100AAAAAC')], '+om-v 100AAAAAC 100AAAAAC')
|
||||
|
||||
def test_wrap_modes_small(self):
|
||||
# wrap_modes is also state independent; it only calls join_modes
|
||||
|
||||
N_USERS = 5
|
||||
modes = [('+m', None)] + [('-b', 'user%s!*@example.org' % num) for num in range(N_USERS)]
|
||||
wr = self.p.wrap_modes(modes, 200)
|
||||
log.debug('wrap_modes input: %s', modes)
|
||||
log.debug('wrap_modes output: %s', wr)
|
||||
|
||||
self.assertEqual(len(wr), 1) # no split should have occurred
|
||||
self.assertEqual(wr[0], self.p.join_modes(modes))
|
||||
|
||||
def test_wrap_modes_split_length_limit(self):
|
||||
# wrap_modes is also state independent; it only calls join_modes
|
||||
N_USERS = 50
|
||||
|
||||
modes = [('+o', 'user%s' % num) for num in range(N_USERS)]
|
||||
wr = self.p.wrap_modes(modes, 120)
|
||||
log.debug('wrap_modes input: %s', modes)
|
||||
log.debug('wrap_modes output: %s', wr)
|
||||
|
||||
self.assertTrue(len(wr) > 1) # we should have induced a split
|
||||
|
||||
for s in wr:
|
||||
# Check that each split item starts with the right mode char
|
||||
self.assertTrue(s.startswith('+oooo'))
|
||||
|
||||
all_args = itertools.chain.from_iterable(s.split() for s in wr)
|
||||
for num in range(N_USERS):
|
||||
# Check that no users are missing
|
||||
self.assertIn('user%s' % num, all_args)
|
||||
|
||||
def test_wrap_modes_split_max_modes(self):
|
||||
# wrap_modes is also state independent; it only calls join_modes
|
||||
N_USERS = 50
|
||||
N_MAX_PER_MSG = 8
|
||||
|
||||
modes = [('+v', 'user%s' % num) for num in range(N_USERS)]
|
||||
wr = self.p.wrap_modes(modes, 200, N_MAX_PER_MSG)
|
||||
log.debug('wrap_modes input: %s', modes)
|
||||
log.debug('wrap_modes output: %s', wr)
|
||||
|
||||
self.assertTrue(len(wr) > 1) # we should have induced a split
|
||||
|
||||
splits = [s.split() for s in wr]
|
||||
for s in splits:
|
||||
# Check that each message sets <= N_MAX_PER_MSG modes
|
||||
self.assertTrue(len(s[0]) <= (N_MAX_PER_MSG + 1)) # add 1 to account for leading +
|
||||
self.assertTrue(s[0].startswith('+'))
|
||||
self.assertTrue(s[0].endswith('v'))
|
||||
|
||||
all_args = itertools.chain.from_iterable(splits)
|
||||
for num in range(N_USERS):
|
||||
# Check that no users are missing
|
||||
self.assertIn('user%s' % num, all_args)
|
||||
|
||||
# TODO: test type coersion if channel or mode targets are ints
|
@ -1,118 +0,0 @@
|
||||
"""
|
||||
Runs IRC parser tests from ircdocs/parser-tests.
|
||||
|
||||
This test suite runs static code only.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
import yaml
|
||||
|
||||
PARSER_DATA_PATH = Path(__file__).parent.resolve() / 'parser-tests' / 'tests'
|
||||
print(PARSER_DATA_PATH)
|
||||
|
||||
from pylinkirc import utils
|
||||
from pylinkirc.protocols.ircs2s_common import IRCCommonProtocol
|
||||
|
||||
class MessageParserTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
with open(PARSER_DATA_PATH / 'msg-split.yaml') as f:
|
||||
cls.MESSAGE_SPLIT_TEST_DATA = yaml.safe_load(f)
|
||||
with open(PARSER_DATA_PATH / 'userhost-split.yaml') as f:
|
||||
cls.USER_HOST_SPLIT_TEST_DATA = yaml.safe_load(f)
|
||||
with open(PARSER_DATA_PATH / 'mask-match.yaml') as f:
|
||||
cls.MASK_MATCH_TEST_DATA = yaml.safe_load(f)
|
||||
with open(PARSER_DATA_PATH / 'validate-hostname.yaml') as f:
|
||||
cls.VALIDATE_HOSTNAME_TEST_DATA = yaml.safe_load(f)
|
||||
|
||||
def testMessageSplit(self):
|
||||
for testdata in self.MESSAGE_SPLIT_TEST_DATA['tests']:
|
||||
inp = testdata['input']
|
||||
atoms = testdata['atoms']
|
||||
|
||||
with self.subTest():
|
||||
expected = []
|
||||
has_source = False
|
||||
if 'source' in atoms:
|
||||
has_source = True
|
||||
expected.append(atoms['source'])
|
||||
|
||||
if 'verb' in atoms:
|
||||
expected.append(atoms['verb'])
|
||||
|
||||
if 'params' in atoms:
|
||||
expected.extend(atoms['params'])
|
||||
|
||||
if 'tags' in atoms:
|
||||
# Remove message tags before parsing
|
||||
_, inp = inp.split(" ", 1)
|
||||
|
||||
if has_source:
|
||||
parts = IRCCommonProtocol.parse_prefixed_args(inp)
|
||||
else:
|
||||
parts = IRCCommonProtocol.parse_args(inp)
|
||||
self.assertEqual(expected, parts, "Parse test failed for string: %r" % inp)
|
||||
|
||||
@unittest.skip("Not quite working yet")
|
||||
def testMessageTags(self):
|
||||
for testdata in self.MESSAGE_SPLIT_TEST_DATA['tests']:
|
||||
inp = testdata['input']
|
||||
atoms = testdata['atoms']
|
||||
|
||||
with self.subTest():
|
||||
if 'tags' in atoms:
|
||||
self.assertEqual(atoms['tags'], IRCCommonProtocol.parse_message_tags(inp.split(" ")),
|
||||
"Parse test failed for message tags: %r" % inp)
|
||||
|
||||
def testUserHostSplit(self):
|
||||
for test in self.USER_HOST_SPLIT_TEST_DATA['tests']:
|
||||
inp = test['source']
|
||||
atoms = test['atoms']
|
||||
|
||||
with self.subTest():
|
||||
if 'nick' not in atoms or 'user' not in atoms or 'host' not in atoms:
|
||||
# Trying to parse a hostmask with missing atoms is an error in split_hostmask()
|
||||
with self.assertRaises(ValueError):
|
||||
utils.split_hostmask(inp)
|
||||
else:
|
||||
expected = [atoms['nick'], atoms['user'], atoms['host']]
|
||||
self.assertEqual(expected, utils.split_hostmask(inp))
|
||||
|
||||
def testHostMatch(self):
|
||||
for test in self.MASK_MATCH_TEST_DATA['tests']:
|
||||
mask = test['mask']
|
||||
|
||||
# N.B.: utils.match_text() does Unicode case-insensitive match by default,
|
||||
# which might not be the right thing to do on IRC.
|
||||
# But irc.to_lower() isn't a static function so we're not testing it here...
|
||||
for match in test['matches']:
|
||||
with self.subTest():
|
||||
self.assertTrue(utils.match_text(mask, match))
|
||||
|
||||
for fail in test['fails']:
|
||||
with self.subTest():
|
||||
self.assertFalse(utils.match_text(mask, fail))
|
||||
|
||||
def testValidateHostname(self):
|
||||
for test in self.VALIDATE_HOSTNAME_TEST_DATA['tests']:
|
||||
with self.subTest():
|
||||
self.assertEqual(test['valid'], IRCCommonProtocol.is_server_name(test['host']),
|
||||
"Failed test for %r; should be %s" % (test['host'], test['valid']))
|
||||
|
||||
|
||||
# N.B. skipping msg-join tests because PyLink doesn't think about messages that way
|
||||
|
||||
### Custom test cases
|
||||
def testMessageSplitSpaces(self):
|
||||
# Test that tokenization ignores empty fields, but doesn't strip away other types of whitespace
|
||||
f = IRCCommonProtocol.parse_prefixed_args
|
||||
self.assertEqual(f(":foo PRIVMSG #test :message"), ["foo", "PRIVMSG", "#test", "message"])
|
||||
self.assertEqual(f(":123LOLWUT NICK cursed\u3000nickname"), ["123LOLWUT", "NICK", "cursed\u3000nickname"])
|
||||
self.assertEqual(f(":123LOLWUT MODE ## +ov \x1f checking"),
|
||||
["123LOLWUT", "MODE", "##", "+ov", "\x1f", "checking"])
|
||||
self.assertEqual(f(":123LOLWUT MODE ## +ov \u3000 checking"),
|
||||
["123LOLWUT", "MODE", "##", "+ov", "\u3000", "checking"])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,38 +0,0 @@
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
from pylinkirc.protocols import clientbot
|
||||
from pylinkirc.classes import User
|
||||
|
||||
import protocol_test_fixture as ptf
|
||||
|
||||
class ClientbotProtocolTest(ptf.BaseProtocolTest):
|
||||
proto_class = clientbot.ClientbotWrapperProtocol
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.p.pseudoclient = self._make_user('PyLink', uid='ClientbotInternal@0')
|
||||
|
||||
def test_get_UID(self):
|
||||
u_internal = self._make_user('you', uid='100')
|
||||
check = lambda inp, expected: self.assertEqual(self.p._get_UID(inp), expected)
|
||||
|
||||
# External clients are returned by the matcher
|
||||
with unittest.mock.patch.object(self.proto_class, 'is_internal_client', return_value=False) as m:
|
||||
check('you', '100') # nick to UID
|
||||
check('YOu', '100')
|
||||
check('100', '100') # already a UID
|
||||
check('Test', 'Test') # non-existent
|
||||
|
||||
# Internal clients are ignored
|
||||
with unittest.mock.patch.object(self.proto_class, 'is_internal_client', return_value=True) as m:
|
||||
check('you', 'you')
|
||||
check('YOu', 'YOu')
|
||||
check('100', '100') # already a UID
|
||||
check('Test', 'Test') # non-existent
|
||||
|
||||
|
||||
# In the future we will have protocol specific test cases here
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,13 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from pylinkirc.protocols import inspircd
|
||||
|
||||
import protocol_test_fixture as ptf
|
||||
|
||||
class InspIRCdProtocolTest(ptf.BaseProtocolTest):
|
||||
proto_class = inspircd.InspIRCdProtocol
|
||||
|
||||
# In the future we will have protocol specific test cases here
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,86 +0,0 @@
|
||||
"""
|
||||
Tests for protocols/p10
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from pylinkirc.protocols import p10
|
||||
|
||||
class P10UIDGeneratorTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.uidgen = p10.P10UIDGenerator('HI')
|
||||
|
||||
def test_initial_UID(self):
|
||||
expected = [
|
||||
"HIAAA",
|
||||
"HIAAB",
|
||||
"HIAAC",
|
||||
"HIAAD",
|
||||
"HIAAE",
|
||||
"HIAAF"
|
||||
]
|
||||
self.uidgen.counter = 0
|
||||
actual = [self.uidgen.next_uid() for i in range(6)]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_rollover_first_lowercase(self):
|
||||
expected = [
|
||||
"HIAAY",
|
||||
"HIAAZ",
|
||||
"HIAAa",
|
||||
"HIAAb",
|
||||
"HIAAc",
|
||||
"HIAAd",
|
||||
]
|
||||
self.uidgen.counter = 24
|
||||
actual = [self.uidgen.next_uid() for i in range(6)]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_rollover_first_num(self):
|
||||
expected = [
|
||||
"HIAAz",
|
||||
"HIAA0",
|
||||
"HIAA1",
|
||||
"HIAA2",
|
||||
"HIAA3",
|
||||
"HIAA4",
|
||||
]
|
||||
self.uidgen.counter = 26*2-1
|
||||
actual = [self.uidgen.next_uid() for i in range(6)]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_rollover_second(self):
|
||||
expected = [
|
||||
"HIAA8",
|
||||
"HIAA9",
|
||||
"HIAA[",
|
||||
"HIAA]",
|
||||
"HIABA",
|
||||
"HIABB",
|
||||
"HIABC",
|
||||
"HIABD",
|
||||
]
|
||||
self.uidgen.counter = 26*2+10-2
|
||||
actual = [self.uidgen.next_uid() for i in range(8)]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_rollover_third(self):
|
||||
expected = [
|
||||
"HIE]9",
|
||||
"HIE][",
|
||||
"HIE]]",
|
||||
"HIFAA",
|
||||
"HIFAB",
|
||||
"HIFAC",
|
||||
]
|
||||
self.uidgen.counter = 5*64**2 - 3
|
||||
actual = [self.uidgen.next_uid() for i in range(6)]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_overflow(self):
|
||||
self.uidgen.counter = 64**3-1
|
||||
self.assertTrue(self.uidgen.next_uid())
|
||||
self.assertRaises(RuntimeError, self.uidgen.next_uid)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,70 +0,0 @@
|
||||
"""
|
||||
Tests for protocols/ts6_common
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from pylinkirc.protocols import ts6_common
|
||||
|
||||
class TS6UIDGeneratorTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.uidgen = ts6_common.TS6UIDGenerator('123')
|
||||
|
||||
def test_initial_UID(self):
|
||||
expected = [
|
||||
"123AAAAAA",
|
||||
"123AAAAAB",
|
||||
"123AAAAAC",
|
||||
"123AAAAAD",
|
||||
"123AAAAAE",
|
||||
"123AAAAAF",
|
||||
]
|
||||
self.uidgen.counter = 0
|
||||
actual = [self.uidgen.next_uid() for i in range(6)]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_rollover_first_num(self):
|
||||
expected = [
|
||||
"123AAAAAY",
|
||||
"123AAAAAZ",
|
||||
"123AAAAA0",
|
||||
"123AAAAA1",
|
||||
"123AAAAA2",
|
||||
"123AAAAA3",
|
||||
]
|
||||
self.uidgen.counter = 24
|
||||
actual = [self.uidgen.next_uid() for i in range(6)]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_rollover_second(self):
|
||||
expected = [
|
||||
"123AAAAA8",
|
||||
"123AAAAA9",
|
||||
"123AAAABA",
|
||||
"123AAAABB",
|
||||
"123AAAABC",
|
||||
"123AAAABD",
|
||||
]
|
||||
self.uidgen.counter = 36 - 2
|
||||
actual = [self.uidgen.next_uid() for i in range(6)]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_rollover_third(self):
|
||||
expected = [
|
||||
"123AAAE98",
|
||||
"123AAAE99",
|
||||
"123AAAFAA",
|
||||
"123AAAFAB",
|
||||
"123AAAFAC",
|
||||
]
|
||||
self.uidgen.counter = 5*36**2 - 2
|
||||
actual = [self.uidgen.next_uid() for i in range(5)]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_overflow(self):
|
||||
self.uidgen.counter = 36**6-1
|
||||
self.assertTrue(self.uidgen.next_uid())
|
||||
self.assertRaises(RuntimeError, self.uidgen.next_uid)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,13 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from pylinkirc.protocols import unreal
|
||||
|
||||
import protocol_test_fixture as ptf
|
||||
|
||||
class UnrealProtocolTest(ptf.BaseProtocolTest):
|
||||
proto_class = unreal.UnrealProtocol
|
||||
|
||||
# In the future we will have protocol specific test cases here
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -3,10 +3,8 @@ Test cases for utils.py
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from pylinkirc import utils
|
||||
|
||||
|
||||
class UtilsTestCase(unittest.TestCase):
|
||||
|
||||
def test_strip_irc_formatting(self):
|
||||
@ -189,87 +187,6 @@ class UtilsTestCase(unittest.TestCase):
|
||||
utils.parse_duration("4s3d")
|
||||
utils.parse_duration("1m5w")
|
||||
|
||||
def test_match_text(self):
|
||||
f = utils.match_text # glob, target
|
||||
|
||||
# Base cases
|
||||
self.assertTrue(f("", ""))
|
||||
self.assertFalse(f("test", ""))
|
||||
self.assertFalse(f("", "abcdef"))
|
||||
self.assertFalse(f("", "*")) # specified the wrong way
|
||||
self.assertFalse(f("", "?"))
|
||||
self.assertTrue(f("foo", "foo"))
|
||||
self.assertFalse(f("foo", "bar"))
|
||||
self.assertFalse(f("foo", "food"))
|
||||
|
||||
# Test use of *
|
||||
self.assertTrue(f("*", "b"))
|
||||
self.assertTrue(f("*", "abc"))
|
||||
self.assertTrue(f("*", ""))
|
||||
self.assertTrue(f("*!*@*", "nick!user@host"))
|
||||
self.assertTrue(f("*@*", "rick!user@lost"))
|
||||
self.assertTrue(f("ni*!*@*st", "nick!user@roast"))
|
||||
self.assertFalse(f("nick!*abcdef*@*st*", "nick!user@roast"))
|
||||
self.assertTrue(f("*!*@*.overdrive.pw", "abc!def@abc.users.overdrive.pw"))
|
||||
|
||||
# Test use of ?
|
||||
self.assertTrue(f("?", "b"))
|
||||
self.assertFalse(f("?", "abc"))
|
||||
self.assertTrue(f("Guest?????!???irc@users.overdrive.pw", "Guest12567!webirc@users.overdrive.pw"))
|
||||
self.assertFalse(f("Guest????!webirc@users.overdrive.pw", "Guest23457!webirc@users.overdrive.pw"))
|
||||
|
||||
def test_match_text_complex(self):
|
||||
f = utils.match_text # glob, target
|
||||
|
||||
# Test combination of * and ?
|
||||
for glob in {"*?", "?*"}:
|
||||
self.assertTrue(f(glob, "a"))
|
||||
self.assertTrue(f(glob, "ab"))
|
||||
self.assertFalse(f(glob, ""))
|
||||
|
||||
self.assertTrue(f("ba*??*ll", "basketball"))
|
||||
self.assertFalse(f("ba*??*ll", "ball"))
|
||||
self.assertFalse(f("ba*??*ll", "basketballs"))
|
||||
|
||||
self.assertTrue(f("**", "fooBarBaz"))
|
||||
self.assertTrue(f("*?*?*?*", "cat"))
|
||||
self.assertTrue(f("*??****?*", "cat"))
|
||||
self.assertFalse(f("*??****?*?****", "MAP"))
|
||||
|
||||
def test_match_text_casemangle(self):
|
||||
f = utils.match_text # glob, target, manglefunc
|
||||
|
||||
# We are case insensitive by default
|
||||
self.assertTrue(f("Test", "TEST"))
|
||||
self.assertTrue(f("ALPHA*", "alphabet"))
|
||||
|
||||
# But we can override this preference
|
||||
self.assertFalse(f("Test", "TEST", None))
|
||||
self.assertFalse(f("*for*", "BEForE", None))
|
||||
self.assertTrue(f("*corn*", "unicorns", None))
|
||||
|
||||
# Or specify some other filter func
|
||||
self.assertTrue(f('005', '5', lambda s: s.zfill(3)))
|
||||
self.assertTrue(f('*0*', '14', lambda s: s.zfill(6)))
|
||||
self.assertFalse(f('*9*', '14', lambda s: s.zfill(13)))
|
||||
self.assertTrue(f('*chin*', 'machine', str.upper))
|
||||
|
||||
def test_merge_iterables(self):
|
||||
f = utils.merge_iterables
|
||||
self.assertEqual(f([], []), [])
|
||||
self.assertEqual(f({}, {}), {})
|
||||
self.assertEqual(f(set(), set()), set())
|
||||
|
||||
self.assertEqual(f([1,2], [4,5,6]), [1,2,4,5,6])
|
||||
self.assertEqual(f({'a': 'b'}, {'c': 'd', 'e': 'f'}),
|
||||
{'a': 'b', 'c': 'd', 'e': 'f'})
|
||||
self.assertEqual(f({0,1,2}, {1,3,5}),
|
||||
{0,1,2,3,5})
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
f([1,2,3], {'a': 'b'}) # mismatched type
|
||||
with self.assertRaises(ValueError):
|
||||
f([], set()) # mismatched type
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
91
utils.py
91
utils.py
@ -5,29 +5,19 @@ This module contains various utility functions related to IRC and/or the PyLink
|
||||
framework.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import functools
|
||||
import importlib
|
||||
import ipaddress
|
||||
import os
|
||||
import re
|
||||
import string
|
||||
import re
|
||||
import importlib
|
||||
import os
|
||||
import collections
|
||||
import argparse
|
||||
import ipaddress
|
||||
|
||||
from .log import log
|
||||
from . import world, conf, structures
|
||||
|
||||
# Load the protocol and plugin packages.
|
||||
from pylinkirc import plugins, protocols
|
||||
|
||||
from . import conf, structures, world
|
||||
from .log import log
|
||||
|
||||
__all__ = ['PLUGIN_PREFIX', 'PROTOCOL_PREFIX', 'NORMALIZEWHITESPACE_RE',
|
||||
'NotAuthorizedError', 'InvalidArgumentsError', 'ProtocolError',
|
||||
'add_cmd', 'add_hook', 'expand_path', 'split_hostmask',
|
||||
'ServiceBot', 'register_service', 'unregister_service',
|
||||
'wrap_arguments', 'IRCParser', 'strip_irc_formatting',
|
||||
'remove_range', 'get_hostname_type', 'parse_duration', 'match_text',
|
||||
'merge_iterables']
|
||||
|
||||
from pylinkirc import protocols, plugins
|
||||
|
||||
PLUGIN_PREFIX = plugins.__name__ + '.'
|
||||
PROTOCOL_PREFIX = protocols.__name__ + '.'
|
||||
@ -102,8 +92,6 @@ def split_hostmask(mask):
|
||||
"""
|
||||
nick, identhost = mask.split('!', 1)
|
||||
ident, host = identhost.split('@', 1)
|
||||
if not all({nick, ident, host}):
|
||||
raise ValueError("Invalid user@host %r" % mask)
|
||||
return [nick, ident, host]
|
||||
splitHostmask = split_hostmask
|
||||
|
||||
@ -162,7 +150,7 @@ class ServiceBot():
|
||||
else:
|
||||
raise NotImplementedError("Network specific plugins not supported yet.")
|
||||
|
||||
def join(self, irc, channels, ignore_empty=None):
|
||||
def join(self, irc, channels, ignore_empty=True):
|
||||
"""
|
||||
Joins the given service bot to the given channel(s). "channels" can be
|
||||
an iterable of channel names or the name of a single channel (type 'str').
|
||||
@ -172,8 +160,8 @@ class ServiceBot():
|
||||
marked persistent). This option is automatically *disabled* on networks
|
||||
where we cannot monitor channels that we're not in (e.g. Clientbot).
|
||||
|
||||
Before PyLink 2.0-alpha3, this function implicitly marked channels it
|
||||
receives to be persistent. This behaviour is no longer the case.
|
||||
Before PyLink 2.0-alpha3, this function implicitly marks channels i
|
||||
receives to be persistent - this is no longer the case!
|
||||
"""
|
||||
uid = self.uids.get(irc.name)
|
||||
if uid is None:
|
||||
@ -185,10 +173,6 @@ class ServiceBot():
|
||||
if irc.has_cap('visible-state-only'):
|
||||
# Disable dynamic channel joining on networks where we can't monitor channels for joins.
|
||||
ignore_empty = False
|
||||
elif ignore_empty is None:
|
||||
ignore_empty = not (irc.serverdata.get('join_empty_channels',
|
||||
conf.conf['pylink'].get('join_empty_channels',
|
||||
False)))
|
||||
|
||||
# Specify modes to join the services bot with.
|
||||
joinmodes = irc.get_service_option(self.name, 'joinmodes', default='')
|
||||
@ -408,7 +392,7 @@ class ServiceBot():
|
||||
chanlist = namespace.setdefault(irc.name, structures.IRCCaseInsensitiveSet(irc))
|
||||
chanlist.add(channel)
|
||||
|
||||
if try_join and irc.has_cap('can-manage-bot-channels'):
|
||||
if try_join:
|
||||
self.join(irc, [channel])
|
||||
|
||||
def remove_persistent_channel(self, irc, namespace, channel, try_part=True, part_reason=''):
|
||||
@ -417,7 +401,7 @@ class ServiceBot():
|
||||
"""
|
||||
chanlist = self.dynamic_channels[namespace][irc.name].remove(channel)
|
||||
|
||||
if try_part and irc.connected.is_set() and irc.has_cap('can-manage-bot-channels'):
|
||||
if try_part and irc.connected.is_set():
|
||||
self.part(irc, [channel], reason=part_reason)
|
||||
|
||||
def get_persistent_channels(self, irc, namespace=None):
|
||||
@ -833,48 +817,3 @@ def parse_duration(text):
|
||||
raise ValueError("Failed to parse duration string %r" % text)
|
||||
|
||||
return result
|
||||
|
||||
@functools.lru_cache(maxsize=1024)
|
||||
def _glob2re(glob):
|
||||
"""Converts an IRC-style glob to a regular expression."""
|
||||
patt = ['^']
|
||||
|
||||
for char in glob:
|
||||
if char == '*' and patt[-1] != '*': # Collapse ** into *
|
||||
patt.append('.*')
|
||||
elif char == '?':
|
||||
patt.append('.')
|
||||
else:
|
||||
patt.append(re.escape(char))
|
||||
|
||||
patt.append('$')
|
||||
return ''.join(patt)
|
||||
|
||||
def match_text(glob, text, filterfunc=str.lower):
|
||||
"""
|
||||
Returns whether glob matches text. If filterfunc is specified, run filterfunc on glob and text
|
||||
before preforming matches.
|
||||
"""
|
||||
if filterfunc:
|
||||
glob = filterfunc(glob)
|
||||
text = filterfunc(text)
|
||||
|
||||
return re.match(_glob2re(glob), text)
|
||||
|
||||
def merge_iterables(A, B):
|
||||
"""
|
||||
Merges the values in two iterables. A and B must be of the same type, and one of the following:
|
||||
|
||||
- list: items are combined as A + B
|
||||
- set: items are combined as A | B
|
||||
- dict: items are combined as {**A, **B}
|
||||
"""
|
||||
if type(A) != type(B):
|
||||
raise ValueError("inputs must be the same type")
|
||||
|
||||
if isinstance(A, list):
|
||||
return A + B
|
||||
elif isinstance(A, set):
|
||||
return A | B
|
||||
elif isinstance(A, dict):
|
||||
return {**A, **B}
|
||||
|
6
world.py
6
world.py
@ -2,13 +2,9 @@
|
||||
world.py: Stores global variables for PyLink, including lists of active IRC objects and plugins.
|
||||
"""
|
||||
|
||||
from collections import defaultdict, deque
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict, deque
|
||||
|
||||
__all__ = ['testing', 'hooks', 'networkobjects', 'plugins', 'services',
|
||||
'exttarget_handlers', 'started', 'start_ts', 'shutting_down',
|
||||
'source', 'fallback_hostname', 'daemon']
|
||||
|
||||
# This indicates whether we're running in tests mode. What it actually does
|
||||
# though is control whether IRC connections should be threaded or not.
|
||||
|
Loading…
x
Reference in New Issue
Block a user