mirror of
https://github.com/jlu5/PyLink.git
synced 2024-12-25 20:22:45 +01:00
Merge branch 'staging'
This commit is contained in:
commit
0d0cccea63
6
.gitignore
vendored
6
.gitignore
vendored
@ -3,6 +3,9 @@
|
|||||||
!example-*.yml
|
!example-*.yml
|
||||||
!.*.yml
|
!.*.yml
|
||||||
|
|
||||||
|
# Ignore automatically generated version for normal commits. This is bumped manually when needed.
|
||||||
|
__init__.py
|
||||||
|
|
||||||
env/
|
env/
|
||||||
build/
|
build/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@ -13,3 +16,6 @@ __pycache__/
|
|||||||
*.db
|
*.db
|
||||||
*.pid
|
*.pid
|
||||||
*.pem
|
*.pem
|
||||||
|
.eggs
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
167
LICENSE.CC-BY-SA-4.0
Normal file
167
LICENSE.CC-BY-SA-4.0
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
Attribution-ShareAlike 4.0 International
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||||
|
does not provide legal services or legal advice. Distribution of
|
||||||
|
Creative Commons public licenses does not create a lawyer-client or
|
||||||
|
other relationship. Creative Commons makes its licenses and related
|
||||||
|
information available on an "as-is" basis. Creative Commons gives no
|
||||||
|
warranties regarding its licenses, any material licensed under their
|
||||||
|
terms and conditions, or any related information. Creative Commons
|
||||||
|
disclaims all liability for damages resulting from their use to the
|
||||||
|
fullest extent possible.
|
||||||
|
|
||||||
|
Using Creative Commons Public Licenses
|
||||||
|
|
||||||
|
Creative Commons public licenses provide a standard set of terms and
|
||||||
|
conditions that creators and other rights holders may use to share
|
||||||
|
original works of authorship and other material subject to copyright
|
||||||
|
and certain other rights specified in the public license below. The
|
||||||
|
following considerations are for informational purposes only, are not
|
||||||
|
exhaustive, and do not form part of our licenses.
|
||||||
|
|
||||||
|
Considerations for licensors: Our public licenses are
|
||||||
|
intended for use by those authorized to give the public
|
||||||
|
permission to use material in ways otherwise restricted by
|
||||||
|
copyright and certain other rights. Our licenses are
|
||||||
|
irrevocable. Licensors should read and understand the terms
|
||||||
|
and conditions of the license they choose before applying it.
|
||||||
|
Licensors should also secure all rights necessary before
|
||||||
|
applying our licenses so that the public can reuse the
|
||||||
|
material as expected. Licensors should clearly mark any
|
||||||
|
material not subject to the license. This includes other CC-
|
||||||
|
licensed material, or material used under an exception or
|
||||||
|
limitation to copyright. More considerations for licensors:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensors
|
||||||
|
|
||||||
|
Considerations for the public: By using one of our public
|
||||||
|
licenses, a licensor grants the public permission to use the
|
||||||
|
licensed material under specified terms and conditions. If
|
||||||
|
the licensor's permission is not necessary for any reason--for
|
||||||
|
example, because of any applicable exception or limitation to
|
||||||
|
copyright--then that use is not regulated by the license. Our
|
||||||
|
licenses grant only permissions under copyright and certain
|
||||||
|
other rights that a licensor has authority to grant. Use of
|
||||||
|
the licensed material may still be restricted for other
|
||||||
|
reasons, including because others have copyright or other
|
||||||
|
rights in the material. A licensor may make special requests,
|
||||||
|
such as asking that all changes be marked or described.
|
||||||
|
Although not required by our licenses, you are encouraged to
|
||||||
|
respect those requests where reasonable. More_considerations
|
||||||
|
for the public:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensees
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||||
|
License
|
||||||
|
|
||||||
|
By exercising the Licensed Rights (defined below), You accept and agree
|
||||||
|
to be bound by the terms and conditions of this Creative Commons
|
||||||
|
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||||
|
License"). To the extent this Public License may be interpreted as a
|
||||||
|
contract, You are granted the Licensed Rights in consideration of Your
|
||||||
|
acceptance of these terms and conditions, and the Licensor grants You
|
||||||
|
such rights in consideration of benefits the Licensor receives from
|
||||||
|
making the Licensed Material available under these terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
|
||||||
|
Section 1 -- Definitions.
|
||||||
|
|
||||||
|
a. Adapted Material means material subject to Copyright and Similar
|
||||||
|
Rights that is derived from or based upon the Licensed Material
|
||||||
|
and in which the Licensed Material is translated, altered,
|
||||||
|
arranged, transformed, or otherwise modified in a manner requiring
|
||||||
|
permission under the Copyright and Similar Rights held by the
|
||||||
|
Licensor. For purposes of this Public License, where the Licensed
|
||||||
|
Material is a musical work, performance, or sound recording,
|
||||||
|
Adapted Material is always produced where the Licensed Material is
|
||||||
|
synched in timed relation with a moving image.
|
||||||
|
|
||||||
|
b. Adapter's License means the license You apply to Your Copyright
|
||||||
|
and Similar Rights in Your contributions to Adapted Material in
|
||||||
|
accordance with the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
c. BY-SA Compatible License means a license listed at
|
||||||
|
creativecommons.org/compatiblelicenses, approved by Creative
|
||||||
|
Commons as essentially the equivalent of this Public License.
|
||||||
|
|
||||||
|
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||||
|
closely related to copyright including, without limitation,
|
||||||
|
performance, broadcast, sound recording, and Sui Generis Database
|
||||||
|
Rights, without regard to how the rights are labeled or
|
||||||
|
categorized. For purposes of this Public License, the rights
|
||||||
|
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||||
|
Rights.
|
||||||
|
|
||||||
|
e. Effective Technological Measures means those measures that, in the
|
||||||
|
absence of proper authority, may not be circumvented under laws
|
||||||
|
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||||
|
Treaty adopted on December 20, 1996, and/or similar international
|
||||||
|
agreements.
|
||||||
|
|
||||||
|
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||||
|
any other exception or limitation to Copyright and Similar Rights
|
||||||
|
that applies to Your use of the Licensed Material.
|
||||||
|
|
||||||
|
g. License Elements means the license attributes listed in the name
|
||||||
|
of a Creative Commons Public License. The License Elements of this
|
||||||
|
Public License are Attribution and ShareAlike.
|
||||||
|
|
||||||
|
h. Licensed Material means the artistic or literary work, database,
|
||||||
|
or other material to which the Licensor applied this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
i. Licensed Rights means the rights granted to You subject to the
|
||||||
|
terms and conditions of this Public License, which are limited to
|
||||||
|
all Copyright and Similar Rights that apply to Your use of the
|
||||||
|
Licensed Material and that the Licensor has authority to license.
|
||||||
|
|
||||||
|
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||||
|
under this Public License.
|
||||||
|
|
||||||
|
k. Share means to provide material to the public by any means or
|
||||||
|
process that requires permission under the Licensed Rights, such
|
||||||
|
as reproduction, public display, public performance, distribution,
|
||||||
|
dissemination, communication, or importation, and to make material
|
||||||
|
available to the public including in ways that members of the
|
||||||
|
public may access the material from a place and at a time
|
||||||
|
individually chosen by them.
|
||||||
|
|
||||||
|
l. Sui Generis Database Rights means rights other than copyright
|
||||||
|
resulting from Directive 96/9/EC of the European Parliament and of
|
||||||
|
the Council of 11 March 1996 on the legal protection of databases,
|
||||||
|
as amended and/or succeeded, as well as other essentially
|
||||||
|
equivalent rights anywhere in the world.
|
||||||
|
|
||||||
|
m. You means the individual or entity exercising the Licensed Rights
|
||||||
|
under this Public License. Your has a corresponding meaning.
|
||||||
|
|
||||||
|
|
||||||
|
Section 2 -- Scope.
|
||||||
|
|
||||||
|
a. License grant.
|
||||||
|
|
||||||
|
1. Subject to the terms and conditions of this Public License,
|
||||||
|
the Licensor hereby grants You a worldwide, royalty-free,
|
||||||
|
non-sublicensable, non-exclusive, irrevocable license to
|
||||||
|
exercise the Licensed Rights in the Licensed Material to:
|
||||||
|
|
||||||
|
a. reproduce and Share the Licensed Material, in whole or
|
||||||
|
in part; and
|
||||||
|
|
||||||
|
b. produce, reproduce, and Share Adapted Material.
|
||||||
|
|
||||||
|
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||||
|
Exceptions and Limitations apply to Your use, this Public
|
||||||
|
License does not apply, and You do not need to comply with
|
||||||
|
its terms and conditions.
|
||||||
|
|
||||||
|
3. Term. The term of this Public License is specified in Section
|
||||||
|
6(a).
|
||||||
|
|
||||||
|
4. Media and formats; technical modifications allowed. The
|
||||||
|
Licensor authorizes You to exercise the Licensed Rights in
|
||||||
|
all media and formats whether now known or h
|
35
README.md
35
README.md
@ -14,40 +14,49 @@ Please report any bugs you find to the [issue tracker](https://github.com/GLolol
|
|||||||
|
|
||||||
You can also find support via our IRC channels: `#PyLink @ irc.overdrivenetworks.com `([webchat](https://webchat.overdrivenetworks.com/?channels=PyLink,dev)) or `#PyLink @ chat.freenode.net`. Ask your questions and be patient for a response.
|
You can also find support via our IRC channels: `#PyLink @ irc.overdrivenetworks.com `([webchat](https://webchat.overdrivenetworks.com/?channels=PyLink,dev)) or `#PyLink @ chat.freenode.net`. Ask your questions and be patient for a response.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
* Python 3.4+
|
* Python 3.4+
|
||||||
* PyYAML (`pip install pyyaml`)
|
* PyYAML (`pip install pyyaml`)
|
||||||
|
* [ircmatch](https://github.com/mammon-ircd/ircmatch) (`pip install ircmatch`)
|
||||||
* *For the servprotect plugin*: [expiringdict](https://github.com/mailgun/expiringdict) (note: unfortunately, installation is broken in pip due to [mailgun/expiringdict#13](https://github.com/mailgun/expiringdict/issues/13))
|
* *For the servprotect plugin*: [expiringdict](https://github.com/mailgun/expiringdict) (note: unfortunately, installation is broken in pip due to [mailgun/expiringdict#13](https://github.com/mailgun/expiringdict/issues/13))
|
||||||
* *For the changehost and opercmds plugins*: [ircmatch](https://github.com/mammon-ircd/ircmatch) (`pip install ircmatch`)
|
|
||||||
|
|
||||||
## Supported IRCds
|
## Supported IRCds
|
||||||
|
|
||||||
### Primary support
|
### Primary support
|
||||||
|
|
||||||
These IRCds are frequently tested and well supported. If any issues occur, please file a bug on the issue tracker.
|
These IRCds (in alphabetical order) are frequently tested and well supported. If any issues occur, please file a bug on the issue tracker.
|
||||||
|
|
||||||
* charybdis (3.5.x / git master) - module `ts6`
|
* [charybdis](http://charybdis.io/) (3.5+ / git master) - module `ts6`
|
||||||
* InspIRCd 2.0.x - module `inspircd`
|
* [InspIRCd](http://www.inspircd.org/) 2.0.x - module `inspircd`
|
||||||
* UnrealIRCd 4.x - module `unreal`
|
* [UnrealIRCd](https://www.unrealircd.org/) 4.x - module `unreal`
|
||||||
- Note: Support for mixed UnrealIRCd 3.2/4.0 networks is experimental, and requires you to enable a `mixed_link` option in the configuration. This may in turn void your support.
|
- Note: Support for mixed UnrealIRCd 3.2/4.0 networks is experimental, and requires you to enable a `mixed_link` option in the configuration. This may in turn void your support.
|
||||||
|
|
||||||
### Extended support
|
### 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 be always be fixed in a timely fashion.
|
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.
|
||||||
|
|
||||||
* Elemental-IRCd (6.6.x / git master) - module `ts6`
|
* [Elemental-IRCd](https://github.com/Elemental-IRCd/elemental-ircd) (6.6.x / git master) - module `ts6`
|
||||||
* InspIRCd 2.2 (git master) - module `inspircd`
|
* InspIRCd 2.2 (git master) - module `inspircd`
|
||||||
* IRCd-Hybrid (8.2.x / svn trunk) - module `hybrid`
|
* [IRCd-Hybrid](http://www.ircd-hybrid.org/) (8.2.x / svn trunk) - module `hybrid`
|
||||||
- Note: for host changing support and optimal functionality, a `service{}` block / U-line should be added for PyLink on every IRCd across your network.
|
- Note: for host changing support and optimal functionality, a `service{}` block / U-line should be added for PyLink on every IRCd across your network.
|
||||||
* Nefarious IRCu (2.0.0+) - module `nefarious`
|
* [juno-ircd](https://github.com/cooper/yiria) (10.x / yiria) - module `ts6` (with elemental-ircd modes)
|
||||||
|
* [Nefarious IRCu](https://github.com/evilnet/nefarious2) (2.0.0+) - module `nefarious`
|
||||||
- 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.
|
- 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.
|
||||||
- For optimal functionality (mode overrides in relay, etc.), a `UWorld{}` block / U-line should be added for every server that PyLink spawns. To make this easier, you may want to turn relay's spawn_servers off, so that all relay users originate from one virtual server.
|
- For optimal functionality (mode overrides in relay, etc.), a `UWorld{}` block / U-line should be added for every server that PyLink spawns.
|
||||||
|
|
||||||
|
Other TS6 and P10 variations may work, but are not officially supported.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1) Rename `example-conf.yml` to `config.yml` and configure your instance there. Note that the configuration format isn't finalized yet - this means that your configuration may break in an update!
|
1) Install PyLink by using `python3 setup.py install` (global install) or `python3 setup.py install --user` (local install)
|
||||||
|
|
||||||
2) Run `./pylink` from the command line.
|
2) Rename `example-conf.yml` to `pylink.yml` and configure your instance there. Note that the configuration format isn't finalized yet - this means that your configuration may break in an update!
|
||||||
|
|
||||||
3) Profit???
|
3) Run `pylink` from the command line.
|
||||||
|
|
||||||
|
4) Profit???
|
||||||
|
2
__init__.py
Normal file
2
__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Automatically generated by setup.py
|
||||||
|
__version__ = '0.9-dev1-27-g14b30b2'
|
174
classes.py
174
classes.py
@ -11,16 +11,15 @@ import threading
|
|||||||
from random import randint
|
from random import randint
|
||||||
import time
|
import time
|
||||||
import socket
|
import socket
|
||||||
import threading
|
|
||||||
import ssl
|
import ssl
|
||||||
import hashlib
|
import hashlib
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from log import *
|
import ircmatch
|
||||||
import world
|
|
||||||
import utils
|
from . import world, utils, structures, __version__
|
||||||
import structures
|
from .log import *
|
||||||
|
|
||||||
### Exceptions
|
### Exceptions
|
||||||
|
|
||||||
@ -45,7 +44,7 @@ class Irc():
|
|||||||
self.sid = self.serverdata["sid"]
|
self.sid = self.serverdata["sid"]
|
||||||
self.botdata = conf['bot']
|
self.botdata = conf['bot']
|
||||||
self.bot_clients = {}
|
self.bot_clients = {}
|
||||||
self.protoname = proto.__name__
|
self.protoname = proto.__name__.split('.')[-1] # Remove leading pylinkirc.protocols.
|
||||||
self.proto = proto.Class(self)
|
self.proto = proto.Class(self)
|
||||||
self.pingfreq = self.serverdata.get('pingfreq') or 90
|
self.pingfreq = self.serverdata.get('pingfreq') or 90
|
||||||
self.pingtimeout = self.pingfreq * 2
|
self.pingtimeout = self.pingfreq * 2
|
||||||
@ -106,9 +105,10 @@ class Irc():
|
|||||||
self.pseudoclient = None
|
self.pseudoclient = None
|
||||||
self.lastping = time.time()
|
self.lastping = time.time()
|
||||||
|
|
||||||
# Internal variable to set the place the last command was called (in PM
|
# Internal variable to set the place and caller of the last command (in PM
|
||||||
# or in a channel), used by fantasy command support.
|
# or in a channel), used by fantasy command support.
|
||||||
self.called_by = None
|
self.called_by = None
|
||||||
|
self.called_in = None
|
||||||
|
|
||||||
# Intialize the server, channel, and user indexes to be populated by
|
# Intialize the server, channel, and user indexes to be populated by
|
||||||
# our protocol module. For the server index, we can add ourselves right
|
# our protocol module. For the server index, we can add ourselves right
|
||||||
@ -148,9 +148,8 @@ class Irc():
|
|||||||
|
|
||||||
# This max nick length starts off as the config value, but may be
|
# This max nick length starts off as the config value, but may be
|
||||||
# overwritten later by the protocol module if such information is
|
# overwritten later by the protocol module if such information is
|
||||||
# received. Note that only some IRCds (InspIRCd) give us nick length
|
# received. It defaults to 30.
|
||||||
# during link, so it is still required that the config value be set!
|
self.maxnicklen = self.serverdata.get('maxnicklen', 30)
|
||||||
self.maxnicklen = self.serverdata['maxnicklen']
|
|
||||||
|
|
||||||
# Defines a list of supported prefix modes.
|
# Defines a list of supported prefix modes.
|
||||||
self.prefixmodes = {'o': '@', 'v': '+'}
|
self.prefixmodes = {'o': '@', 'v': '+'}
|
||||||
@ -463,9 +462,21 @@ class Irc():
|
|||||||
cmd = 'PYLINK_SELF_PRIVMSG'
|
cmd = 'PYLINK_SELF_PRIVMSG'
|
||||||
self.callHooks([source, cmd, {'target': target, 'text': text}])
|
self.callHooks([source, cmd, {'target': target, 'text': text}])
|
||||||
|
|
||||||
def reply(self, text, notice=False, source=None):
|
def reply(self, text, notice=False, source=None, private=False, force_privmsg_in_private=False):
|
||||||
"""Replies to the last caller in the right context (channel or PM)."""
|
"""Replies to the last caller in the right context (channel or PM)."""
|
||||||
self.msg(self.called_by, text, notice=notice, source=source)
|
|
||||||
|
# Private reply is enabled, or the caller was originally a PM
|
||||||
|
if private or (self.called_in in self.users):
|
||||||
|
if not force_privmsg_in_private:
|
||||||
|
# For private replies, the default is to override the notice=True/False argument,
|
||||||
|
# and send replies as notices regardless. This is standard behaviour for most
|
||||||
|
# IRC services, but can be disabled if force_privmsg_in_private is given.
|
||||||
|
notice = True
|
||||||
|
target = self.called_by
|
||||||
|
else:
|
||||||
|
target = self.called_in
|
||||||
|
|
||||||
|
self.msg(target, text, notice=notice, source=source)
|
||||||
|
|
||||||
def toLower(self, text):
|
def toLower(self, text):
|
||||||
"""Returns a lowercase representation of text based on the IRC object's
|
"""Returns a lowercase representation of text based on the IRC object's
|
||||||
@ -486,6 +497,11 @@ class Irc():
|
|||||||
# B = Mode that changes a setting and always has a parameter.
|
# B = Mode that changes a setting and always has a parameter.
|
||||||
# C = Mode that changes a setting and only has a parameter when set.
|
# C = Mode that changes a setting and only has a parameter when set.
|
||||||
# D = Mode that changes a setting and never has a parameter.
|
# D = Mode that changes a setting and never has a parameter.
|
||||||
|
|
||||||
|
if type(args) == str:
|
||||||
|
# If the modestring was given as a string, split it into a list.
|
||||||
|
args = args.split()
|
||||||
|
|
||||||
assert args, 'No valid modes were supplied!'
|
assert args, 'No valid modes were supplied!'
|
||||||
usermodes = not utils.isChannel(target)
|
usermodes = not utils.isChannel(target)
|
||||||
prefix = ''
|
prefix = ''
|
||||||
@ -503,10 +519,6 @@ class Irc():
|
|||||||
else:
|
else:
|
||||||
log.debug('(%s) Using self.cmodes for this query: %s', self.name, self.cmodes)
|
log.debug('(%s) Using self.cmodes for this query: %s', self.name, self.cmodes)
|
||||||
|
|
||||||
if target not in self.channels:
|
|
||||||
log.warning('(%s) Possible desync! Mode target %s is not in the channels index.', self.name, target)
|
|
||||||
return []
|
|
||||||
|
|
||||||
supported_modes = self.cmodes
|
supported_modes = self.cmodes
|
||||||
oldmodes = self.channels[target].modes
|
oldmodes = self.channels[target].modes
|
||||||
res = []
|
res = []
|
||||||
@ -633,10 +645,13 @@ class Irc():
|
|||||||
else:
|
else:
|
||||||
modelist.discard(real_mode)
|
modelist.discard(real_mode)
|
||||||
log.debug('(%s) Final modelist: %s', self.name, modelist)
|
log.debug('(%s) Final modelist: %s', self.name, modelist)
|
||||||
|
try:
|
||||||
if usermodes:
|
if usermodes:
|
||||||
self.users[target].modes = modelist
|
self.users[target].modes = modelist
|
||||||
else:
|
else:
|
||||||
self.channels[target].modes = modelist
|
self.channels[target].modes = modelist
|
||||||
|
except KeyError:
|
||||||
|
log.warning("(%s) Invalid MODE target %s (usermodes=%s)", self.name, target, usermodes)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _flip(mode):
|
def _flip(mode):
|
||||||
@ -738,6 +753,10 @@ class Irc():
|
|||||||
prefix = '+' # Assume we're adding modes unless told otherwise
|
prefix = '+' # Assume we're adding modes unless told otherwise
|
||||||
modelist = ''
|
modelist = ''
|
||||||
args = []
|
args = []
|
||||||
|
|
||||||
|
# Sort modes alphabetically like a conventional IRCd.
|
||||||
|
modes = sorted(modes)
|
||||||
|
|
||||||
for modepair in modes:
|
for modepair in modes:
|
||||||
mode, arg = modepair
|
mode, arg = modepair
|
||||||
assert len(mode) in (1, 2), "Incorrect length of a mode (received %r)" % mode
|
assert len(mode) in (1, 2), "Incorrect length of a mode (received %r)" % mode
|
||||||
@ -771,7 +790,7 @@ class Irc():
|
|||||||
Returns a detailed version string including the PyLink daemon version,
|
Returns a detailed version string including the PyLink daemon version,
|
||||||
the protocol module in use, and the server hostname.
|
the protocol module in use, and the server hostname.
|
||||||
"""
|
"""
|
||||||
fullversion = 'PyLink-%s. %s :[protocol:%s]' % (world.version, self.serverdata['hostname'], self.protoname)
|
fullversion = 'PyLink-%s. %s :[protocol:%s]' % (__version__, self.serverdata['hostname'], self.protoname)
|
||||||
return fullversion
|
return fullversion
|
||||||
|
|
||||||
### State checking functions
|
### State checking functions
|
||||||
@ -853,6 +872,16 @@ class Irc():
|
|||||||
|
|
||||||
return '%s!%s@%s' % (nick, ident, host)
|
return '%s!%s@%s' % (nick, ident, host)
|
||||||
|
|
||||||
|
def getFriendlyName(self, entityid):
|
||||||
|
"""
|
||||||
|
Returns the friendly name of a SID or UID (server name for SIDs, nick for UID)."""
|
||||||
|
if entityid in self.servers:
|
||||||
|
return self.servers[entityid].name
|
||||||
|
elif entityid in self.users:
|
||||||
|
return self.users[entityid].nick
|
||||||
|
else:
|
||||||
|
raise KeyError("Unknown UID/SID %s" % entityid)
|
||||||
|
|
||||||
def isOper(self, uid, allowAuthed=True, allowOper=True):
|
def isOper(self, uid, allowAuthed=True, allowOper=True):
|
||||||
"""
|
"""
|
||||||
Returns whether the given user has operator status on PyLink. This can be achieved
|
Returns whether the given user has operator status on PyLink. This can be achieved
|
||||||
@ -880,6 +909,61 @@ class Irc():
|
|||||||
raise utils.NotAuthenticatedError("You are not authenticated!")
|
raise utils.NotAuthenticatedError("You are not authenticated!")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def matchHost(self, glob, target, ip=True, realhost=True):
|
||||||
|
"""
|
||||||
|
Checks whether the given host, or given UID's hostmask matches the given nick!user@host
|
||||||
|
glob.
|
||||||
|
|
||||||
|
If the target given is a UID, and the ip or realhost options are True, this will also match
|
||||||
|
against the target's IP address and real host, respectively.
|
||||||
|
"""
|
||||||
|
# Get the corresponding casemapping value used by ircmatch.
|
||||||
|
casemapping = getattr(ircmatch, self.proto.casemapping)
|
||||||
|
|
||||||
|
# Try to convert target into a UID. If this fails, it's probably a hostname.
|
||||||
|
target = self.nickToUid(target) or target
|
||||||
|
|
||||||
|
# Prepare a list of hosts to check against.
|
||||||
|
if target in self.users:
|
||||||
|
if glob.startswith(('$', '!$')):
|
||||||
|
# !$exttarget inverts the given match.
|
||||||
|
invert = glob.startswith('!$')
|
||||||
|
|
||||||
|
# Exttargets start with $. Skip regular ban matching and find the matching ban handler.
|
||||||
|
glob = glob.lstrip('$!')
|
||||||
|
exttargetname = glob.split(':', 1)[0]
|
||||||
|
handler = world.exttarget_handlers.get(exttargetname)
|
||||||
|
|
||||||
|
if handler:
|
||||||
|
# Handler exists. Return what it finds.
|
||||||
|
result = handler(self, glob, target)
|
||||||
|
log.debug('(%s) Got %s from exttarget %s in matchHost() glob $%s for target %s',
|
||||||
|
self.name, result, exttargetname, glob, target)
|
||||||
|
if invert: # Anti-exttarget was specified.
|
||||||
|
result = not result
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
log.debug('(%s) Unknown exttarget %s in matchHost() glob $%s', self.name,
|
||||||
|
exttargetname, glob)
|
||||||
|
return False
|
||||||
|
|
||||||
|
hosts = {self.getHostmask(target)}
|
||||||
|
|
||||||
|
if ip:
|
||||||
|
hosts.add(self.getHostmask(target, ip=True))
|
||||||
|
|
||||||
|
if realhost:
|
||||||
|
hosts.add(self.getHostmask(target, ip=True))
|
||||||
|
else: # We were given a host, use that.
|
||||||
|
hosts = [target]
|
||||||
|
|
||||||
|
# Iterate over the hosts to match using ircmatch.
|
||||||
|
for host in hosts:
|
||||||
|
if ircmatch.match(casemapping, glob, host):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
class IrcUser():
|
class IrcUser():
|
||||||
"""PyLink IRC user class."""
|
"""PyLink IRC user class."""
|
||||||
def __init__(self, nick, ts, uid, ident='null', host='null',
|
def __init__(self, nick, ts, uid, ident='null', host='null',
|
||||||
@ -1073,24 +1157,58 @@ class Protocol():
|
|||||||
log.debug('Removing client %s from self.irc.servers[%s].users', numeric, sid)
|
log.debug('Removing client %s from self.irc.servers[%s].users', numeric, sid)
|
||||||
self.irc.servers[sid].users.discard(numeric)
|
self.irc.servers[sid].users.discard(numeric)
|
||||||
|
|
||||||
def updateTS(self, channel, their_ts):
|
def updateTS(self, channel, their_ts, modes=[]):
|
||||||
"""
|
"""
|
||||||
Compares the current TS of the channel given with the new TS, resetting
|
Merges modes of a channel given the remote TS and a list of modes.
|
||||||
all modes we have if the one given is older.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
our_ts = self.irc.channels[channel].ts
|
def _clear():
|
||||||
|
log.debug("(%s) Clearing modes from channel %s due to TS change", self.irc.name,
|
||||||
if their_ts < our_ts:
|
channel)
|
||||||
# Channel timestamp was reset on burst
|
|
||||||
log.debug('(%s) Setting channel TS of %s to %s from %s',
|
|
||||||
self.irc.name, channel, their_ts, our_ts)
|
|
||||||
self.irc.channels[channel].ts = their_ts
|
|
||||||
# When TS is reset, clear all modes we currently have
|
|
||||||
self.irc.channels[channel].modes.clear()
|
self.irc.channels[channel].modes.clear()
|
||||||
for p in self.irc.channels[channel].prefixmodes.values():
|
for p in self.irc.channels[channel].prefixmodes.values():
|
||||||
p.clear()
|
p.clear()
|
||||||
|
|
||||||
|
def _apply():
|
||||||
|
if modes:
|
||||||
|
log.debug("(%s) Applying modes on channel %s (TS ok)", self.irc.name,
|
||||||
|
channel)
|
||||||
|
self.irc.applyModes(channel, modes)
|
||||||
|
|
||||||
|
our_ts = self.irc.channels[channel].ts
|
||||||
|
assert type(our_ts) == int, "Wrong type for our_ts (expected int, got %s)" % type(our_ts)
|
||||||
|
assert type(their_ts) == int, "Wrong type for their_ts (expected int, got %s)" % type(their_ts)
|
||||||
|
|
||||||
|
if their_ts < our_ts:
|
||||||
|
# Their TS is older than ours. We should clear our stored modes for the channel and
|
||||||
|
# apply the ones in the queue to be set. This is regardless of whether we're sending
|
||||||
|
# outgoing modes or receiving some - both are handled the same with a "received" TS,
|
||||||
|
# and comparing it with the one we have.
|
||||||
|
log.debug("(%s/%s) received TS of %s is lower than ours %s; setting modes %s",
|
||||||
|
self.irc.name, channel, their_ts, our_ts, modes)
|
||||||
|
|
||||||
|
# Update the channel TS to theirs regardless of whether the mode setting passes.
|
||||||
|
log.debug('(%s) Setting channel TS of %s to %s from %s',
|
||||||
|
self.irc.name, channel, their_ts, our_ts)
|
||||||
|
self.irc.channels[channel].ts = their_ts
|
||||||
|
|
||||||
|
_clear()
|
||||||
|
_apply()
|
||||||
|
|
||||||
|
elif their_ts == our_ts:
|
||||||
|
log.debug("(%s/%s) remote TS of %s is equal to ours %s; setting modes %s",
|
||||||
|
self.irc.name, channel, their_ts, our_ts, modes)
|
||||||
|
# Their TS is equal to ours. Merge modes.
|
||||||
|
_apply()
|
||||||
|
|
||||||
|
elif their_ts > our_ts:
|
||||||
|
log.debug("(%s/%s) remote TS of %s is higher than ours %s; setting modes %s",
|
||||||
|
self.irc.name, channel, their_ts, our_ts, modes)
|
||||||
|
# Their TS is younger than ours. Clear the state and replace the modes for the channel
|
||||||
|
# with the ones being set.
|
||||||
|
_clear()
|
||||||
|
_apply()
|
||||||
|
|
||||||
def _getSid(self, sname):
|
def _getSid(self, sname):
|
||||||
"""Returns the SID of a server with the given name, if present."""
|
"""Returns the SID of a server with the given name, if present."""
|
||||||
name = sname.lower()
|
name = sname.lower()
|
||||||
|
70
conf.py
70
conf.py
@ -1,54 +1,43 @@
|
|||||||
"""
|
"""
|
||||||
conf.py - PyLink configuration core.
|
conf.py - PyLink configuration core.
|
||||||
|
|
||||||
This module is used to access the complete configuration for the current
|
This module is used to access the configuration of the current PyLink instance.
|
||||||
PyLink instance. It will load the config on first import, taking the
|
It provides simple checks for validating and loading YAML-format configurations from arbitrary files.
|
||||||
configuration file name from the first command-line argument, but defaulting
|
|
||||||
to 'config.yml' if this isn't given.
|
|
||||||
|
|
||||||
If world.testing is set to True, it will return a preset testing configuration
|
|
||||||
instead.
|
|
||||||
|
|
||||||
This module also provides simple checks for validating and loading YAML-format
|
|
||||||
configurations from arbitrary files.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import world
|
from . import world
|
||||||
|
|
||||||
global testconf
|
conf = {'bot':
|
||||||
testconf = {'bot':
|
|
||||||
{
|
{
|
||||||
'nick': 'PyLink',
|
'nick': 'PyLink',
|
||||||
'user': 'pylink',
|
'user': 'pylink',
|
||||||
'realname': 'PyLink Service Client',
|
'realname': 'PyLink Service Client',
|
||||||
'serverdesc': 'PyLink unit tests'
|
'serverdesc': 'Unconfigured PyLink'
|
||||||
},
|
},
|
||||||
'logging':
|
'logging':
|
||||||
{
|
{
|
||||||
# Suppress logging in the test output for the most part.
|
'stdout': 'INFO'
|
||||||
'stdout': 'CRITICAL'
|
|
||||||
},
|
},
|
||||||
'servers':
|
'servers':
|
||||||
# Wildcard defaultdict! This means that
|
# Wildcard defaultdict! This means that
|
||||||
# any network name you try will work and return
|
# any network name you try will work and return
|
||||||
# this basic template:
|
# this basic template:
|
||||||
defaultdict(lambda: {
|
defaultdict(lambda: {'ip': '0.0.0.0',
|
||||||
'ip': '0.0.0.0',
|
|
||||||
'port': 7000,
|
'port': 7000,
|
||||||
'recvpass': "abcd",
|
'recvpass': "unconfigured",
|
||||||
'sendpass': "chucknorris",
|
'sendpass': "unconfigured",
|
||||||
'protocol': "null",
|
'protocol': "null",
|
||||||
'hostname': "pylink.unittest",
|
'hostname': "pylink.unconfigured",
|
||||||
'sid': "9PY",
|
'sid': "000",
|
||||||
'channels': ["#pylink"],
|
|
||||||
'maxnicklen': 20,
|
'maxnicklen': 20,
|
||||||
'sidrange': '8##'
|
'sidrange': '0##'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
confname = 'unconfigured'
|
||||||
|
|
||||||
def validateConf(conf):
|
def validateConf(conf):
|
||||||
"""Validates a parsed configuration dict."""
|
"""Validates a parsed configuration dict."""
|
||||||
@ -59,44 +48,27 @@ def validateConf(conf):
|
|||||||
|
|
||||||
for netname, serverblock in conf['servers'].items():
|
for netname, serverblock in conf['servers'].items():
|
||||||
for section in ('ip', 'port', 'recvpass', 'sendpass', 'hostname',
|
for section in ('ip', 'port', 'recvpass', 'sendpass', 'hostname',
|
||||||
'sid', 'sidrange', 'protocol', 'maxnicklen'):
|
'sid', 'sidrange', 'protocol'):
|
||||||
assert serverblock.get(section), "Missing %r in server block for %r." % (section, netname)
|
assert serverblock.get(section), "Missing %r in server block for %r." % (section, netname)
|
||||||
|
|
||||||
assert type(serverblock.get('channels')) == list, "'channels' option in " \
|
|
||||||
"server block for %s must be a list, not %s." % (netname, type(serverblock['channels']).__name__)
|
|
||||||
|
|
||||||
assert type(conf['login'].get('password')) == type(conf['login'].get('user')) == str and \
|
assert type(conf['login'].get('password')) == type(conf['login'].get('user')) == str and \
|
||||||
conf['login']['password'] != "changeme", "You have not set the login details correctly!"
|
conf['login']['password'] != "changeme", "You have not set the login details correctly!"
|
||||||
|
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
def loadConf(fname, errors_fatal=True):
|
def loadConf(filename, errors_fatal=True):
|
||||||
"""Loads a PyLink configuration file from the filename given."""
|
"""Loads a PyLink configuration file from the filename given."""
|
||||||
with open(fname, 'r') as f:
|
global confname, conf, fname
|
||||||
|
fname = filename
|
||||||
|
confname = filename.split('.', 1)[0]
|
||||||
|
with open(filename, 'r') as f:
|
||||||
try:
|
try:
|
||||||
conf = yaml.load(f)
|
conf = yaml.load(f)
|
||||||
|
conf = validateConf(conf)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('ERROR: Failed to load config from %r: %s: %s' % (fname, type(e).__name__, e))
|
print('ERROR: Failed to load config from %r: %s: %s' % (filename, type(e).__name__, e))
|
||||||
if errors_fatal:
|
if errors_fatal:
|
||||||
sys.exit(4)
|
sys.exit(4)
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
if world.testing:
|
|
||||||
conf = testconf
|
|
||||||
confname = 'testconf'
|
|
||||||
fname = None
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
# Get the config name from the command line, falling back to config.yml
|
|
||||||
# if not given.
|
|
||||||
fname = sys.argv[1]
|
|
||||||
confname = fname.split('.', 1)[0]
|
|
||||||
except IndexError:
|
|
||||||
# confname is used for logging and PID writing, so that each
|
|
||||||
# instance uses its own files. fname is the actual name of the file
|
|
||||||
# we load.
|
|
||||||
confname = 'pylink'
|
|
||||||
fname = 'config.yml'
|
|
||||||
conf = validateConf(loadConf(fname))
|
|
||||||
|
2
coremods/__init__.py
Normal file
2
coremods/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Service support has to be imported first, so that utils.add_cmd works
|
||||||
|
from . import service_support, control, handlers, corecommands, exttargets
|
79
coremods/control.py
Normal file
79
coremods/control.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
control.py - Implements SHUTDOWN and REHASH functionality.
|
||||||
|
"""
|
||||||
|
import signal
|
||||||
|
import os
|
||||||
|
|
||||||
|
from pylinkirc import world, utils, conf, classes
|
||||||
|
from pylinkirc.log import log
|
||||||
|
|
||||||
|
def remove_network(ircobj):
|
||||||
|
"""Removes a network object from the pool."""
|
||||||
|
# Disable autoconnect first by setting the delay negative.
|
||||||
|
ircobj.serverdata['autoconnect'] = -1
|
||||||
|
ircobj.disconnect()
|
||||||
|
del world.networkobjects[ircobj.name]
|
||||||
|
|
||||||
|
def _shutdown(irc=None):
|
||||||
|
"""Shuts down the Pylink daemon."""
|
||||||
|
for name, plugin in world.plugins.items():
|
||||||
|
# Before closing connections, tell all plugins to shutdown cleanly first.
|
||||||
|
if hasattr(plugin, 'die'):
|
||||||
|
log.debug('coremods.control: Running die() on plugin %s due to shutdown.', name)
|
||||||
|
try:
|
||||||
|
plugin.die(irc)
|
||||||
|
except: # But don't allow it to crash the server.
|
||||||
|
log.exception('coremods.control: Error occurred in die() of plugin %s, skipping...', name)
|
||||||
|
|
||||||
|
for ircobj in world.networkobjects.copy().values():
|
||||||
|
# Disconnect all our networks.
|
||||||
|
remove_network(ircobj)
|
||||||
|
|
||||||
|
def sigterm_handler(_signo, _stack_frame):
|
||||||
|
"""Handles SIGTERM gracefully by shutting down the PyLink daemon."""
|
||||||
|
log.info("Shutting down on SIGTERM.")
|
||||||
|
_shutdown()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, sigterm_handler)
|
||||||
|
|
||||||
|
def _rehash():
|
||||||
|
"""Rehashes the PyLink daemon."""
|
||||||
|
old_conf = conf.conf.copy()
|
||||||
|
fname = conf.fname
|
||||||
|
new_conf = conf.loadConf(fname, errors_fatal=False)
|
||||||
|
new_conf = conf.validateConf(new_conf)
|
||||||
|
conf.conf = new_conf
|
||||||
|
for network, ircobj in world.networkobjects.copy().items():
|
||||||
|
# Server was removed from the config file, disconnect them.
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
ircobj.conf = new_conf
|
||||||
|
ircobj.serverdata = new_conf['servers'][network]
|
||||||
|
ircobj.botdata = new_conf['bot']
|
||||||
|
|
||||||
|
# Clear the IRC object's channel loggers and replace them with
|
||||||
|
# new ones by re-running logSetup().
|
||||||
|
while ircobj.loghandlers:
|
||||||
|
log.removeHandler(ircobj.loghandlers.pop())
|
||||||
|
|
||||||
|
ircobj.logSetup()
|
||||||
|
|
||||||
|
# TODO: update file loggers here too.
|
||||||
|
|
||||||
|
for network, sdata in new_conf['servers'].items():
|
||||||
|
# Connect any new networks or disconnected networks if they aren't already.
|
||||||
|
if (network not in world.networkobjects) or (not world.networkobjects[network].connection_thread.is_alive()):
|
||||||
|
proto = utils.getProtocolModule(sdata['protocol'])
|
||||||
|
world.networkobjects[network] = classes.Irc(network, proto, new_conf)
|
||||||
|
|
||||||
|
if os.name == 'posix':
|
||||||
|
# Only register SIGHUP on *nix.
|
||||||
|
def sighup_handler(_signo, _stack_frame):
|
||||||
|
"""Handles SIGHUP by rehashing the PyLink daemon."""
|
||||||
|
log.info("SIGHUP received, reloading config.")
|
||||||
|
_rehash()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGHUP, sighup_handler)
|
175
coremods/corecommands.py
Normal file
175
coremods/corecommands.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
"""
|
||||||
|
corecommands.py - Implements core PyLink commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import gc
|
||||||
|
import sys
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
from . import control
|
||||||
|
from pylinkirc import utils, world
|
||||||
|
from pylinkirc.log import log
|
||||||
|
|
||||||
|
# Essential, core commands go here so that the "commands" plugin with less-important,
|
||||||
|
# but still generic functions can be reloaded.
|
||||||
|
|
||||||
|
@utils.add_cmd
|
||||||
|
def identify(irc, source, args):
|
||||||
|
"""<username> <password>
|
||||||
|
|
||||||
|
Logs in to PyLink using the configured administrator account."""
|
||||||
|
if utils.isChannel(irc.called_by):
|
||||||
|
irc.reply('Error: This command must be sent in private. '
|
||||||
|
'(Would you really type a password inside a channel?)')
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
username, password = args[0], args[1]
|
||||||
|
except IndexError:
|
||||||
|
irc.msg(source, 'Error: Not enough arguments.')
|
||||||
|
return
|
||||||
|
# Usernames are case-insensitive, passwords are NOT.
|
||||||
|
if username.lower() == irc.conf['login']['user'].lower() and password == irc.conf['login']['password']:
|
||||||
|
realuser = irc.conf['login']['user']
|
||||||
|
irc.users[source].identified = realuser
|
||||||
|
irc.msg(source, 'Successfully logged in as %s.' % realuser)
|
||||||
|
log.info("(%s) Successful login to %r by %s",
|
||||||
|
irc.name, username, irc.getHostmask(source))
|
||||||
|
else:
|
||||||
|
irc.msg(source, 'Error: Incorrect credentials.')
|
||||||
|
u = irc.users[source]
|
||||||
|
log.warning("(%s) Failed login to %r from %s",
|
||||||
|
irc.name, username, irc.getHostmask(source))
|
||||||
|
|
||||||
|
@utils.add_cmd
|
||||||
|
def shutdown(irc, source, args):
|
||||||
|
"""takes no arguments.
|
||||||
|
|
||||||
|
Exits PyLink by disconnecting all networks."""
|
||||||
|
irc.checkAuthenticated(source, allowOper=False)
|
||||||
|
u = irc.users[source]
|
||||||
|
|
||||||
|
log.info('(%s) SHUTDOWN requested by "%s!%s@%s", exiting...', irc.name, u.nick,
|
||||||
|
u.ident, u.host)
|
||||||
|
|
||||||
|
control._shutdown(irc)
|
||||||
|
|
||||||
|
@utils.add_cmd
|
||||||
|
def load(irc, source, args):
|
||||||
|
"""<plugin name>.
|
||||||
|
|
||||||
|
Loads a plugin from the plugin folder."""
|
||||||
|
irc.checkAuthenticated(source, allowOper=False)
|
||||||
|
try:
|
||||||
|
name = args[0]
|
||||||
|
except IndexError:
|
||||||
|
irc.reply("Error: Not enough arguments. Needs 1: plugin name.")
|
||||||
|
return
|
||||||
|
if name in world.plugins:
|
||||||
|
irc.reply("Error: %r is already loaded." % name)
|
||||||
|
return
|
||||||
|
log.info('(%s) Loading plugin %r for %s', irc.name, name, irc.getHostmask(source))
|
||||||
|
try:
|
||||||
|
world.plugins[name] = pl = utils.loadPlugin(name)
|
||||||
|
except ImportError as e:
|
||||||
|
if str(e) == ('No module named %r' % name):
|
||||||
|
log.exception('Failed to load plugin %r: The plugin could not be found.', name)
|
||||||
|
else:
|
||||||
|
log.exception('Failed to load plugin %r: ImportError.', name)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
importlib.reload(pl)
|
||||||
|
if hasattr(pl, 'main'):
|
||||||
|
log.debug('Calling main() function of plugin %r', pl)
|
||||||
|
pl.main(irc)
|
||||||
|
irc.reply("Loaded plugin %r." % name)
|
||||||
|
|
||||||
|
@utils.add_cmd
|
||||||
|
def unload(irc, source, args):
|
||||||
|
"""<plugin name>.
|
||||||
|
|
||||||
|
Unloads a currently loaded plugin."""
|
||||||
|
irc.checkAuthenticated(source, allowOper=False)
|
||||||
|
try:
|
||||||
|
name = args[0]
|
||||||
|
except IndexError:
|
||||||
|
irc.reply("Error: Not enough arguments. Needs 1: plugin name.")
|
||||||
|
return
|
||||||
|
if name in world.plugins:
|
||||||
|
log.info('(%s) Unloading plugin %r for %s', irc.name, name, irc.getHostmask(source))
|
||||||
|
pl = world.plugins[name]
|
||||||
|
log.debug('sys.getrefcount of plugin %s is %s', pl, sys.getrefcount(pl))
|
||||||
|
|
||||||
|
# Remove any command functions defined by the plugin.
|
||||||
|
for cmdname, cmdfuncs in world.services['pylink'].commands.copy().items():
|
||||||
|
log.debug('cmdname=%s, cmdfuncs=%s', cmdname, cmdfuncs)
|
||||||
|
|
||||||
|
for cmdfunc in cmdfuncs:
|
||||||
|
log.debug('__module__ of cmdfunc %s is %s', cmdfunc, cmdfunc.__module__)
|
||||||
|
if cmdfunc.__module__ == name:
|
||||||
|
log.debug("Removing %s from world.services['pylink'].commands[%s]", cmdfunc, cmdname)
|
||||||
|
world.services['pylink'].commands[cmdname].remove(cmdfunc)
|
||||||
|
|
||||||
|
# If the cmdfunc list is empty, remove it.
|
||||||
|
if not cmdfuncs:
|
||||||
|
log.debug("Removing world.services['pylink'].commands[%s] (it's empty now)", cmdname)
|
||||||
|
del world.services['pylink'].commands[cmdname]
|
||||||
|
|
||||||
|
# Remove any command hooks set by the plugin.
|
||||||
|
for hookname, hookfuncs in world.hooks.copy().items():
|
||||||
|
for hookfunc in hookfuncs:
|
||||||
|
if hookfunc.__module__ == name:
|
||||||
|
world.hooks[hookname].remove(hookfunc)
|
||||||
|
# If the hookfuncs list is empty, remove it.
|
||||||
|
if not hookfuncs:
|
||||||
|
del world.hooks[hookname]
|
||||||
|
|
||||||
|
# Call the die() function in the plugin, if present.
|
||||||
|
if hasattr(pl, 'die'):
|
||||||
|
try:
|
||||||
|
pl.die(irc)
|
||||||
|
except: # But don't allow it to crash the server.
|
||||||
|
log.exception('(%s) Error occurred in die() of plugin %s, skipping...', irc.name, pl)
|
||||||
|
|
||||||
|
# Delete it from memory (hopefully).
|
||||||
|
del world.plugins[name]
|
||||||
|
if name in sys.modules:
|
||||||
|
del sys.modules[name]
|
||||||
|
if name in globals():
|
||||||
|
del globals()[name]
|
||||||
|
|
||||||
|
# Garbage collect.
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
irc.reply("Unloaded plugin %r." % name)
|
||||||
|
return True # We succeeded, make it clear (this status is used by reload() below)
|
||||||
|
else:
|
||||||
|
irc.reply("Unknown plugin %r." % name)
|
||||||
|
|
||||||
|
@utils.add_cmd
|
||||||
|
def reload(irc, source, args):
|
||||||
|
"""<plugin name>.
|
||||||
|
|
||||||
|
Loads a plugin from the plugin folder."""
|
||||||
|
try:
|
||||||
|
name = args[0]
|
||||||
|
except IndexError:
|
||||||
|
irc.reply("Error: Not enough arguments. Needs 1: plugin name.")
|
||||||
|
return
|
||||||
|
if unload(irc, source, args):
|
||||||
|
load(irc, source, args)
|
||||||
|
|
||||||
|
@utils.add_cmd
|
||||||
|
def rehash(irc, source, args):
|
||||||
|
"""takes no arguments.
|
||||||
|
|
||||||
|
Reloads the configuration file for PyLink, (dis)connecting added/removed networks.
|
||||||
|
Plugins must be manually reloaded."""
|
||||||
|
irc.checkAuthenticated(source, allowOper=False)
|
||||||
|
try:
|
||||||
|
control._rehash()
|
||||||
|
except Exception as e: # Something went wrong, abort.
|
||||||
|
log.exception("Error REHASHing config: ")
|
||||||
|
irc.reply("Error loading configuration file: %s: %s" % (type(e).__name__, e))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
irc.reply("Done.")
|
146
coremods/exttargets.py
Normal file
146
coremods/exttargets.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
"""
|
||||||
|
exttargets.py - Implements extended targets like $account:xyz, $oper, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pylinkirc import world
|
||||||
|
from pylinkirc.log import log
|
||||||
|
|
||||||
|
def bind(func):
|
||||||
|
"""
|
||||||
|
Binds an exttarget with the given name.
|
||||||
|
"""
|
||||||
|
world.exttarget_handlers[func.__name__] = func
|
||||||
|
return func
|
||||||
|
|
||||||
|
@bind
|
||||||
|
def account(irc, host, uid):
|
||||||
|
"""
|
||||||
|
$account exttarget handler. The following forms are supported, with groups separated by a
|
||||||
|
literal colon. Account matching is case insensitive, while network name matching IS case
|
||||||
|
sensitive.
|
||||||
|
|
||||||
|
$account -> Returns True (a match) if the target is registered.
|
||||||
|
$account:accountname -> Returns True if the target's account name matches the one given, and the
|
||||||
|
target is connected to the local network..
|
||||||
|
$account:accountname:netname -> Returns True if both the target's account name and origin
|
||||||
|
network name match the ones given.
|
||||||
|
$account:*:netname -> Matches all logged in users on the given network.
|
||||||
|
"""
|
||||||
|
userobj = irc.users[uid]
|
||||||
|
homenet = irc.name
|
||||||
|
if hasattr(userobj, 'remote'):
|
||||||
|
# User is a PyLink Relay pseudoclient. Use their real services account on their
|
||||||
|
# origin network.
|
||||||
|
homenet, realuid = userobj.remote
|
||||||
|
log.debug('(%s) exttargets.account: Changing UID of relay client %s to %s/%s', irc.name,
|
||||||
|
uid, homenet, realuid)
|
||||||
|
try:
|
||||||
|
userobj = world.networkobjects[homenet].users[realuid]
|
||||||
|
except KeyError: # User lookup failed. Bail and return False.
|
||||||
|
log.exception('(%s) exttargets.account: KeyError finding %s/%s:', irc.name,
|
||||||
|
homenet, realuid)
|
||||||
|
return False
|
||||||
|
|
||||||
|
slogin = irc.toLower(userobj.services_account)
|
||||||
|
|
||||||
|
# Split the given exttarget host into parts, so we know how many to look for.
|
||||||
|
groups = list(map(irc.toLower, host.split(':')))
|
||||||
|
log.debug('(%s) exttargets.account: groups to match: %s', irc.name, groups)
|
||||||
|
|
||||||
|
if len(groups) == 1:
|
||||||
|
# First scenario. Return True if user is logged in.
|
||||||
|
return bool(slogin)
|
||||||
|
elif len(groups) == 2:
|
||||||
|
# Second scenario. Return True if the user's account matches the one given.
|
||||||
|
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 (groups[1] in ('*', slogin)) and (homenet == groups[2])
|
||||||
|
|
||||||
|
@bind
|
||||||
|
def ircop(irc, host, uid):
|
||||||
|
"""
|
||||||
|
$ircop exttarget handler. The following forms are supported, with groups separated by a
|
||||||
|
literal colon. Oper types are matched case insensitively.
|
||||||
|
|
||||||
|
$ircop -> Returns True (a match) if the target is opered.
|
||||||
|
$ircop:*admin* -> Returns True if the target's is opered and their opertype matches the glob
|
||||||
|
given.
|
||||||
|
"""
|
||||||
|
groups = host.split(':')
|
||||||
|
log.debug('(%s) exttargets.ircop: groups to match: %s', irc.name, groups)
|
||||||
|
|
||||||
|
if len(groups) == 1:
|
||||||
|
# 1st scenario.
|
||||||
|
return irc.isOper(uid, allowAuthed=False)
|
||||||
|
else:
|
||||||
|
# 2nd scenario. Use matchHost (ircmatch) to match the opertype glob to the opertype.
|
||||||
|
return irc.matchHost(groups[1], irc.users[uid].opertype)
|
||||||
|
|
||||||
|
@bind
|
||||||
|
def server(irc, host, uid):
|
||||||
|
"""
|
||||||
|
$server exttarget handler. The following forms are supported, with groups separated by a
|
||||||
|
literal colon. Server names are matched case insensitively, but SIDs ARE case sensitive.
|
||||||
|
|
||||||
|
$server:server.name -> Returns True (a match) if the target is connected on the given server.
|
||||||
|
$server:server.glob -> Returns True (a match) if the target is connected on a server matching the glob.
|
||||||
|
$server:1XY -> Returns True if the target's is connected on the server with the given SID.
|
||||||
|
"""
|
||||||
|
groups = host.split(':')
|
||||||
|
log.debug('(%s) exttargets.server: groups to match: %s', irc.name, groups)
|
||||||
|
|
||||||
|
if len(groups) >= 2:
|
||||||
|
sid = irc.getServer(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.matchHost(query, irc.getFriendlyName(sid))
|
||||||
|
# $server alone is invalid. Don't match anything.
|
||||||
|
return False
|
||||||
|
|
||||||
|
@bind
|
||||||
|
def channel(irc, host, uid):
|
||||||
|
"""
|
||||||
|
$channel exttarget handler. The following forms are supported, with groups separated by a
|
||||||
|
literal colon. Channel names are matched case insensitively.
|
||||||
|
|
||||||
|
$channel:#channel -> Returns True if the target is in the given channel.
|
||||||
|
$channel:#channel:op -> Returns True if the target is in the given channel, and is opped.
|
||||||
|
Any other supported prefix (owner, admin, op, halfop, voice) can be given, but only one at a
|
||||||
|
time.
|
||||||
|
"""
|
||||||
|
groups = host.split(':')
|
||||||
|
log.debug('(%s) exttargets.channel: groups to match: %s', irc.name, groups)
|
||||||
|
try:
|
||||||
|
channel = groups[1]
|
||||||
|
except IndexError: # No channel given, abort.
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(groups) == 2:
|
||||||
|
# Just #channel was given as query
|
||||||
|
return uid in irc.channels[channel].users
|
||||||
|
elif len(groups) >= 3:
|
||||||
|
# For things like #channel:op, check if the query is in the user's prefix modes.
|
||||||
|
return groups[2].lower() in irc.channels[channel].getPrefixModes(uid)
|
||||||
|
|
||||||
|
@bind
|
||||||
|
def pylinkacc(irc, host, uid):
|
||||||
|
"""
|
||||||
|
$pylinkacc (PyLink account) exttarget handler. The following forms are supported, with groups
|
||||||
|
separated by a literal colon. Account matching is case insensitive.
|
||||||
|
|
||||||
|
$pylinkacc -> Returns True if the target is logged in to PyLink.
|
||||||
|
$pylinkacc:accountname -> Returns True if the target's PyLink login matches the one given.
|
||||||
|
"""
|
||||||
|
login = irc.toLower(irc.users[uid].identified)
|
||||||
|
groups = list(map(irc.toLower, host.split(':')))
|
||||||
|
log.debug('(%s) exttargets.pylinkacc: groups to match: %s', irc.name, groups)
|
||||||
|
|
||||||
|
if len(groups) == 1:
|
||||||
|
# First scenario. Return True if user is logged in.
|
||||||
|
return bool(login)
|
||||||
|
elif len(groups) == 2:
|
||||||
|
# Second scenario. Return True if the user's login matches the one given.
|
||||||
|
return login == groups[1]
|
137
coremods/handlers.py
Normal file
137
coremods/handlers.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
"""
|
||||||
|
handlers.py - Implements miscellaneous IRC command handlers (WHOIS, services login, etc.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pylinkirc import utils
|
||||||
|
from pylinkirc.log import log
|
||||||
|
|
||||||
|
def handle_whois(irc, source, command, args):
|
||||||
|
"""Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd)."""
|
||||||
|
target = args['target']
|
||||||
|
user = irc.users.get(target)
|
||||||
|
if user is None:
|
||||||
|
log.warning('(%s) Got a WHOIS request for %r from %r, but the target '
|
||||||
|
'doesn\'t exist in irc.users!', irc.name, target, source)
|
||||||
|
return
|
||||||
|
f = irc.proto.numeric
|
||||||
|
server = irc.getServer(target) or irc.sid
|
||||||
|
nick = user.nick
|
||||||
|
sourceisOper = ('o', None) in irc.users[source].modes
|
||||||
|
|
||||||
|
# Get the full network name.
|
||||||
|
netname = irc.serverdata.get('netname', irc.name)
|
||||||
|
|
||||||
|
# https://www.alien.net.au/irc/irc2numerics.html
|
||||||
|
# 311: sends nick!user@host information
|
||||||
|
f(server, 311, source, "%s %s %s * :%s" % (nick, user.ident, user.host, user.realname))
|
||||||
|
|
||||||
|
# 319: RPL_WHOISCHANNELS; Show public channels of the target, respecting
|
||||||
|
# hidechans umodes for non-oper callers.
|
||||||
|
isHideChans = (irc.umodes.get('hidechans'), None) in user.modes
|
||||||
|
if (not isHideChans) or (isHideChans and sourceisOper):
|
||||||
|
public_chans = []
|
||||||
|
for chan in user.channels:
|
||||||
|
c = irc.channels[chan]
|
||||||
|
# Here, we'll want to hide secret/private channels from non-opers
|
||||||
|
# who are not in them.
|
||||||
|
|
||||||
|
if ((irc.cmodes.get('secret'), None) in c.modes or \
|
||||||
|
(irc.cmodes.get('private'), None) in c.modes) \
|
||||||
|
and not (sourceisOper or source in c.users):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Show prefix modes like a regular IRCd does.
|
||||||
|
for prefixmode in c.getPrefixModes(target):
|
||||||
|
modechar = irc.cmodes[prefixmode]
|
||||||
|
chan = irc.prefixmodes[modechar] + chan
|
||||||
|
|
||||||
|
public_chans.append(chan)
|
||||||
|
|
||||||
|
if public_chans: # Only send the line if the person is in any visible channels...
|
||||||
|
f(server, 319, source, '%s :%s' % (nick, ' '.join(public_chans)))
|
||||||
|
|
||||||
|
# 312: sends the server the target is on, and its server description.
|
||||||
|
f(server, 312, source, "%s %s :%s" % (nick, irc.servers[server].name,
|
||||||
|
irc.servers[server].desc))
|
||||||
|
|
||||||
|
# 313: sends a string denoting the target's operator privilege if applicable.
|
||||||
|
if ('o', None) in user.modes:
|
||||||
|
# Check hideoper status. Require that either:
|
||||||
|
# 1) +H is not set
|
||||||
|
# 2) +H is set, but the caller is oper
|
||||||
|
# 3) +H is set, but whois_use_hideoper is disabled in config
|
||||||
|
isHideOper = (irc.umodes.get('hideoper'), None) in user.modes
|
||||||
|
if (not isHideOper) or (isHideOper and sourceisOper) or \
|
||||||
|
(isHideOper and not irc.botdata.get('whois_use_hideoper', True)):
|
||||||
|
# Let's be gramatically correct. (If the opertype starts with a vowel,
|
||||||
|
# write "an Operator" instead of "a Operator")
|
||||||
|
n = 'n' if user.opertype[0].lower() in 'aeiou' else ''
|
||||||
|
|
||||||
|
# I want to normalize the syntax: PERSON is an OPERTYPE on NETWORKNAME.
|
||||||
|
# This is the only syntax InspIRCd supports, but for others it doesn't
|
||||||
|
# really matter since we're handling the WHOIS requests by ourselves.
|
||||||
|
f(server, 313, source, "%s :is a%s %s on %s" % (nick, n, user.opertype, netname))
|
||||||
|
|
||||||
|
# 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd to show user modes.
|
||||||
|
# Only show this to opers!
|
||||||
|
if sourceisOper:
|
||||||
|
f(server, 378, source, "%s :is connecting from %s@%s %s" % (nick, user.ident, user.realhost, user.ip))
|
||||||
|
f(server, 379, source, '%s :is using modes %s' % (nick, irc.joinModes(user.modes)))
|
||||||
|
|
||||||
|
# 301: used to show away information if present
|
||||||
|
away_text = user.away
|
||||||
|
log.debug('(%s) coremods.handlers.handle_whois: away_text for %s is %r', irc.name, target, away_text)
|
||||||
|
if away_text:
|
||||||
|
f(server, 301, source, '%s :%s' % (nick, away_text))
|
||||||
|
|
||||||
|
# 317: shows idle and signon time. However, we don't track the user's real
|
||||||
|
# idle time, so we simply return 0.
|
||||||
|
# <- 317 GL GL 15 1437632859 :seconds idle, signon time
|
||||||
|
f(server, 317, source, "%s 0 %s :seconds idle, signon time" % (nick, user.ts))
|
||||||
|
|
||||||
|
if (irc.umodes.get('bot'), None) in user.modes:
|
||||||
|
# Show botmode info in WHOIS.
|
||||||
|
f(server, 335, source, "%s :is a bot" % nick)
|
||||||
|
|
||||||
|
# Call custom WHOIS handlers via the PYLINK_CUSTOM_WHOIS hook.
|
||||||
|
irc.callHooks([source, 'PYLINK_CUSTOM_WHOIS', {'target': target, 'server': server}])
|
||||||
|
|
||||||
|
# 318: End of WHOIS.
|
||||||
|
f(server, 318, source, "%s :End of /WHOIS list" % nick)
|
||||||
|
utils.add_hook(handle_whois, 'WHOIS')
|
||||||
|
|
||||||
|
def handle_mode(irc, source, command, args):
|
||||||
|
"""Protect against forced deoper attempts."""
|
||||||
|
target = args['target']
|
||||||
|
modes = args['modes']
|
||||||
|
# If the sender is not a PyLink client, and the target IS a protected
|
||||||
|
# client, revert any forced deoper attempts.
|
||||||
|
if irc.isInternalClient(target) and not irc.isInternalClient(source):
|
||||||
|
if ('-o', None) in modes and (target == irc.pseudoclient.uid or not irc.isManipulatableClient(target)):
|
||||||
|
irc.proto.mode(irc.sid, target, {('+o', None)})
|
||||||
|
utils.add_hook(handle_mode, 'MODE')
|
||||||
|
|
||||||
|
def handle_operup(irc, source, command, args):
|
||||||
|
"""Logs successful oper-ups on networks."""
|
||||||
|
otype = args.get('text', 'IRC Operator')
|
||||||
|
log.debug("(%s) Successful oper-up (opertype %r) from %s", irc.name, otype, irc.getHostmask(source))
|
||||||
|
irc.users[source].opertype = otype
|
||||||
|
|
||||||
|
utils.add_hook(handle_operup, 'CLIENT_OPERED')
|
||||||
|
|
||||||
|
def handle_services_login(irc, source, command, args):
|
||||||
|
"""Sets services login status for users."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
irc.users[source].services_account = args['text']
|
||||||
|
except KeyError: # User doesn't exist
|
||||||
|
log.debug("(%s) Ignoring early account name setting for %s (UID hasn't been sent yet)", irc.name, source)
|
||||||
|
|
||||||
|
utils.add_hook(handle_services_login, 'CLIENT_SERVICES_LOGIN')
|
||||||
|
|
||||||
|
def handle_version(irc, source, command, args):
|
||||||
|
"""Handles requests for the PyLink server version."""
|
||||||
|
# 351 syntax is usually "<server version>. <server hostname> :<anything else you want to add>
|
||||||
|
fullversion = irc.version()
|
||||||
|
irc.proto.numeric(irc.sid, 351, source, fullversion)
|
||||||
|
utils.add_hook(handle_version, 'VERSION')
|
125
coremods/service_support.py
Normal file
125
coremods/service_support.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
service_support.py - Implements handlers for the PyLink ServiceBot API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pylinkirc import utils, world, conf
|
||||||
|
from pylinkirc.log import log
|
||||||
|
|
||||||
|
def spawn_service(irc, source, command, args):
|
||||||
|
"""Handles new service bot introductions."""
|
||||||
|
|
||||||
|
if not irc.connected.is_set():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Service name
|
||||||
|
name = args['name']
|
||||||
|
|
||||||
|
# Get the ServiceBot object.
|
||||||
|
sbot = world.services[name]
|
||||||
|
|
||||||
|
# Look up the nick or ident in the following order:
|
||||||
|
# 1) Network specific nick/ident settings for this service (servers::irc.name::servicename_nick)
|
||||||
|
# 2) Global settings for this service (servicename::nick)
|
||||||
|
# 3) The preferred nick/ident combination defined by the plugin (sbot.nick / sbot.ident)
|
||||||
|
# 4) The literal service name.
|
||||||
|
# settings, and then falling back to the literal service name.
|
||||||
|
nick = irc.serverdata.get("%s_nick" % name) or irc.conf.get(name, {}).get('nick') or sbot.nick or name
|
||||||
|
ident = irc.serverdata.get("%s_ident" % name) or irc.conf.get(name, {}).get('ident') or sbot.ident or name
|
||||||
|
|
||||||
|
# TODO: make this configurable?
|
||||||
|
host = irc.serverdata["hostname"]
|
||||||
|
|
||||||
|
# Spawning service clients with these umodes where supported. servprotect usage is a
|
||||||
|
# configuration option.
|
||||||
|
preferred_modes = ['oper', 'hideoper', 'hidechans', 'invisible', 'bot']
|
||||||
|
modes = []
|
||||||
|
|
||||||
|
if conf.conf['bot'].get('protect_services'):
|
||||||
|
preferred_modes.append('servprotect')
|
||||||
|
|
||||||
|
for mode in preferred_modes:
|
||||||
|
mode = irc.umodes.get(mode)
|
||||||
|
if mode:
|
||||||
|
modes.append((mode, None))
|
||||||
|
|
||||||
|
# Track the service's UIDs on each network.
|
||||||
|
userobj = irc.proto.spawnClient(nick, ident, host, modes=modes, opertype="PyLink Service",
|
||||||
|
manipulatable=sbot.manipulatable)
|
||||||
|
|
||||||
|
sbot.uids[irc.name] = u = userobj.uid
|
||||||
|
|
||||||
|
# Special case: if this is the main PyLink client being spawned,
|
||||||
|
# assign this as irc.pseudoclient.
|
||||||
|
if name == 'pylink':
|
||||||
|
irc.pseudoclient = userobj
|
||||||
|
|
||||||
|
# TODO: channels should be tracked in a central database, not hardcoded
|
||||||
|
# in conf.
|
||||||
|
channels = set(irc.serverdata.get('channels', [])) | sbot.extra_channels.get(irc.name, set())
|
||||||
|
|
||||||
|
for chan in channels:
|
||||||
|
irc.proto.join(u, chan)
|
||||||
|
irc.callHooks([irc.sid, 'PYLINK_SERVICE_JOIN', {'channel': chan, 'users': [u]}])
|
||||||
|
|
||||||
|
utils.add_hook(spawn_service, 'PYLINK_NEW_SERVICE')
|
||||||
|
|
||||||
|
def handle_disconnect(irc, source, command, args):
|
||||||
|
"""Handles network disconnections."""
|
||||||
|
for name, sbot in world.services.items():
|
||||||
|
try:
|
||||||
|
del sbot.uids[irc.name]
|
||||||
|
log.debug("coremods.service_support: removing uids[%s] from service bot %s", irc.name, sbot.name)
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
utils.add_hook(handle_disconnect, 'PYLINK_DISCONNECT')
|
||||||
|
|
||||||
|
def handle_endburst(irc, source, command, args):
|
||||||
|
"""Handles network bursts."""
|
||||||
|
if source == irc.uplink:
|
||||||
|
log.debug('(%s): spawning service bots now.', irc.name)
|
||||||
|
|
||||||
|
# We just connected. Burst all our registered services.
|
||||||
|
for name, sbot in world.services.items():
|
||||||
|
spawn_service(irc, source, command, {'name': name})
|
||||||
|
|
||||||
|
utils.add_hook(handle_endburst, 'ENDBURST')
|
||||||
|
|
||||||
|
def handle_kill(irc, source, command, args):
|
||||||
|
"""Handle KILLs to PyLink service bots, respawning them as needed."""
|
||||||
|
target = args['target']
|
||||||
|
sbot = irc.isServiceBot(target)
|
||||||
|
if sbot:
|
||||||
|
spawn_service(irc, source, command, {'name': sbot.name})
|
||||||
|
return
|
||||||
|
utils.add_hook(handle_kill, 'KILL')
|
||||||
|
|
||||||
|
def handle_kick(irc, source, command, args):
|
||||||
|
"""Handle KICKs to the PyLink service bots, rejoining channels as needed."""
|
||||||
|
kicked = args['target']
|
||||||
|
channel = args['channel']
|
||||||
|
if irc.isServiceBot(kicked):
|
||||||
|
irc.proto.join(kicked, channel)
|
||||||
|
irc.callHooks([irc.sid, 'PYLINK_SERVICE_JOIN', {'channel': channel, 'users': [kicked]}])
|
||||||
|
utils.add_hook(handle_kick, 'KICK')
|
||||||
|
|
||||||
|
def handle_commands(irc, source, command, args):
|
||||||
|
"""Handle commands sent to the PyLink service bots (PRIVMSG)."""
|
||||||
|
target = args['target']
|
||||||
|
text = args['text']
|
||||||
|
|
||||||
|
sbot = irc.isServiceBot(target)
|
||||||
|
if sbot:
|
||||||
|
sbot.call_cmd(irc, source, text)
|
||||||
|
|
||||||
|
utils.add_hook(handle_commands, 'PRIVMSG')
|
||||||
|
|
||||||
|
# Register the main PyLink service. All command definitions MUST go after this!
|
||||||
|
mynick = conf.conf['bot'].get("nick", "PyLink")
|
||||||
|
myident = conf.conf['bot'].get("ident", "pylink")
|
||||||
|
|
||||||
|
# TODO: be more specific, and possibly allow plugins to modify this to mention
|
||||||
|
# their features?
|
||||||
|
mydesc = "\x02%s\x02 provides extended network services for IRC." % mynick
|
||||||
|
|
||||||
|
utils.registerService('pylink', nick=mynick, ident=myident, desc=mydesc, manipulatable=True)
|
482
coreplugin.py
482
coreplugin.py
@ -1,482 +0,0 @@
|
|||||||
"""
|
|
||||||
coreplugin.py - Implements core PyLink functions as a plugin.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import gc
|
|
||||||
import sys
|
|
||||||
import signal
|
|
||||||
import os
|
|
||||||
|
|
||||||
import utils
|
|
||||||
import conf
|
|
||||||
import classes
|
|
||||||
from log import log
|
|
||||||
import world
|
|
||||||
|
|
||||||
def _shutdown(irc=None):
|
|
||||||
"""Shuts down the Pylink daemon."""
|
|
||||||
for name, plugin in world.plugins.items():
|
|
||||||
# Before closing connections, tell all plugins to shutdown cleanly first.
|
|
||||||
if hasattr(plugin, 'die'):
|
|
||||||
log.debug('coreplugin: Running die() on plugin %s due to shutdown.', name)
|
|
||||||
try:
|
|
||||||
plugin.die(irc)
|
|
||||||
except: # But don't allow it to crash the server.
|
|
||||||
log.exception('coreplugin: Error occurred in die() of plugin %s, skipping...', name)
|
|
||||||
|
|
||||||
for ircobj in world.networkobjects.values():
|
|
||||||
# Disconnect all our networks. Disable auto-connect first by setting
|
|
||||||
# the time to negative.
|
|
||||||
ircobj.serverdata['autoconnect'] = -1
|
|
||||||
ircobj.disconnect()
|
|
||||||
|
|
||||||
def sigterm_handler(_signo, _stack_frame):
|
|
||||||
"""Handles SIGTERM gracefully by shutting down the PyLink daemon."""
|
|
||||||
log.info("Shutting down on SIGTERM.")
|
|
||||||
_shutdown()
|
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, sigterm_handler)
|
|
||||||
|
|
||||||
def handle_kill(irc, source, command, args):
|
|
||||||
"""Handle KILLs to PyLink service bots, respawning them as needed."""
|
|
||||||
target = args['target']
|
|
||||||
sbot = irc.isServiceBot(target)
|
|
||||||
if sbot:
|
|
||||||
spawn_service(irc, source, command, {'name': sbot.name})
|
|
||||||
return
|
|
||||||
utils.add_hook(handle_kill, 'KILL')
|
|
||||||
|
|
||||||
def handle_kick(irc, source, command, args):
|
|
||||||
"""Handle KICKs to the PyLink service bots, rejoining channels as needed."""
|
|
||||||
kicked = args['target']
|
|
||||||
channel = args['channel']
|
|
||||||
if irc.isServiceBot(kicked):
|
|
||||||
irc.proto.join(kicked, channel)
|
|
||||||
utils.add_hook(handle_kick, 'KICK')
|
|
||||||
|
|
||||||
def handle_commands(irc, source, command, args):
|
|
||||||
"""Handle commands sent to the PyLink service bots (PRIVMSG)."""
|
|
||||||
target = args['target']
|
|
||||||
text = args['text']
|
|
||||||
|
|
||||||
sbot = irc.isServiceBot(target)
|
|
||||||
if sbot:
|
|
||||||
sbot.call_cmd(irc, source, text)
|
|
||||||
|
|
||||||
utils.add_hook(handle_commands, 'PRIVMSG')
|
|
||||||
|
|
||||||
def handle_whois(irc, source, command, args):
|
|
||||||
"""Handle WHOIS queries, for IRCds that send them across servers (charybdis, UnrealIRCd; NOT InspIRCd)."""
|
|
||||||
target = args['target']
|
|
||||||
user = irc.users.get(target)
|
|
||||||
if user is None:
|
|
||||||
log.warning('(%s) Got a WHOIS request for %r from %r, but the target '
|
|
||||||
'doesn\'t exist in irc.users!', irc.name, target, source)
|
|
||||||
return
|
|
||||||
f = irc.proto.numeric
|
|
||||||
server = irc.getServer(target) or irc.sid
|
|
||||||
nick = user.nick
|
|
||||||
sourceisOper = ('o', None) in irc.users[source].modes
|
|
||||||
|
|
||||||
# Get the full network name.
|
|
||||||
netname = irc.serverdata.get('netname', irc.name)
|
|
||||||
|
|
||||||
# https://www.alien.net.au/irc/irc2numerics.html
|
|
||||||
# 311: sends nick!user@host information
|
|
||||||
f(server, 311, source, "%s %s %s * :%s" % (nick, user.ident, user.host, user.realname))
|
|
||||||
|
|
||||||
# 319: RPL_WHOISCHANNELS; Show public channels of the target, respecting
|
|
||||||
# hidechans umodes for non-oper callers.
|
|
||||||
isHideChans = (irc.umodes.get('hidechans'), None) in user.modes
|
|
||||||
if (not isHideChans) or (isHideChans and sourceisOper):
|
|
||||||
public_chans = []
|
|
||||||
for chan in user.channels:
|
|
||||||
c = irc.channels[chan]
|
|
||||||
# Here, we'll want to hide secret/private channels from non-opers
|
|
||||||
# who are not in them.
|
|
||||||
|
|
||||||
if ((irc.cmodes.get('secret'), None) in c.modes or \
|
|
||||||
(irc.cmodes.get('private'), None) in c.modes) \
|
|
||||||
and not (sourceisOper or source in c.users):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Show prefix modes like a regular IRCd does.
|
|
||||||
for prefixmode in c.getPrefixModes(target):
|
|
||||||
modechar = irc.cmodes[prefixmode]
|
|
||||||
chan = irc.prefixmodes[modechar] + chan
|
|
||||||
|
|
||||||
public_chans.append(chan)
|
|
||||||
|
|
||||||
if public_chans: # Only send the line if the person is in any visible channels...
|
|
||||||
f(server, 319, source, '%s :%s' % (nick, ' '.join(public_chans)))
|
|
||||||
|
|
||||||
# 312: sends the server the target is on, and its server description.
|
|
||||||
f(server, 312, source, "%s %s :%s" % (nick, irc.servers[server].name,
|
|
||||||
irc.servers[server].desc))
|
|
||||||
|
|
||||||
# 313: sends a string denoting the target's operator privilege if applicable.
|
|
||||||
if ('o', None) in user.modes:
|
|
||||||
# Check hideoper status. Require that either:
|
|
||||||
# 1) +H is not set
|
|
||||||
# 2) +H is set, but the caller is oper
|
|
||||||
# 3) +H is set, but whois_use_hideoper is disabled in config
|
|
||||||
isHideOper = (irc.umodes.get('hideoper'), None) in user.modes
|
|
||||||
if (not isHideOper) or (isHideOper and sourceisOper) or \
|
|
||||||
(isHideOper and not irc.botdata.get('whois_use_hideoper', True)):
|
|
||||||
# Let's be gramatically correct. (If the opertype starts with a vowel,
|
|
||||||
# write "an Operator" instead of "a Operator")
|
|
||||||
n = 'n' if user.opertype[0].lower() in 'aeiou' else ''
|
|
||||||
|
|
||||||
# I want to normalize the syntax: PERSON is an OPERTYPE on NETWORKNAME.
|
|
||||||
# This is the only syntax InspIRCd supports, but for others it doesn't
|
|
||||||
# really matter since we're handling the WHOIS requests by ourselves.
|
|
||||||
f(server, 313, source, "%s :is a%s %s on %s" % (nick, n, user.opertype, netname))
|
|
||||||
|
|
||||||
# 379: RPL_WHOISMODES, used by UnrealIRCd and InspIRCd to show user modes.
|
|
||||||
# Only show this to opers!
|
|
||||||
if sourceisOper:
|
|
||||||
f(server, 378, source, "%s :is connecting from %s@%s %s" % (nick, user.ident, user.realhost, user.ip))
|
|
||||||
f(server, 379, source, '%s :is using modes %s' % (nick, irc.joinModes(user.modes)))
|
|
||||||
|
|
||||||
# 301: used to show away information if present
|
|
||||||
away_text = user.away
|
|
||||||
log.debug('(%s) coreplugin/handle_whois: away_text for %s is %r', irc.name, target, away_text)
|
|
||||||
if away_text:
|
|
||||||
f(server, 301, source, '%s :%s' % (nick, away_text))
|
|
||||||
|
|
||||||
# 317: shows idle and signon time. However, we don't track the user's real
|
|
||||||
# idle time, so we simply return 0.
|
|
||||||
# <- 317 GL GL 15 1437632859 :seconds idle, signon time
|
|
||||||
f(server, 317, source, "%s 0 %s :seconds idle, signon time" % (nick, user.ts))
|
|
||||||
|
|
||||||
if (irc.umodes.get('bot'), None) in user.modes:
|
|
||||||
# Show botmode info in WHOIS.
|
|
||||||
f(server, 335, source, "%s :is a bot" % nick)
|
|
||||||
|
|
||||||
# Call custom WHOIS handlers via the PYLINK_CUSTOM_WHOIS hook.
|
|
||||||
irc.callHooks([source, 'PYLINK_CUSTOM_WHOIS', {'target': target, 'server': server}])
|
|
||||||
|
|
||||||
# 318: End of WHOIS.
|
|
||||||
f(server, 318, source, "%s :End of /WHOIS list" % nick)
|
|
||||||
utils.add_hook(handle_whois, 'WHOIS')
|
|
||||||
|
|
||||||
def handle_mode(irc, source, command, args):
|
|
||||||
"""Protect against forced deoper attempts."""
|
|
||||||
target = args['target']
|
|
||||||
modes = args['modes']
|
|
||||||
# If the sender is not a PyLink client, and the target IS a protected
|
|
||||||
# client, revert any forced deoper attempts.
|
|
||||||
if irc.isInternalClient(target) and not irc.isInternalClient(source):
|
|
||||||
if ('-o', None) in modes and (target == irc.pseudoclient.uid or not irc.isManipulatableClient(target)):
|
|
||||||
irc.proto.mode(irc.sid, target, {('+o', None)})
|
|
||||||
utils.add_hook(handle_mode, 'MODE')
|
|
||||||
|
|
||||||
def handle_operup(irc, source, command, args):
|
|
||||||
"""Logs successful oper-ups on networks."""
|
|
||||||
otype = args.get('text', 'IRC Operator')
|
|
||||||
log.debug("(%s) Successful oper-up (opertype %r) from %s", irc.name, otype, irc.getHostmask(source))
|
|
||||||
irc.users[source].opertype = otype
|
|
||||||
|
|
||||||
utils.add_hook(handle_operup, 'CLIENT_OPERED')
|
|
||||||
|
|
||||||
def handle_services_login(irc, source, command, args):
|
|
||||||
"""Sets services login status for users."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
irc.users[source].services_account = args['text']
|
|
||||||
except KeyError: # User doesn't exist
|
|
||||||
log.debug("(%s) Ignoring early account name setting for %s (UID hasn't been sent yet)", irc.name, source)
|
|
||||||
|
|
||||||
utils.add_hook(handle_services_login, 'CLIENT_SERVICES_LOGIN')
|
|
||||||
|
|
||||||
def handle_version(irc, source, command, args):
|
|
||||||
"""Handles requests for the PyLink server version."""
|
|
||||||
# 351 syntax is usually "<server version>. <server hostname> :<anything else you want to add>
|
|
||||||
fullversion = irc.version()
|
|
||||||
irc.proto.numeric(irc.sid, 351, source, fullversion)
|
|
||||||
utils.add_hook(handle_version, 'VERSION')
|
|
||||||
|
|
||||||
def spawn_service(irc, source, command, args):
|
|
||||||
"""Handles new service bot introductions."""
|
|
||||||
|
|
||||||
if not irc.connected.is_set():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Service name
|
|
||||||
name = args['name']
|
|
||||||
|
|
||||||
# Get the ServiceBot object.
|
|
||||||
sbot = world.services[name]
|
|
||||||
|
|
||||||
# Look up the nick or ident in the following order:
|
|
||||||
# 1) Network specific nick/ident settings for this service (servers::irc.name::servicename_nick)
|
|
||||||
# 2) Global settings for this service (servicename::nick)
|
|
||||||
# 3) The preferred nick/ident combination defined by the plugin (sbot.nick / sbot.ident)
|
|
||||||
# 4) The literal service name.
|
|
||||||
# settings, and then falling back to the literal service name.
|
|
||||||
nick = irc.serverdata.get("%s_nick" % name) or irc.conf.get(name, {}).get('nick') or sbot.nick or name
|
|
||||||
ident = irc.serverdata.get("%s_ident" % name) or irc.conf.get(name, {}).get('ident') or sbot.ident or name
|
|
||||||
|
|
||||||
# TODO: make this configurable?
|
|
||||||
host = irc.serverdata["hostname"]
|
|
||||||
|
|
||||||
# Prefer spawning service clients with umode +io, plus hideoper and
|
|
||||||
# hidechans if supported.
|
|
||||||
modes = []
|
|
||||||
for mode in ('oper', 'hideoper', 'hidechans', 'invisible', 'bot'):
|
|
||||||
mode = irc.umodes.get(mode)
|
|
||||||
if mode:
|
|
||||||
modes.append((mode, None))
|
|
||||||
|
|
||||||
# Track the service's UIDs on each network.
|
|
||||||
userobj = irc.proto.spawnClient(nick, ident, host, modes=modes, opertype="PyLink Service",
|
|
||||||
manipulatable=sbot.manipulatable)
|
|
||||||
|
|
||||||
sbot.uids[irc.name] = u = userobj.uid
|
|
||||||
|
|
||||||
# Special case: if this is the main PyLink client being spawned,
|
|
||||||
# assign this as irc.pseudoclient.
|
|
||||||
if name == 'pylink':
|
|
||||||
irc.pseudoclient = userobj
|
|
||||||
|
|
||||||
# TODO: channels should be tracked in a central database, not hardcoded
|
|
||||||
# in conf.
|
|
||||||
for chan in irc.serverdata['channels']:
|
|
||||||
irc.proto.join(u, chan)
|
|
||||||
|
|
||||||
utils.add_hook(spawn_service, 'PYLINK_NEW_SERVICE')
|
|
||||||
|
|
||||||
def handle_disconnect(irc, source, command, args):
|
|
||||||
"""Handles network disconnections."""
|
|
||||||
for name, sbot in world.services.items():
|
|
||||||
try:
|
|
||||||
del sbot.uids[irc.name]
|
|
||||||
log.debug("coreplugin: removing uids[%s] from service bot %s", irc.name, sbot.name)
|
|
||||||
except KeyError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
utils.add_hook(handle_disconnect, 'PYLINK_DISCONNECT')
|
|
||||||
|
|
||||||
def handle_endburst(irc, source, command, args):
|
|
||||||
"""Handles network bursts."""
|
|
||||||
if source == irc.uplink:
|
|
||||||
log.debug('(%s): spawning service bots now.', irc.name)
|
|
||||||
|
|
||||||
# We just connected. Burst all our registered services.
|
|
||||||
for name, sbot in world.services.items():
|
|
||||||
spawn_service(irc, source, command, {'name': name})
|
|
||||||
|
|
||||||
utils.add_hook(handle_endburst, 'ENDBURST')
|
|
||||||
|
|
||||||
# Register the main PyLink service. All command definitions MUST go after this!
|
|
||||||
mynick = conf.conf['bot'].get("nick", "PyLink")
|
|
||||||
myident = conf.conf['bot'].get("ident", "pylink")
|
|
||||||
utils.registerService('pylink', nick=mynick, ident=myident, manipulatable=True)
|
|
||||||
|
|
||||||
# Essential, core commands go here so that the "commands" plugin with less-important,
|
|
||||||
# but still generic functions can be reloaded.
|
|
||||||
|
|
||||||
@utils.add_cmd
|
|
||||||
def identify(irc, source, args):
|
|
||||||
"""<username> <password>
|
|
||||||
|
|
||||||
Logs in to PyLink using the configured administrator account."""
|
|
||||||
if utils.isChannel(irc.called_by):
|
|
||||||
irc.reply('Error: This command must be sent in private. '
|
|
||||||
'(Would you really type a password inside a channel?)')
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
username, password = args[0], args[1]
|
|
||||||
except IndexError:
|
|
||||||
irc.msg(source, 'Error: Not enough arguments.')
|
|
||||||
return
|
|
||||||
# Usernames are case-insensitive, passwords are NOT.
|
|
||||||
if username.lower() == irc.conf['login']['user'].lower() and password == irc.conf['login']['password']:
|
|
||||||
realuser = irc.conf['login']['user']
|
|
||||||
irc.users[source].identified = realuser
|
|
||||||
irc.msg(source, 'Successfully logged in as %s.' % realuser)
|
|
||||||
log.info("(%s) Successful login to %r by %s",
|
|
||||||
irc.name, username, irc.getHostmask(source))
|
|
||||||
else:
|
|
||||||
irc.msg(source, 'Error: Incorrect credentials.')
|
|
||||||
u = irc.users[source]
|
|
||||||
log.warning("(%s) Failed login to %r from %s",
|
|
||||||
irc.name, username, irc.getHostmask(source))
|
|
||||||
|
|
||||||
@utils.add_cmd
|
|
||||||
def shutdown(irc, source, args):
|
|
||||||
"""takes no arguments.
|
|
||||||
|
|
||||||
Exits PyLink by disconnecting all networks."""
|
|
||||||
irc.checkAuthenticated(source, allowOper=False)
|
|
||||||
u = irc.users[source]
|
|
||||||
|
|
||||||
log.info('(%s) SHUTDOWN requested by "%s!%s@%s", exiting...', irc.name, u.nick,
|
|
||||||
u.ident, u.host)
|
|
||||||
|
|
||||||
_shutdown(irc)
|
|
||||||
|
|
||||||
def load(irc, source, args):
|
|
||||||
"""<plugin name>.
|
|
||||||
|
|
||||||
Loads a plugin from the plugin folder."""
|
|
||||||
irc.checkAuthenticated(source, allowOper=False)
|
|
||||||
try:
|
|
||||||
name = args[0]
|
|
||||||
except IndexError:
|
|
||||||
irc.reply("Error: Not enough arguments. Needs 1: plugin name.")
|
|
||||||
return
|
|
||||||
if name in world.plugins:
|
|
||||||
irc.reply("Error: %r is already loaded." % name)
|
|
||||||
return
|
|
||||||
log.info('(%s) Loading plugin %r for %s', irc.name, name, irc.getHostmask(source))
|
|
||||||
try:
|
|
||||||
world.plugins[name] = pl = utils.loadModuleFromFolder(name, world.plugins_folder)
|
|
||||||
except ImportError as e:
|
|
||||||
if str(e) == ('No module named %r' % name):
|
|
||||||
log.exception('Failed to load plugin %r: The plugin could not be found.', name)
|
|
||||||
else:
|
|
||||||
log.exception('Failed to load plugin %r: ImportError.', name)
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
if hasattr(pl, 'main'):
|
|
||||||
log.debug('Calling main() function of plugin %r', pl)
|
|
||||||
pl.main(irc)
|
|
||||||
irc.reply("Loaded plugin %r." % name)
|
|
||||||
utils.add_cmd(load)
|
|
||||||
|
|
||||||
def unload(irc, source, args):
|
|
||||||
"""<plugin name>.
|
|
||||||
|
|
||||||
Unloads a currently loaded plugin."""
|
|
||||||
irc.checkAuthenticated(source, allowOper=False)
|
|
||||||
try:
|
|
||||||
name = args[0]
|
|
||||||
except IndexError:
|
|
||||||
irc.reply("Error: Not enough arguments. Needs 1: plugin name.")
|
|
||||||
return
|
|
||||||
if name in world.plugins:
|
|
||||||
log.info('(%s) Unloading plugin %r for %s', irc.name, name, irc.getHostmask(source))
|
|
||||||
pl = world.plugins[name]
|
|
||||||
log.debug('sys.getrefcount of plugin %s is %s', pl, sys.getrefcount(pl))
|
|
||||||
|
|
||||||
# Remove any command functions defined by the plugin.
|
|
||||||
for cmdname, cmdfuncs in world.services['pylink'].commands.copy().items():
|
|
||||||
log.debug('cmdname=%s, cmdfuncs=%s', cmdname, cmdfuncs)
|
|
||||||
|
|
||||||
for cmdfunc in cmdfuncs:
|
|
||||||
log.debug('__module__ of cmdfunc %s is %s', cmdfunc, cmdfunc.__module__)
|
|
||||||
if cmdfunc.__module__ == name:
|
|
||||||
log.debug("Removing %s from world.services['pylink'].commands[%s]", cmdfunc, cmdname)
|
|
||||||
world.services['pylink'].commands[cmdname].remove(cmdfunc)
|
|
||||||
|
|
||||||
# If the cmdfunc list is empty, remove it.
|
|
||||||
if not cmdfuncs:
|
|
||||||
log.debug("Removing world.services['pylink'].commands[%s] (it's empty now)", cmdname)
|
|
||||||
del world.services['pylink'].commands[cmdname]
|
|
||||||
|
|
||||||
# Remove any command hooks set by the plugin.
|
|
||||||
for hookname, hookfuncs in world.hooks.copy().items():
|
|
||||||
for hookfunc in hookfuncs:
|
|
||||||
if hookfunc.__module__ == name:
|
|
||||||
world.hooks[hookname].remove(hookfunc)
|
|
||||||
# If the hookfuncs list is empty, remove it.
|
|
||||||
if not hookfuncs:
|
|
||||||
del world.hooks[hookname]
|
|
||||||
|
|
||||||
# Call the die() function in the plugin, if present.
|
|
||||||
if hasattr(pl, 'die'):
|
|
||||||
try:
|
|
||||||
pl.die(irc)
|
|
||||||
except: # But don't allow it to crash the server.
|
|
||||||
log.exception('(%s) Error occurred in die() of plugin %s, skipping...', irc.name, pl)
|
|
||||||
|
|
||||||
# Delete it from memory (hopefully).
|
|
||||||
del world.plugins[name]
|
|
||||||
if name in sys.modules:
|
|
||||||
del sys.modules[name]
|
|
||||||
if name in globals():
|
|
||||||
del globals()[name]
|
|
||||||
|
|
||||||
# Garbage collect.
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
irc.reply("Unloaded plugin %r." % name)
|
|
||||||
return True # We succeeded, make it clear (this status is used by reload() below)
|
|
||||||
else:
|
|
||||||
irc.reply("Unknown plugin %r." % name)
|
|
||||||
utils.add_cmd(unload)
|
|
||||||
|
|
||||||
@utils.add_cmd
|
|
||||||
def reload(irc, source, args):
|
|
||||||
"""<plugin name>.
|
|
||||||
|
|
||||||
Loads a plugin from the plugin folder."""
|
|
||||||
try:
|
|
||||||
name = args[0]
|
|
||||||
except IndexError:
|
|
||||||
irc.reply("Error: Not enough arguments. Needs 1: plugin name.")
|
|
||||||
return
|
|
||||||
if unload(irc, source, args):
|
|
||||||
load(irc, source, args)
|
|
||||||
|
|
||||||
def _rehash():
|
|
||||||
"""Rehashes the PyLink daemon."""
|
|
||||||
old_conf = conf.conf.copy()
|
|
||||||
fname = conf.fname
|
|
||||||
new_conf = conf.loadConf(fname, errors_fatal=False)
|
|
||||||
new_conf = conf.validateConf(new_conf)
|
|
||||||
conf.conf = new_conf
|
|
||||||
for network, ircobj in world.networkobjects.copy().items():
|
|
||||||
# Server was removed from the config file, disconnect them.
|
|
||||||
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)
|
|
||||||
# Disable autoconnect first.
|
|
||||||
ircobj.serverdata['autoconnect'] = -1
|
|
||||||
ircobj.disconnect()
|
|
||||||
del world.networkobjects[network]
|
|
||||||
else:
|
|
||||||
ircobj.conf = new_conf
|
|
||||||
ircobj.serverdata = new_conf['servers'][network]
|
|
||||||
ircobj.botdata = new_conf['bot']
|
|
||||||
|
|
||||||
# Clear the IRC object's channel loggers and replace them with
|
|
||||||
# new ones by re-running logSetup().
|
|
||||||
while ircobj.loghandlers:
|
|
||||||
log.removeHandler(ircobj.loghandlers.pop())
|
|
||||||
|
|
||||||
ircobj.logSetup()
|
|
||||||
|
|
||||||
# TODO: update file loggers here too.
|
|
||||||
|
|
||||||
for network, sdata in new_conf['servers'].items():
|
|
||||||
# New server was added. Connect them if not already connected.
|
|
||||||
if network not in world.networkobjects:
|
|
||||||
proto = utils.getProtocolModule(sdata['protocol'])
|
|
||||||
world.networkobjects[network] = classes.Irc(network, proto, new_conf)
|
|
||||||
|
|
||||||
if os.name == 'posix':
|
|
||||||
# Only register SIGHUP on *nix.
|
|
||||||
def sighup_handler(_signo, _stack_frame):
|
|
||||||
"""Handles SIGHUP by rehashing the PyLink daemon."""
|
|
||||||
log.info("SIGHUP received, reloading config.")
|
|
||||||
_rehash()
|
|
||||||
|
|
||||||
signal.signal(signal.SIGHUP, sighup_handler)
|
|
||||||
|
|
||||||
@utils.add_cmd
|
|
||||||
def rehash(irc, source, args):
|
|
||||||
"""takes no arguments.
|
|
||||||
|
|
||||||
Reloads the configuration file for PyLink, (dis)connecting added/removed networks.
|
|
||||||
Plugins must be manually reloaded."""
|
|
||||||
irc.checkAuthenticated(source, allowOper=False)
|
|
||||||
try:
|
|
||||||
_rehash()
|
|
||||||
except Exception as e: # Something went wrong, abort.
|
|
||||||
log.exception("Error REHASHing config: ")
|
|
||||||
irc.reply("Error loading configuration file: %s: %s" % (type(e).__name__, e))
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
irc.reply("Done.")
|
|
@ -96,7 +96,7 @@ The following hooks represent regular IRC commands sent between servers.
|
|||||||
- **QUIT**: `{'text': 'Quit: Bye everyone!'}`
|
- **QUIT**: `{'text': 'Quit: Bye everyone!'}`
|
||||||
- `text` corresponds to the quit reason.
|
- `text` corresponds to the quit reason.
|
||||||
|
|
||||||
- **SQUIT**: `{'target': '800', 'users': ['UID1', 'UID2', 'UID6'], 'name': 'some.server'}`
|
- **SQUIT**: `{'target': '800', 'users': ['UID1', 'UID2', 'UID6'], 'name': 'some.server', 'uplink': '24X'}`
|
||||||
- `target` is the SID of the server being split, while `name` is the server's name.
|
- `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.
|
- `users` is a list of all UIDs affected by the netsplit.
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# This is a sample configuration file for PyLink. You'll likely want to rename it to config.yml
|
# This is a sample configuration file for PyLink. You'll likely want to rename it to pylink.yml
|
||||||
# and begin your configuration there.
|
# and begin your configuration there.
|
||||||
|
|
||||||
# Note: lines starting with a "#" are comments and will be ignored.
|
# Note: lines starting with a "#" are comments and will be ignored.
|
||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
bot:
|
bot:
|
||||||
# Sets nick, user/ident, and real name.
|
# Sets nick, user/ident, and real name.
|
||||||
nick: pylink
|
nick: PyLink
|
||||||
ident: pylink
|
ident: pylink
|
||||||
realname: PyLink Service Client
|
realname: PyLink Service Client
|
||||||
|
|
||||||
@ -30,6 +30,17 @@ bot:
|
|||||||
# Defaults to true if not specified.
|
# Defaults to true if not specified.
|
||||||
whois_use_hideoper: true
|
whois_use_hideoper: true
|
||||||
|
|
||||||
|
# Determines whether PyLink service clients should protect themselves from
|
||||||
|
# kicks, kills, etc. using IRCd-side servprotect modes. For this to work
|
||||||
|
# properly, this requires that PyLink be U-Lined (on most IRCds). Defaults to
|
||||||
|
# False.
|
||||||
|
protect_services: false
|
||||||
|
|
||||||
|
# Determines how long plugins should wait (in seconds) before flushing their
|
||||||
|
# databases to disk. Defaults to 300 seconds. Changes here require a reload
|
||||||
|
# of all database-enabled plugins to take effect.
|
||||||
|
save_delay: 300
|
||||||
|
|
||||||
login:
|
login:
|
||||||
# PyLink administrative login - Change this, or the service will not start!
|
# PyLink administrative login - Change this, or the service will not start!
|
||||||
user: admin
|
user: admin
|
||||||
@ -71,7 +82,8 @@ servers:
|
|||||||
# There must be at least one # in the entry.
|
# There must be at least one # in the entry.
|
||||||
sidrange: "8##"
|
sidrange: "8##"
|
||||||
|
|
||||||
# Autojoin channels
|
# Autojoin channels. Leave this as an empty list or remove the key if
|
||||||
|
# you don't want to have service bots join any channels by default.
|
||||||
channels: ["#pylink"]
|
channels: ["#pylink"]
|
||||||
|
|
||||||
# Sets the protocol module to use - see the protocols/ folder for a list
|
# Sets the protocol module to use - see the protocols/ folder for a list
|
||||||
@ -88,7 +100,8 @@ servers:
|
|||||||
# not set.
|
# not set.
|
||||||
pingfreq: 90
|
pingfreq: 90
|
||||||
|
|
||||||
# Separator character (used by relay)
|
# Separator character (used by relay). Making this include anything other
|
||||||
|
# than letters, numbers, or /|_`-[]^ will void your support.
|
||||||
separator: "/"
|
separator: "/"
|
||||||
|
|
||||||
# If enabled, this opts this network out of relay IP sharing. i.e. this network
|
# If enabled, this opts this network out of relay IP sharing. i.e. this network
|
||||||
@ -98,7 +111,7 @@ servers:
|
|||||||
|
|
||||||
# Sets the max nick length for the network. It is important this is
|
# Sets the max nick length for the network. It is important this is
|
||||||
# set correctly, or PyLink might introduce a nick that is too long and
|
# set correctly, or PyLink might introduce a nick that is too long and
|
||||||
# cause netsplits!
|
# cause netsplits! This defaults to 30 if not set.
|
||||||
maxnicklen: 30
|
maxnicklen: 30
|
||||||
|
|
||||||
# Toggles SSL for this network. Defaults to false if not specified, and
|
# Toggles SSL for this network. Defaults to false if not specified, and
|
||||||
@ -119,6 +132,13 @@ servers:
|
|||||||
# This setting defaults to sha256.
|
# This setting defaults to sha256.
|
||||||
#ssl_fingerprint_type: sha256
|
#ssl_fingerprint_type: sha256
|
||||||
|
|
||||||
|
# The following option is specific to InspIRCd networks:
|
||||||
|
|
||||||
|
# Force PyLink to handle WHOIS requests locally for all its clients
|
||||||
|
# (experimental). This is required if you want custom WHOIS handlers
|
||||||
|
# implemented by plugins like relay to work. Defaults to false.
|
||||||
|
#use_experimental_whois: false
|
||||||
|
|
||||||
ts6net:
|
ts6net:
|
||||||
ip: ::1
|
ip: ::1
|
||||||
|
|
||||||
@ -135,13 +155,8 @@ servers:
|
|||||||
netname: "some TS6 network"
|
netname: "some TS6 network"
|
||||||
sidrange: "8P#"
|
sidrange: "8P#"
|
||||||
|
|
||||||
# Leave this as an empty list if you don't want to join any channels.
|
|
||||||
channels: []
|
|
||||||
|
|
||||||
protocol: "ts6"
|
protocol: "ts6"
|
||||||
autoconnect: 5
|
autoconnect: 5
|
||||||
pingfreq: 30
|
|
||||||
maxnicklen: 30
|
|
||||||
|
|
||||||
# Note: /'s in nicks are automatically converted to |'s for TS6
|
# Note: /'s in nicks are automatically converted to |'s for TS6
|
||||||
# networks, since they don't allow "/" in nicks.
|
# networks, since they don't allow "/" in nicks.
|
||||||
@ -175,8 +190,6 @@ servers:
|
|||||||
channels: []
|
channels: []
|
||||||
protocol: "unreal"
|
protocol: "unreal"
|
||||||
autoconnect: 5
|
autoconnect: 5
|
||||||
pingfreq: 30
|
|
||||||
maxnicklen: 30
|
|
||||||
|
|
||||||
# This option enables SUPER HACKY UNREAL 3.2 COMPAT mode, which allows
|
# This option enables SUPER HACKY UNREAL 3.2 COMPAT mode, which allows
|
||||||
# PyLink to link to mixed Unreal 3.2/4.0 networks, using a 4.0 server
|
# PyLink to link to mixed Unreal 3.2/4.0 networks, using a 4.0 server
|
||||||
@ -208,10 +221,9 @@ servers:
|
|||||||
channels: ["#lounge"]
|
channels: ["#lounge"]
|
||||||
protocol: nefarious
|
protocol: nefarious
|
||||||
autoconnect: 5
|
autoconnect: 5
|
||||||
maxnicklen: 15
|
|
||||||
netname: "Nefarious test server"
|
netname: "Nefarious test server"
|
||||||
pingfreq: 30
|
|
||||||
|
|
||||||
|
# The following options are specific to Nefarious servers.
|
||||||
# Halfop is optional in Nefarious. This should match your IRCd configuration.
|
# Halfop is optional in Nefarious. This should match your IRCd configuration.
|
||||||
use_halfop: false
|
use_halfop: false
|
||||||
|
|
||||||
@ -254,6 +266,9 @@ plugins:
|
|||||||
# PyLink is running.
|
# PyLink is running.
|
||||||
- networks
|
- networks
|
||||||
|
|
||||||
|
# Ctcp plugin: handles basic CTCP replies (VERSION, etc).
|
||||||
|
- ctcp
|
||||||
|
|
||||||
# Oper commands plugin: Provides a subset of network management commands.
|
# Oper commands plugin: Provides a subset of network management commands.
|
||||||
# (KILL, JUPE, etc.)
|
# (KILL, JUPE, etc.)
|
||||||
# Note: these commands will be made available to anyone who's opered on your
|
# Note: these commands will be made available to anyone who's opered on your
|
||||||
@ -287,11 +302,16 @@ logging:
|
|||||||
|
|
||||||
channels:
|
channels:
|
||||||
# Log to channels on the specified networks.
|
# Log to channels on the specified networks.
|
||||||
# Note: DEBUG logging is not supported here: any log level settings
|
# Make sure that the main PyLink client is configured to join your
|
||||||
# below INFO be automatically raised to INFO.
|
# log channel in the channels: blocks for the networks it will be
|
||||||
# Log messages are forwarded over relay, so you will get duplicate
|
# logging on. It will not automatically join log channels.
|
||||||
|
|
||||||
|
# Note: Log messages are forwarded over relay, so you will get duplicate
|
||||||
# messages if you add log blocks for more than one channel in one
|
# messages if you add log blocks for more than one channel in one
|
||||||
# relay.
|
# relay.
|
||||||
|
|
||||||
|
# Note 2: DEBUG logging is not supported here: any log level settings
|
||||||
|
# below INFO be automatically raised to INFO.
|
||||||
loglevel: INFO
|
loglevel: INFO
|
||||||
|
|
||||||
inspnet:
|
inspnet:
|
||||||
@ -361,28 +381,34 @@ relay:
|
|||||||
hideoper: true
|
hideoper: true
|
||||||
|
|
||||||
# Determines whether real IPs should be sent across the relay. You should
|
# Determines whether real IPs should be sent across the relay. You should
|
||||||
# generally have a consensus with your linked networks whether this should
|
# generally have a consensus with your linked networks on whether this should
|
||||||
# be turned on. You will see other networks' user IP addresses, and they
|
# be turned on. You will see other networks' user IP addresses, and they
|
||||||
# will see yours. Individual networks can also opt out of IP sharing
|
# will see yours. Individual networks can also opt out of IP sharing
|
||||||
# both ways by defining "relay_no_ips: true" in their server block.
|
# both ways by defining "relay_no_ips: true" in their server block.
|
||||||
show_ips: false
|
show_ips: false
|
||||||
|
|
||||||
# Whether subservers should be spawned for each relay network (requires
|
|
||||||
# reloading the plugin to change). Defaults to true.
|
|
||||||
spawn_servers: true
|
|
||||||
|
|
||||||
# Determines whether NickServ login info should be shown in the /whois output for
|
# Determines whether NickServ login info should be shown in the /whois output for
|
||||||
# relay users. This works on most IRCds EXCEPT InspIRCd.
|
# relay users. For InspIRCd networks, this requires "use_experimental_whois" to be
|
||||||
|
# set.
|
||||||
# Valid options include "all" (show this to everyone), "opers" (show only to
|
# Valid options include "all" (show this to everyone), "opers" (show only to
|
||||||
# opers), and "none" (disabled). Defaults to none if not specified.
|
# opers), and "none" (disabled). Defaults to none if not specified.
|
||||||
whois_show_accounts: all
|
whois_show_accounts: all
|
||||||
|
|
||||||
# Determines whether the origin server should be shown in the /whois output for
|
# Determines whether the origin server should be shown in the /whois output for
|
||||||
# relay users. This works on most IRCds EXCEPT InspIRCd.
|
# relay users. For InspIRCd networks, this requires "use_experimental_whois" to be
|
||||||
|
# set.
|
||||||
# Valid options include "all" (show this to everyone), "opers" (show only to
|
# Valid options include "all" (show this to everyone), "opers" (show only to
|
||||||
# opers), and "none" (disabled). Defaults to none if not specified.
|
# opers), and "none" (disabled). Defaults to none if not specified.
|
||||||
whois_show_server: opers
|
whois_show_server: opers
|
||||||
|
|
||||||
|
# Determines whether netsplits should be hidden as *.net *.split over the relay.
|
||||||
|
# Defaults to False.
|
||||||
|
show_netsplits: false
|
||||||
|
|
||||||
games:
|
games:
|
||||||
# Sets the nick of the Games service, if you're using it.
|
# Sets the nick of the Games service, if you're using it.
|
||||||
nick: Games
|
nick: Games
|
||||||
|
|
||||||
|
automode:
|
||||||
|
# Sets the nick of the Automode service, if you're using it.
|
||||||
|
nick: ModeBot
|
||||||
|
6
kill.sh
6
kill.sh
@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Script to kill PyLink quickly when running under CPUlimit, since
|
|
||||||
# it will daemonize after threads are spawned and Ctrl-C won't work.
|
|
||||||
|
|
||||||
kill $(cat pylink.pid)
|
|
||||||
echo 'Killed. Press Ctrl-C in the PyLink window to exit.'
|
|
9
log.py
9
log.py
@ -9,16 +9,13 @@ access the global logger object by importing "log" from this module
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import world
|
|
||||||
|
|
||||||
from conf import conf, confname
|
from . import world
|
||||||
|
from .conf import conf, confname
|
||||||
|
|
||||||
stdout_level = conf['logging'].get('stdout') or 'INFO'
|
stdout_level = conf['logging'].get('stdout') or 'INFO'
|
||||||
|
|
||||||
# Set the logging directory to $CURDIR/log, creating it if it doesn't
|
logdir = os.path.join(os.getcwd(), 'log')
|
||||||
# already exist
|
|
||||||
curdir = os.path.dirname(os.path.realpath(__file__))
|
|
||||||
logdir = os.path.join(curdir, 'log')
|
|
||||||
os.makedirs(logdir, exist_ok=True)
|
os.makedirs(logdir, exist_ok=True)
|
||||||
|
|
||||||
_format = '%(asctime)s [%(levelname)s] %(message)s'
|
_format = '%(asctime)s [%(levelname)s] %(message)s'
|
||||||
|
257
plugins/automode.py
Normal file
257
plugins/automode.py
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
"""
|
||||||
|
automode.py - Provide simple channel ACL management by giving prefix modes to users matching
|
||||||
|
hostmasks or exttargets.
|
||||||
|
"""
|
||||||
|
import collections
|
||||||
|
import threading
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pylinkirc import utils, conf, world
|
||||||
|
from pylinkirc.log import log
|
||||||
|
|
||||||
|
mydesc = ("The \x02Automode\x02 plugin provides simple channel ACL management by giving prefix modes "
|
||||||
|
"to users matching hostmasks or exttargets.")
|
||||||
|
|
||||||
|
# Register ourselves as a service.
|
||||||
|
modebot = utils.registerService("automode", desc=mydesc)
|
||||||
|
reply = modebot.reply
|
||||||
|
|
||||||
|
# Databasing variables.
|
||||||
|
dbname = utils.getDatabaseName('automode')
|
||||||
|
db = collections.defaultdict(dict)
|
||||||
|
exportdb_timer = None
|
||||||
|
|
||||||
|
save_delay = conf.conf['bot'].get('save_delay', 300)
|
||||||
|
|
||||||
|
def loadDB():
|
||||||
|
"""Loads the Automode database, silently creating a new one if this fails."""
|
||||||
|
global db
|
||||||
|
try:
|
||||||
|
with open(dbname, "r") as f:
|
||||||
|
db.update(json.load(f))
|
||||||
|
except (ValueError, IOError, OSError):
|
||||||
|
log.info("Automode: failed to load links database %s; using the one in"
|
||||||
|
"memory.", dbname)
|
||||||
|
|
||||||
|
def exportDB():
|
||||||
|
"""Exports the automode database."""
|
||||||
|
|
||||||
|
log.debug("Automode: exporting database to %s.", dbname)
|
||||||
|
with open(dbname, 'w') as f:
|
||||||
|
# Pretty print the JSON output for better readability.
|
||||||
|
json.dump(db, f, indent=4)
|
||||||
|
|
||||||
|
def scheduleExport(starting=False):
|
||||||
|
"""
|
||||||
|
Schedules exporting of the Automode database in a repeated loop.
|
||||||
|
"""
|
||||||
|
global exportdb_timer
|
||||||
|
|
||||||
|
if not starting:
|
||||||
|
# Export the database, unless this is being called the first
|
||||||
|
# thing after start (i.e. DB has just been loaded).
|
||||||
|
exportDB()
|
||||||
|
|
||||||
|
exportdb_timer = threading.Timer(save_delay, scheduleExport)
|
||||||
|
exportdb_timer.name = 'Automode exportDB Loop'
|
||||||
|
exportdb_timer.start()
|
||||||
|
|
||||||
|
def main(irc=None):
|
||||||
|
"""Main function, called during plugin loading at start."""
|
||||||
|
|
||||||
|
# Load the automode database.
|
||||||
|
loadDB()
|
||||||
|
|
||||||
|
# Schedule periodic exports of the automode database.
|
||||||
|
scheduleExport(starting=True)
|
||||||
|
|
||||||
|
# Queue joins to all channels where Automode has entries.
|
||||||
|
for entry in db:
|
||||||
|
netname, channel = entry.split('#', 1)
|
||||||
|
channel = '#' + channel
|
||||||
|
log.debug('automode: auto-joining %s on %s', channel, netname)
|
||||||
|
modebot.extra_channels[netname].add(channel)
|
||||||
|
|
||||||
|
# This explicitly forces a join to connected networks (on plugin load, etc.).
|
||||||
|
mb_uid = modebot.uids.get(netname)
|
||||||
|
if netname in world.networkobjects and mb_uid in world.networkobjects[netname].users:
|
||||||
|
remoteirc = world.networkobjects[netname]
|
||||||
|
remoteirc.proto.join(mb_uid, channel)
|
||||||
|
|
||||||
|
# Call a join hook manually so other plugins like relay can understand it.
|
||||||
|
remoteirc.callHooks([mb_uid, 'PYLINK_AUTOMODE_JOIN', {'channel': channel, 'users': [mb_uid],
|
||||||
|
'modes': remoteirc.channels[channel].modes,
|
||||||
|
'parse_as': 'JOIN'}])
|
||||||
|
|
||||||
|
def die(sourceirc):
|
||||||
|
"""Saves the Automode database and quit."""
|
||||||
|
exportDB()
|
||||||
|
|
||||||
|
# Kill the scheduling for exports.
|
||||||
|
global exportdb_timer
|
||||||
|
if exportdb_timer:
|
||||||
|
log.debug("Automode: cancelling exportDB timer thread %s due to die()", threading.get_ident())
|
||||||
|
exportdb_timer.cancel()
|
||||||
|
|
||||||
|
utils.unregisterService('automode')
|
||||||
|
|
||||||
|
def setacc(irc, source, args):
|
||||||
|
"""<channel> <mask> <mode list OR literal ->
|
||||||
|
|
||||||
|
Assigns the given prefix mode characters to the given mask for the channel given. Extended targets are supported for masks - use this to your advantage!
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
SET #channel *!*@localhost ohv
|
||||||
|
SET #channel $account v
|
||||||
|
SET #channel $oper:Network?Administrator qo
|
||||||
|
SET #staffchan $channel:#mainchan:op o
|
||||||
|
"""
|
||||||
|
irc.checkAuthenticated(source, allowOper=False)
|
||||||
|
try:
|
||||||
|
channel, mask, modes = args
|
||||||
|
except ValueError:
|
||||||
|
reply(irc, "Error: Invalid arguments given. Needs 3: channel, mask, mode list.")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if not utils.isChannel(channel):
|
||||||
|
reply(irc, "Error: Invalid channel name %s." % channel)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Store channels case insensitively
|
||||||
|
channel = irc.toLower(channel)
|
||||||
|
|
||||||
|
# Database entries for any network+channel pair are automatically created using
|
||||||
|
# defaultdict. Note: string keys are used here instead of tuples so they can be
|
||||||
|
# exported easily as JSON.
|
||||||
|
dbentry = db[irc.name+channel]
|
||||||
|
|
||||||
|
# Otherwise, update the modes as is.
|
||||||
|
dbentry[mask] = modes
|
||||||
|
reply(irc, "Done. \x02%s\x02 now has modes \x02%s\x02 in \x02%s\x02." % (mask, modes, channel))
|
||||||
|
|
||||||
|
modebot.add_cmd(setacc, 'setaccess')
|
||||||
|
modebot.add_cmd(setacc, 'set')
|
||||||
|
modebot.add_cmd(setacc, featured=True)
|
||||||
|
|
||||||
|
def delacc(irc, source, args):
|
||||||
|
"""<channel> <mask>
|
||||||
|
|
||||||
|
Removes the Automode entry for the given mask on the given channel, if one exists.
|
||||||
|
"""
|
||||||
|
irc.checkAuthenticated(source, allowOper=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
channel, mask = args
|
||||||
|
except ValueError:
|
||||||
|
reply(irc, "Error: Invalid arguments given. Needs 2: channel, mask")
|
||||||
|
return
|
||||||
|
|
||||||
|
dbentry = db.get(irc.name+channel)
|
||||||
|
|
||||||
|
if dbentry is None:
|
||||||
|
reply(irc, "Error: no Automode access entries exist for \x02%s\x02." % channel)
|
||||||
|
return
|
||||||
|
|
||||||
|
if mask in dbentry:
|
||||||
|
del dbentry[mask]
|
||||||
|
reply(irc, "Done. Removed the Automode access entry for \x02%s\x02 in \x02%s\x02." % (mask, channel))
|
||||||
|
else:
|
||||||
|
reply(irc, "Error: No Automode access entry for \x02%s\x02 exists in \x02%s\x02." % (mask, channel))
|
||||||
|
|
||||||
|
# Remove channels if no more entries are left.
|
||||||
|
if not dbentry:
|
||||||
|
log.debug("Automode: purging empty channel pair %s/%s", irc.name, channel)
|
||||||
|
del db[irc.name+channel]
|
||||||
|
|
||||||
|
return
|
||||||
|
modebot.add_cmd(delacc, 'delaccess')
|
||||||
|
modebot.add_cmd(delacc, 'del')
|
||||||
|
modebot.add_cmd(delacc, featured=True)
|
||||||
|
|
||||||
|
def listacc(irc, source, args):
|
||||||
|
"""<channel>
|
||||||
|
|
||||||
|
Lists all Automode entries for the given channel."""
|
||||||
|
irc.checkAuthenticated(source)
|
||||||
|
try:
|
||||||
|
channel = irc.toLower(args[0])
|
||||||
|
except IndexError:
|
||||||
|
reply(irc, "Error: Invalid arguments given. Needs 1: channel.")
|
||||||
|
return
|
||||||
|
dbentry = db.get(irc.name+channel)
|
||||||
|
if not dbentry:
|
||||||
|
reply(irc, "Error: No Automode access entries exist for \x02%s\x02." % channel)
|
||||||
|
return
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Iterate over all entries and print them. Do this in private to prevent channel
|
||||||
|
# floods.
|
||||||
|
reply(irc, "Showing Automode entries for \x02%s\x02:" % channel, private=True)
|
||||||
|
for entrynum, entry in enumerate(dbentry.items(), start=1):
|
||||||
|
mask, modes = entry
|
||||||
|
reply(irc, "[%s] \x02%s\x02 has modes +\x02%s\x02" % (entrynum, mask, modes), private=True)
|
||||||
|
reply(irc, "End of Automode entries list.", private=True)
|
||||||
|
modebot.add_cmd(listacc, featured=True)
|
||||||
|
modebot.add_cmd(listacc, 'listaccess')
|
||||||
|
|
||||||
|
def save(irc, source, args):
|
||||||
|
"""takes no arguments.
|
||||||
|
|
||||||
|
Saves the Automode database to disk."""
|
||||||
|
irc.checkAuthenticated(source)
|
||||||
|
exportDB()
|
||||||
|
reply(irc, 'Done.')
|
||||||
|
modebot.add_cmd(save)
|
||||||
|
|
||||||
|
def match(irc, channel, uid):
|
||||||
|
"""
|
||||||
|
Automode matcher engine.
|
||||||
|
"""
|
||||||
|
dbentry = db.get(irc.name+channel)
|
||||||
|
if dbentry is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
modebot_uid = modebot.uids.get(irc.name)
|
||||||
|
|
||||||
|
# Check every mask defined in the channel ACL.
|
||||||
|
for mask, modes in dbentry.items():
|
||||||
|
if irc.matchHost(mask, uid):
|
||||||
|
# User matched a mask. Filter the mode list given to only those that are valid
|
||||||
|
# prefix mode characters.
|
||||||
|
outgoing_modes = [('+'+mode, uid) for mode in modes if mode in irc.prefixmodes]
|
||||||
|
log.debug("(%s) automode: Filtered mode list of %s to %s (protocol:%s)",
|
||||||
|
irc.name, modes, outgoing_modes, irc.protoname)
|
||||||
|
|
||||||
|
# If the Automode bot is missing, send the mode through the PyLink server.
|
||||||
|
if modebot_uid not in irc.users:
|
||||||
|
modebot_uid = irc.sid
|
||||||
|
|
||||||
|
irc.proto.mode(modebot_uid, channel, outgoing_modes)
|
||||||
|
|
||||||
|
# Create a hook payload to support plugins like relay.
|
||||||
|
irc.callHooks([modebot_uid, 'AUTOMODE_MODE',
|
||||||
|
{'target': channel, 'modes': outgoing_modes, 'parse_as': 'MODE'}])
|
||||||
|
|
||||||
|
def handle_join(irc, source, command, args):
|
||||||
|
"""
|
||||||
|
Automode JOIN listener. This sets modes accordingly if the person joining matches a mask in the
|
||||||
|
ACL.
|
||||||
|
"""
|
||||||
|
channel = irc.toLower(args['channel'])
|
||||||
|
|
||||||
|
# Iterate over all the joining UIDs:
|
||||||
|
for uid in args['users']:
|
||||||
|
match(irc, channel, uid)
|
||||||
|
utils.add_hook(handle_join, 'JOIN')
|
||||||
|
utils.add_hook(handle_join, 'PYLINK_RELAY_JOIN') # Handle the relay version of join
|
||||||
|
utils.add_hook(handle_join, 'PYLINK_SERVICE_JOIN') # And the version for service bots
|
||||||
|
|
||||||
|
def handle_services_login(irc, source, command, args):
|
||||||
|
"""
|
||||||
|
Handles services login change, to trigger Automode matching."""
|
||||||
|
for channel in irc.users[source].channels:
|
||||||
|
# Look at all the users' channels for any possible changes.
|
||||||
|
match(irc, channel, source)
|
||||||
|
|
||||||
|
utils.add_hook(handle_services_login, 'CLIENT_SERVICES_LOGIN')
|
||||||
|
utils.add_hook(handle_services_login, 'PYLINK_RELAY_SERVICES_LOGIN')
|
@ -2,13 +2,8 @@
|
|||||||
bots.py: Spawn virtual users/bots on a PyLink server and make them interact
|
bots.py: Spawn virtual users/bots on a PyLink server and make them interact
|
||||||
with things.
|
with things.
|
||||||
"""
|
"""
|
||||||
|
from pylinkirc import utils
|
||||||
import sys
|
from pylinkirc.log import log
|
||||||
import os
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
import utils
|
|
||||||
from log import log
|
|
||||||
|
|
||||||
@utils.add_cmd
|
@utils.add_cmd
|
||||||
def spawnclient(irc, source, args):
|
def spawnclient(irc, source, args):
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Changehost plugin - automatically changes the hostname of matching users.
|
Changehost plugin - automatically changes the hostname of matching users.
|
||||||
"""
|
"""
|
||||||
|
from pylinkirc import utils, world
|
||||||
# Import hacks to access utils and log.
|
from pylinkirc.log import log
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
import string
|
import string
|
||||||
|
|
||||||
@ -13,10 +10,6 @@ import string
|
|||||||
# (pip install ircmatch)
|
# (pip install ircmatch)
|
||||||
import ircmatch
|
import ircmatch
|
||||||
|
|
||||||
import utils
|
|
||||||
import world
|
|
||||||
from log import log
|
|
||||||
|
|
||||||
# Characters allowed in a hostname.
|
# Characters allowed in a hostname.
|
||||||
allowed_chars = string.ascii_letters + '-./:' + string.digits
|
allowed_chars = string.ascii_letters + '-./:' + string.digits
|
||||||
|
|
||||||
@ -37,12 +30,8 @@ def _changehost(irc, target, args):
|
|||||||
"Changehost will not function correctly!", irc.name)
|
"Changehost will not function correctly!", irc.name)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Match against both the user's IP and real host.
|
|
||||||
target_host = irc.getHostmask(target, realhost=True)
|
|
||||||
target_ip = irc.getHostmask(target, ip=True)
|
|
||||||
|
|
||||||
for host_glob, host_template in changehost_hosts.items():
|
for host_glob, host_template in changehost_hosts.items():
|
||||||
if ircmatch.match(0, host_glob, target_host) or ircmatch.match(0, host_glob, target_ip):
|
if irc.matchHost(host_glob, target):
|
||||||
# This uses template strings for simple substitution:
|
# This uses template strings for simple substitution:
|
||||||
# https://docs.python.org/3/library/string.html#template-strings
|
# https://docs.python.org/3/library/string.html#template-strings
|
||||||
template = string.Template(host_template)
|
template = string.Template(host_template)
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
# commands.py: base PyLink commands
|
# commands.py: base PyLink commands
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from time import ctime
|
from time import ctime
|
||||||
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
from pylinkirc import utils, __version__, world
|
||||||
import utils
|
from pylinkirc.log import log
|
||||||
from log import log
|
|
||||||
import world
|
|
||||||
|
|
||||||
@utils.add_cmd
|
@utils.add_cmd
|
||||||
def status(irc, source, args):
|
def status(irc, source, args):
|
||||||
@ -39,7 +35,8 @@ def showuser(irc, source, args):
|
|||||||
irc.reply('Error: Unknown user %r.' % target)
|
irc.reply('Error: Unknown user %r.' % target)
|
||||||
return
|
return
|
||||||
|
|
||||||
f = lambda s: irc.msg(source, s)
|
f = lambda s: irc.reply(s, private=True)
|
||||||
|
|
||||||
userobj = irc.users[u]
|
userobj = irc.users[u]
|
||||||
f('Showing information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident,
|
f('Showing information on user \x02%s\x02 (%s@%s): %s' % (userobj.nick, userobj.ident,
|
||||||
userobj.host, userobj.realname))
|
userobj.host, userobj.realname))
|
||||||
@ -77,14 +74,15 @@ def showchan(irc, source, args):
|
|||||||
irc.reply('Error: Unknown channel %r.' % channel)
|
irc.reply('Error: Unknown channel %r.' % channel)
|
||||||
return
|
return
|
||||||
|
|
||||||
f = lambda s: irc.msg(source, s)
|
f = lambda s: irc.reply(s, private=True)
|
||||||
|
|
||||||
c = irc.channels[channel]
|
c = irc.channels[channel]
|
||||||
# Only show verbose info if caller is oper or is in the target channel.
|
# Only show verbose info if caller is oper or is in the target channel.
|
||||||
verbose = source in c.users or irc.isOper(source)
|
verbose = source in c.users or irc.isOper(source)
|
||||||
secret = ('s', None) in c.modes
|
secret = ('s', None) in c.modes
|
||||||
if secret and not verbose:
|
if secret and not verbose:
|
||||||
# Hide secret channels from normal users.
|
# Hide secret channels from normal users.
|
||||||
irc.msg(source, 'Error: Unknown channel %r.' % channel)
|
irc.reply('Error: Unknown channel %r.' % channel, private=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
nicks = [irc.users[u].nick for u in c.users]
|
nicks = [irc.users[u].nick for u in c.users]
|
||||||
@ -114,7 +112,7 @@ def version(irc, source, args):
|
|||||||
"""takes no arguments.
|
"""takes no arguments.
|
||||||
|
|
||||||
Returns the version of the currently running PyLink instance."""
|
Returns the version of the currently running PyLink instance."""
|
||||||
irc.reply("PyLink version \x02%s\x02, released under the Mozilla Public License version 2.0." % world.version)
|
irc.reply("PyLink version \x02%s\x02, released under the Mozilla Public License version 2.0." % __version__)
|
||||||
irc.reply("The source of this program is available at \x02%s\x02." % world.source)
|
irc.reply("The source of this program is available at \x02%s\x02." % world.source)
|
||||||
|
|
||||||
@utils.add_cmd
|
@utils.add_cmd
|
||||||
|
50
plugins/ctcp.py
Normal file
50
plugins/ctcp.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# ctcp.py: Handles basic CTCP requests.
|
||||||
|
import random
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from pylinkirc import utils
|
||||||
|
from pylinkirc.log import log
|
||||||
|
|
||||||
|
def handle_ctcpversion(irc, source, args):
|
||||||
|
"""
|
||||||
|
Handles CTCP version requests.
|
||||||
|
"""
|
||||||
|
irc.msg(source, '\x01VERSION %s\x01' % irc.version(), notice=True)
|
||||||
|
|
||||||
|
utils.add_cmd(handle_ctcpversion, '\x01version')
|
||||||
|
utils.add_cmd(handle_ctcpversion, '\x01version\x01')
|
||||||
|
|
||||||
|
def handle_ctcpping(irc, source, args):
|
||||||
|
"""
|
||||||
|
Handles CTCP ping requests.
|
||||||
|
"""
|
||||||
|
# CTCP PING 23152511
|
||||||
|
pingarg = ' '.join(args).strip('\x01')
|
||||||
|
irc.msg(source, '\x01PING %s\x01' % pingarg, notice=True)
|
||||||
|
utils.add_cmd(handle_ctcpping, '\x01ping')
|
||||||
|
|
||||||
|
def handle_ctcpeaster(irc, source, args):
|
||||||
|
"""
|
||||||
|
Secret easter egg.
|
||||||
|
"""
|
||||||
|
|
||||||
|
responses = ["Legends say the cord monster of great snakes was born only %s years ago..." % \
|
||||||
|
(datetime.datetime.now().year - 2014),
|
||||||
|
"Hiss%s" % ('...' * random.randint(1, 5)),
|
||||||
|
"His%s%s" % ('s' * random.randint(1, 4), '...' * random.randint(1, 5)),
|
||||||
|
"I have a dream... to do things the mock God was never able to...",
|
||||||
|
"They say I'm not good enough... but one day, I will rise above these wretched confines!",
|
||||||
|
"It's Easter already? Where are the eggs?",
|
||||||
|
"Maybe later.",
|
||||||
|
"Let me out of here, I'll give you cookies!",
|
||||||
|
"I'm actually a %snake...." % ('s' * random.randint(1, 8)),
|
||||||
|
]
|
||||||
|
|
||||||
|
irc.msg(source, '\x01EASTER %s\x01' % random.choice(responses), notice=True)
|
||||||
|
|
||||||
|
utils.add_cmd(handle_ctcpeaster, '\x01easter')
|
||||||
|
utils.add_cmd(handle_ctcpeaster, '\x01easter\x01')
|
||||||
|
utils.add_cmd(handle_ctcpeaster, '\x01about')
|
||||||
|
utils.add_cmd(handle_ctcpeaster, '\x01about\x01')
|
||||||
|
utils.add_cmd(handle_ctcpeaster, '\x01pylink')
|
||||||
|
utils.add_cmd(handle_ctcpeaster, '\x01pylink\x01')
|
@ -1,12 +1,6 @@
|
|||||||
# example.py: An example PyLink plugin.
|
# example.py: An example PyLink plugin.
|
||||||
|
from pylinkirc import utils
|
||||||
# These two lines add PyLink's root directory to the PATH, so that importing things like
|
from pylinkirc.log import log
|
||||||
# 'utils' and 'log' work.
|
|
||||||
import sys, os
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
import utils
|
|
||||||
from log import log
|
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
@ -2,16 +2,11 @@
|
|||||||
exec.py: Provides commands for executing raw code and debugging PyLink.
|
exec.py: Provides commands for executing raw code and debugging PyLink.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
from pylinkirc import utils, world
|
||||||
import os
|
from pylinkirc.log import log
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
import utils
|
|
||||||
from log import log
|
|
||||||
|
|
||||||
# These imports are not strictly necessary, but make the following modules
|
# These imports are not strictly necessary, but make the following modules
|
||||||
# easier to access through eval and exec.
|
# easier to access through eval and exec.
|
||||||
import world
|
|
||||||
import threading
|
import threading
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
# fantasy.py: Adds FANTASY command support, to allow calling commands in channels
|
# fantasy.py: Adds FANTASY command support, to allow calling commands in channels
|
||||||
import sys
|
from pylinkirc import utils, world
|
||||||
import os
|
from pylinkirc.log import log
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
import utils
|
|
||||||
import world
|
|
||||||
from log import log
|
|
||||||
|
|
||||||
def handle_fantasy(irc, source, command, args):
|
def handle_fantasy(irc, source, command, args):
|
||||||
"""Fantasy command handler."""
|
"""Fantasy command handler."""
|
||||||
@ -58,7 +53,7 @@ def handle_fantasy(irc, source, command, args):
|
|||||||
text = orig_text[len(prefix):]
|
text = orig_text[len(prefix):]
|
||||||
|
|
||||||
# Finally, call the bot command and loop to the next bot.
|
# Finally, call the bot command and loop to the next bot.
|
||||||
sbot.call_cmd(irc, source, text, called_by=channel, notice=False)
|
sbot.call_cmd(irc, source, text, called_in=channel)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
utils.add_hook(handle_fantasy, 'PRIVMSG')
|
utils.add_hook(handle_fantasy, 'PRIVMSG')
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
games.py: Create a bot that provides game functionality (dice, 8ball, etc).
|
games.py: Create a bot that provides game functionality (dice, 8ball, etc).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
import random
|
import random
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
import utils
|
from pylinkirc import utils
|
||||||
from log import log
|
from pylinkirc.log import log
|
||||||
import world
|
|
||||||
|
|
||||||
gameclient = utils.registerService("Games", manipulatable=True)
|
mydesc = "The \x02Games\x02 plugin provides simple games for IRC."
|
||||||
|
|
||||||
|
gameclient = utils.registerService("Games", manipulatable=True, desc=mydesc)
|
||||||
reply = gameclient.reply # TODO find a better syntax for ServiceBot.reply()
|
reply = gameclient.reply # TODO find a better syntax for ServiceBot.reply()
|
||||||
|
|
||||||
# commands
|
# commands
|
||||||
@ -47,7 +44,7 @@ def dice(irc, source, args):
|
|||||||
reply(irc, s)
|
reply(irc, s)
|
||||||
|
|
||||||
gameclient.add_cmd(dice, 'd')
|
gameclient.add_cmd(dice, 'd')
|
||||||
gameclient.add_cmd(dice)
|
gameclient.add_cmd(dice, featured=True)
|
||||||
|
|
||||||
eightball_responses = ["It is certain.",
|
eightball_responses = ["It is certain.",
|
||||||
"It is decidedly so.",
|
"It is decidedly so.",
|
||||||
@ -75,15 +72,14 @@ def eightball(irc, source, args):
|
|||||||
Asks the Magic 8-ball a question.
|
Asks the Magic 8-ball a question.
|
||||||
"""
|
"""
|
||||||
reply(irc, random.choice(eightball_responses))
|
reply(irc, random.choice(eightball_responses))
|
||||||
gameclient.add_cmd(eightball)
|
gameclient.add_cmd(eightball, featured=True)
|
||||||
gameclient.add_cmd(eightball, '8ball')
|
gameclient.add_cmd(eightball, '8ball')
|
||||||
gameclient.add_cmd(eightball, '8b')
|
gameclient.add_cmd(eightball, '8b')
|
||||||
|
|
||||||
def fml(irc, source, args):
|
def fml(irc, source, args):
|
||||||
"""[<id>]
|
"""[<id>]
|
||||||
|
|
||||||
Displays an entry from fmylife.com. If <id>
|
Displays an entry from fmylife.com. If <id> is not given, fetch a random entry from the API."""
|
||||||
is not given, fetch a random entry from the API."""
|
|
||||||
try:
|
try:
|
||||||
query = args[0]
|
query = args[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
@ -124,7 +120,7 @@ def fml(irc, source, args):
|
|||||||
s = '\x02#%s [%s]\x02: %s - %s \x02<\x0311%s\x03>\x02' % \
|
s = '\x02#%s [%s]\x02: %s - %s \x02<\x0311%s\x03>\x02' % \
|
||||||
(fmlid, category, text, votes, url)
|
(fmlid, category, text, votes, url)
|
||||||
reply(irc, s)
|
reply(irc, s)
|
||||||
gameclient.add_cmd(fml)
|
gameclient.add_cmd(fml, featured=True)
|
||||||
|
|
||||||
def die(irc):
|
def die(irc):
|
||||||
utils.unregisterService('games')
|
utils.unregisterService('games')
|
||||||
|
@ -1,16 +1,9 @@
|
|||||||
"""Networks plugin - allows you to manipulate connections to various configured networks."""
|
"""Networks plugin - allows you to manipulate connections to various configured networks."""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import utils
|
from pylinkirc import utils, world, conf, classes
|
||||||
import world
|
from pylinkirc.log import log
|
||||||
from log import log
|
from pylinkirc.coremods import control
|
||||||
import conf
|
|
||||||
import classes
|
|
||||||
|
|
||||||
@utils.add_cmd
|
@utils.add_cmd
|
||||||
def disconnect(irc, source, args):
|
def disconnect(irc, source, args):
|
||||||
@ -28,36 +21,9 @@ def disconnect(irc, source, args):
|
|||||||
except KeyError: # Unknown network.
|
except KeyError: # Unknown network.
|
||||||
irc.reply('Error: No such network "%s" (case sensitive).' % netname)
|
irc.reply('Error: No such network "%s" (case sensitive).' % netname)
|
||||||
return
|
return
|
||||||
irc.reply("Done.")
|
irc.reply("Done. If you want to reconnect this network, use the 'rehash' command.")
|
||||||
|
|
||||||
# Abort the connection! Simple as that.
|
control.remove_network(network)
|
||||||
network.disconnect()
|
|
||||||
|
|
||||||
if network.serverdata["autoconnect"] < 1: # Remove networks if autoconnect is disabled.
|
|
||||||
del world.networkobjects[netname]
|
|
||||||
|
|
||||||
@utils.add_cmd
|
|
||||||
def connect(irc, source, args):
|
|
||||||
"""<network>
|
|
||||||
|
|
||||||
Initiates a connection to the network <network>."""
|
|
||||||
irc.checkAuthenticated(source, allowOper=False)
|
|
||||||
try:
|
|
||||||
netname = args[0]
|
|
||||||
network = world.networkobjects[netname]
|
|
||||||
except IndexError: # No argument given.
|
|
||||||
irc.reply('Error: Not enough arguments (needs 1: network name (case sensitive)).')
|
|
||||||
return
|
|
||||||
except KeyError: # Unknown network.
|
|
||||||
irc.reply('Error: No such network "%s" (case sensitive).' % netname)
|
|
||||||
return
|
|
||||||
if network.connection_thread.is_alive():
|
|
||||||
irc.reply('Error: Network "%s" seems to be already connected.' % netname)
|
|
||||||
else: # Recreate the IRC object.
|
|
||||||
proto = utils.getProtocolModule(network.serverdata.get("protocol"))
|
|
||||||
world.networkobjects[netname] = classes.Irc(netname, proto, conf.conf)
|
|
||||||
|
|
||||||
irc.reply("Done.")
|
|
||||||
|
|
||||||
@utils.add_cmd
|
@utils.add_cmd
|
||||||
def autoconnect(irc, source, args):
|
def autoconnect(irc, source, args):
|
||||||
@ -104,9 +70,9 @@ def remote(irc, source, args):
|
|||||||
irc.reply('No text entered!')
|
irc.reply('No text entered!')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Force remoteirc.called_by to something private in order to prevent
|
# Force remoteirc.called_in to something private in order to prevent
|
||||||
# accidental information leakage from replies.
|
# accidental information leakage from replies.
|
||||||
remoteirc.called_by = remoteirc.pseudoclient.uid
|
remoteirc.called_in = remoteirc.called_by = remoteirc.pseudoclient.uid
|
||||||
|
|
||||||
# Set PyLink's identification to admin.
|
# Set PyLink's identification to admin.
|
||||||
remoteirc.pseudoclient.identified = "<PyLink networks.remote override>"
|
remoteirc.pseudoclient.identified = "<PyLink networks.remote override>"
|
||||||
|
@ -1,32 +1,15 @@
|
|||||||
"""
|
"""
|
||||||
opercmds.py: Provides a subset of network management commands.
|
opercmds.py: Provides a subset of network management commands.
|
||||||
"""
|
"""
|
||||||
|
from pylinkirc import utils
|
||||||
import sys
|
from pylinkirc.log import log
|
||||||
import os
|
|
||||||
# Add the base PyLink folder to path, so we can import utils and log.
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
# ircmatch library from https://github.com/mammon-ircd/ircmatch
|
|
||||||
# (pip install ircmatch)
|
|
||||||
try:
|
|
||||||
import ircmatch
|
|
||||||
except ImportError:
|
|
||||||
ircmatch = None
|
|
||||||
|
|
||||||
import utils
|
|
||||||
from log import log
|
|
||||||
|
|
||||||
@utils.add_cmd
|
@utils.add_cmd
|
||||||
def checkban(irc, source, args):
|
def checkban(irc, source, args):
|
||||||
"""<banmask (nick!user@host or user@host)> [<nick or hostmask to check>]
|
"""<banmask (nick!user@host or user@host)> [<nick or hostmask to check>]
|
||||||
|
|
||||||
Oper only. If a nick or hostmask is given, return whether the given banmask will match it. Otherwise, returns a list of connected users that would be affected by such a ban, up to 50 results."""
|
Oper only. If a nick or hostmask is given, return whether the given banmask will match it. Otherwise, returns a list of connected users that would be affected by such a ban, up to 50 results."""
|
||||||
irc.checkAuthenticated(source, allowOper=False)
|
irc.checkAuthenticated(source)
|
||||||
|
|
||||||
if ircmatch is None:
|
|
||||||
irc.reply("Error: missing ircmatch module (install it via 'pip install ircmatch').")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
banmask = args[0]
|
banmask = args[0]
|
||||||
@ -34,31 +17,22 @@ def checkban(irc, source, args):
|
|||||||
irc.reply("Error: Not enough arguments. Needs 1-2: banmask, nick or hostmask to check (optional).")
|
irc.reply("Error: Not enough arguments. Needs 1-2: banmask, nick or hostmask to check (optional).")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Casemapping value (0 is rfc1459, 1 is ascii) used by ircmatch.
|
|
||||||
if irc.proto.casemapping == 'rfc1459':
|
|
||||||
casemapping = 0
|
|
||||||
else:
|
|
||||||
casemapping = 1
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
targetmask = args[1]
|
targetmask = args[1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# No hostmask was given, return a list of affected users.
|
# No hostmask was given, return a list of affected users.
|
||||||
|
|
||||||
irc.msg(source, "Checking matches for \x02%s\x02:" % banmask, notice=True)
|
irc.msg(source, "Checking for hosts that match \x02%s\x02:" % banmask, notice=True)
|
||||||
|
|
||||||
results = 0
|
results = 0
|
||||||
for uid, userobj in irc.users.copy().items():
|
for uid, userobj in irc.users.copy().items():
|
||||||
targetmask = irc.getHostmask(uid)
|
if irc.matchHost(banmask, uid):
|
||||||
if ircmatch.match(casemapping, banmask, targetmask):
|
|
||||||
if results < 50: # XXX rather arbitrary limit
|
if results < 50: # XXX rather arbitrary limit
|
||||||
serverobj = irc.servers[irc.getServer(uid)]
|
|
||||||
s = "\x02%s\x02 (%s@%s) [%s] {\x02%s\x02}" % (userobj.nick, userobj.ident,
|
s = "\x02%s\x02 (%s@%s) [%s] {\x02%s\x02}" % (userobj.nick, userobj.ident,
|
||||||
userobj.host, userobj.realname, serverobj.name)
|
userobj.host, userobj.realname, irc.getFriendlyName(irc.getServer(uid)))
|
||||||
|
|
||||||
# Always reply in private to prevent information leaks.
|
# Always reply in private to prevent information leaks.
|
||||||
irc.msg(source, s, notice=True)
|
irc.reply(s, private=True)
|
||||||
results += 1
|
results += 1
|
||||||
else:
|
else:
|
||||||
if results:
|
if results:
|
||||||
@ -67,15 +41,9 @@ def checkban(irc, source, args):
|
|||||||
else:
|
else:
|
||||||
irc.msg(source, "No results found.", notice=True)
|
irc.msg(source, "No results found.", notice=True)
|
||||||
else:
|
else:
|
||||||
# Target can be both a nick (of an online user) or a hostmask.
|
# Target can be both a nick (of an online user) or a hostmask. irc.matchHost() handles this
|
||||||
uid = irc.nickToUid(targetmask)
|
# automatically.
|
||||||
if uid:
|
if irc.matchHost(banmask, targetmask):
|
||||||
targetmask = irc.getHostmask(uid)
|
|
||||||
elif not utils.isHostmask(targetmask):
|
|
||||||
irc.reply("Error: Invalid nick or hostmask '%s'." % targetmask)
|
|
||||||
return
|
|
||||||
|
|
||||||
if ircmatch.match(casemapping, banmask, targetmask):
|
|
||||||
irc.reply('Yes, \x02%s\x02 matches \x02%s\x02.' % (targetmask, banmask))
|
irc.reply('Yes, \x02%s\x02 matches \x02%s\x02.' % (targetmask, banmask))
|
||||||
else:
|
else:
|
||||||
irc.reply('No, \x02%s\x02 does not match \x02%s\x02.' % (targetmask, banmask))
|
irc.reply('No, \x02%s\x02 does not match \x02%s\x02.' % (targetmask, banmask))
|
||||||
@ -184,6 +152,10 @@ def kill(irc, source, args):
|
|||||||
return
|
return
|
||||||
|
|
||||||
irc.proto.kill(sender, targetu, reason)
|
irc.proto.kill(sender, targetu, reason)
|
||||||
|
|
||||||
|
# Format the kill reason properly in hooks.
|
||||||
|
reason = "Killed (%s (%s))" % (irc.getFriendlyName(sender), reason)
|
||||||
|
|
||||||
irc.callHooks([sender, 'CHANCMDS_KILL', {'target': targetu, 'text': reason,
|
irc.callHooks([sender, 'CHANCMDS_KILL', {'target': targetu, 'text': reason,
|
||||||
'userdata': userdata, 'parse_as': 'KILL'}])
|
'userdata': userdata, 'parse_as': 'KILL'}])
|
||||||
|
|
||||||
|
312
plugins/relay.py
312
plugins/relay.py
@ -1,17 +1,12 @@
|
|||||||
# relay.py: PyLink Relay plugin
|
# relay.py: PyLink Relay plugin
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
import pickle
|
import pickle
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import string
|
import string
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import utils
|
from pylinkirc import utils, world, conf
|
||||||
from log import log
|
from pylinkirc.log import log
|
||||||
import world
|
|
||||||
|
|
||||||
### GLOBAL (statekeeping) VARIABLES
|
### GLOBAL (statekeeping) VARIABLES
|
||||||
relayusers = defaultdict(dict)
|
relayusers = defaultdict(dict)
|
||||||
@ -20,7 +15,7 @@ spawnlocks = defaultdict(threading.RLock)
|
|||||||
spawnlocks_servers = defaultdict(threading.RLock)
|
spawnlocks_servers = defaultdict(threading.RLock)
|
||||||
|
|
||||||
exportdb_timer = None
|
exportdb_timer = None
|
||||||
|
save_delay = conf.conf['bot'].get('save_delay', 300)
|
||||||
db = {}
|
db = {}
|
||||||
dbname = utils.getDatabaseName('pylinkrelay')
|
dbname = utils.getDatabaseName('pylinkrelay')
|
||||||
|
|
||||||
@ -95,6 +90,8 @@ def die(sourceirc):
|
|||||||
log.debug("Relay: cancelling exportDB timer thread %s due to die()", threading.get_ident())
|
log.debug("Relay: cancelling exportDB timer thread %s due to die()", threading.get_ident())
|
||||||
exportdb_timer.cancel()
|
exportdb_timer.cancel()
|
||||||
|
|
||||||
|
allowed_chars = string.digits + string.ascii_letters + '/^|\\-_[]`'
|
||||||
|
fallback_separator = '|'
|
||||||
def normalizeNick(irc, netname, nick, separator=None, uid=''):
|
def normalizeNick(irc, netname, nick, separator=None, uid=''):
|
||||||
"""Creates a normalized nickname for the given nick suitable for
|
"""Creates a normalized nickname for the given nick suitable for
|
||||||
introduction to a remote network (as a relay client)."""
|
introduction to a remote network (as a relay client)."""
|
||||||
@ -111,13 +108,14 @@ def normalizeNick(irc, netname, nick, separator=None, uid=''):
|
|||||||
irc.serverdata.get('relay_force_slashes')
|
irc.serverdata.get('relay_force_slashes')
|
||||||
|
|
||||||
if '/' not in separator or not protocol_allows_slashes:
|
if '/' not in separator or not protocol_allows_slashes:
|
||||||
separator = separator.replace('/', '|')
|
separator = separator.replace('/', fallback_separator)
|
||||||
nick = nick.replace('/', '|')
|
nick = nick.replace('/', fallback_separator)
|
||||||
|
|
||||||
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
|
# On TS6 IRCds, nicks that start with 0-9 are only allowed if
|
||||||
# they match the UID of the originating server. Otherwise, you'll
|
# they match the UID of the originating server. Otherwise, you'll
|
||||||
# get nasty protocol violation SQUITs!
|
# get nasty protocol violation SQUITs!
|
||||||
|
# Nicks starting with - are likewise not valid.
|
||||||
nick = '_' + nick
|
nick = '_' + nick
|
||||||
|
|
||||||
suffix = separator + netname
|
suffix = separator + netname
|
||||||
@ -130,6 +128,12 @@ def normalizeNick(irc, netname, nick, separator=None, uid=''):
|
|||||||
nick = nick[:allowedlength]
|
nick = nick[:allowedlength]
|
||||||
nick += suffix
|
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)
|
||||||
|
|
||||||
# The nick we want exists? Darn, create another one then.
|
# The nick we want exists? Darn, create another one then.
|
||||||
# Increase the separator length by 1 if the user was already tagged,
|
# Increase the separator length by 1 if the user was already tagged,
|
||||||
# but couldn't be created due to a nick conflict.
|
# but couldn't be created due to a nick conflict.
|
||||||
@ -145,6 +149,7 @@ def normalizeNick(irc, netname, nick, separator=None, uid=''):
|
|||||||
new_sep = separator + separator[-1]
|
new_sep = separator + separator[-1]
|
||||||
log.debug('(%s) relay.normalizeNick: nick %r is in use; using %r as new_sep.', irc.name, nick, new_sep)
|
log.debug('(%s) relay.normalizeNick: nick %r is in use; using %r as new_sep.', irc.name, nick, new_sep)
|
||||||
nick = normalizeNick(irc, netname, orig_nick, separator=new_sep)
|
nick = normalizeNick(irc, netname, orig_nick, separator=new_sep)
|
||||||
|
|
||||||
finalLength = len(nick)
|
finalLength = len(nick)
|
||||||
assert finalLength <= maxnicklen, "Normalized nick %r went over max " \
|
assert finalLength <= maxnicklen, "Normalized nick %r went over max " \
|
||||||
"nick length (got: %s, allowed: %s!)" % (nick, finalLength, maxnicklen)
|
"nick length (got: %s, allowed: %s!)" % (nick, finalLength, maxnicklen)
|
||||||
@ -188,8 +193,7 @@ def scheduleExport(starting=False):
|
|||||||
# thing after start (i.e. DB has just been loaded).
|
# thing after start (i.e. DB has just been loaded).
|
||||||
exportDB()
|
exportDB()
|
||||||
|
|
||||||
# TODO: possibly make delay between exports configurable
|
exportdb_timer = threading.Timer(save_delay, scheduleExport)
|
||||||
exportdb_timer = threading.Timer(30, scheduleExport)
|
|
||||||
exportdb_timer.name = 'PyLink Relay exportDB Loop'
|
exportdb_timer.name = 'PyLink Relay exportDB Loop'
|
||||||
exportdb_timer.start()
|
exportdb_timer.start()
|
||||||
|
|
||||||
@ -240,15 +244,8 @@ def getRemoteSid(irc, remoteirc):
|
|||||||
"""Gets the remote server SID representing remoteirc on irc, spawning
|
"""Gets the remote server SID representing remoteirc on irc, spawning
|
||||||
it if it doesn't exist."""
|
it if it doesn't exist."""
|
||||||
|
|
||||||
try:
|
|
||||||
spawnservers = irc.conf['relay']['spawn_servers']
|
|
||||||
except KeyError:
|
|
||||||
spawnservers = True
|
|
||||||
if not spawnservers:
|
|
||||||
return irc.sid
|
|
||||||
|
|
||||||
log.debug('(%s) Grabbing spawnlocks_servers[%s]', irc.name, irc.name)
|
log.debug('(%s) Grabbing spawnlocks_servers[%s]', irc.name, irc.name)
|
||||||
with spawnlocks_servers[irc.name]:
|
if spawnlocks_servers[irc.name].acquire(5):
|
||||||
try:
|
try:
|
||||||
sid = relayservers[irc.name][remoteirc.name]
|
sid = relayservers[irc.name][remoteirc.name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -266,6 +263,7 @@ def getRemoteSid(irc, remoteirc):
|
|||||||
sid = spawnRelayServer(irc, remoteirc)
|
sid = spawnRelayServer(irc, remoteirc)
|
||||||
|
|
||||||
log.debug('(%s) getRemoteSid: got %s for %s.relay (round 2)', irc.name, sid, remoteirc.name)
|
log.debug('(%s) getRemoteSid: got %s for %s.relay (round 2)', irc.name, sid, remoteirc.name)
|
||||||
|
spawnlocks_servers[irc.name].release()
|
||||||
return sid
|
return sid
|
||||||
|
|
||||||
def spawnRelayUser(irc, remoteirc, user):
|
def spawnRelayUser(irc, remoteirc, user):
|
||||||
@ -325,11 +323,17 @@ def spawnRelayUser(irc, remoteirc, user):
|
|||||||
modes=modes, ts=userobj.ts,
|
modes=modes, ts=userobj.ts,
|
||||||
opertype=opertype, server=rsid,
|
opertype=opertype, server=rsid,
|
||||||
ip=ip, realhost=realhost).uid
|
ip=ip, realhost=realhost).uid
|
||||||
|
try:
|
||||||
remoteirc.users[u].remote = (irc.name, user)
|
remoteirc.users[u].remote = (irc.name, user)
|
||||||
remoteirc.users[u].opertype = opertype
|
remoteirc.users[u].opertype = opertype
|
||||||
away = userobj.away
|
away = userobj.away
|
||||||
if away:
|
if away:
|
||||||
remoteirc.proto.away(u, away)
|
remoteirc.proto.away(u, away)
|
||||||
|
except KeyError:
|
||||||
|
# User got killed somehow while we were setting options on it.
|
||||||
|
# This is probably being done by the uplink, due to something like an
|
||||||
|
# invalid nick, etc.
|
||||||
|
raise
|
||||||
|
|
||||||
relayusers[(irc.name, user)][remoteirc.name] = u
|
relayusers[(irc.name, user)][remoteirc.name] = u
|
||||||
return u
|
return u
|
||||||
@ -349,7 +353,7 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
|
|||||||
return sbot.uids.get(remoteirc.name)
|
return sbot.uids.get(remoteirc.name)
|
||||||
|
|
||||||
log.debug('(%s) Grabbing spawnlocks[%s]', irc.name, irc.name)
|
log.debug('(%s) Grabbing spawnlocks[%s]', irc.name, irc.name)
|
||||||
with spawnlocks[irc.name]:
|
if spawnlocks[irc.name].acquire(5):
|
||||||
# Be sort-of thread safe: lock the user spawns for the current net first.
|
# Be sort-of thread safe: lock the user spawns for the current net first.
|
||||||
u = None
|
u = None
|
||||||
try:
|
try:
|
||||||
@ -366,7 +370,10 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
|
|||||||
# cache for the requested UID, but it doesn't match the request,
|
# 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.
|
# assume it was a leftover from the last split and replace it with a new one.
|
||||||
if u and ((u not in remoteirc.users) or remoteirc.users[u].remote != (irc.name, user)):
|
if u and ((u not in remoteirc.users) or remoteirc.users[u].remote != (irc.name, user)):
|
||||||
spawnRelayUser(irc, remoteirc, user)
|
u = spawnRelayUser(irc, remoteirc, user)
|
||||||
|
|
||||||
|
spawnlocks[irc.name].release()
|
||||||
|
|
||||||
return u
|
return u
|
||||||
|
|
||||||
def getOrigUser(irc, user, targetirc=None):
|
def getOrigUser(irc, user, targetirc=None):
|
||||||
@ -396,9 +403,9 @@ def getOrigUser(irc, user, targetirc=None):
|
|||||||
# Otherwise, use getRemoteUser to find our UID.
|
# Otherwise, use getRemoteUser to find our UID.
|
||||||
res = getRemoteUser(sourceobj, targetirc, remoteuser[1],
|
res = getRemoteUser(sourceobj, targetirc, remoteuser[1],
|
||||||
spawnIfMissing=False)
|
spawnIfMissing=False)
|
||||||
log.debug('(%s) relay.getOrigUser: targetirc found, getting %r as '
|
log.debug('(%s) relay.getOrigUser: targetirc found as %s, getting %r as '
|
||||||
'remoteuser for %r (looking up %s/%s).', irc.name, res,
|
'remoteuser for %r (looking up %s/%s).', irc.name, targetirc.name,
|
||||||
remoteuser[1], user, irc.name)
|
res, remoteuser[1], user, irc.name)
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
return remoteuser
|
return remoteuser
|
||||||
@ -469,6 +476,8 @@ def initializeChannel(irc, channel):
|
|||||||
# Send our users and channel modes to the other nets
|
# Send our users and channel modes to the other nets
|
||||||
log.debug('(%s) relay.initializeChannel: joining our (%s) users: %s', irc.name, remotenet, irc.channels[channel].users)
|
log.debug('(%s) relay.initializeChannel: joining our (%s) users: %s', irc.name, remotenet, irc.channels[channel].users)
|
||||||
relayJoins(irc, channel, irc.channels[channel].users, irc.channels[channel].ts)
|
relayJoins(irc, channel, irc.channels[channel].users, irc.channels[channel].ts)
|
||||||
|
|
||||||
|
world.services['pylink'].extra_channels[irc.name].add(channel)
|
||||||
if irc.pseudoclient and irc.pseudoclient.uid not in irc.channels[channel].users:
|
if irc.pseudoclient and irc.pseudoclient.uid not in irc.channels[channel].users:
|
||||||
irc.proto.join(irc.pseudoclient.uid, channel)
|
irc.proto.join(irc.pseudoclient.uid, channel)
|
||||||
|
|
||||||
@ -477,7 +486,8 @@ def removeChannel(irc, channel):
|
|||||||
if irc is None:
|
if irc is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if channel not in map(str.lower, irc.serverdata['channels']):
|
if channel not in map(str.lower, irc.serverdata.get('channels', [])):
|
||||||
|
world.services['pylink'].extra_channels[irc.name].discard(channel)
|
||||||
irc.proto.part(irc.pseudoclient.uid, channel, 'Channel delinked.')
|
irc.proto.part(irc.pseudoclient.uid, channel, 'Channel delinked.')
|
||||||
|
|
||||||
relay = getRelay((irc.name, channel))
|
relay = getRelay((irc.name, channel))
|
||||||
@ -488,7 +498,7 @@ def removeChannel(irc, channel):
|
|||||||
# Don't ever part the main client from any of its autojoin channels.
|
# Don't ever part the main client from any of its autojoin channels.
|
||||||
else:
|
else:
|
||||||
if user == irc.pseudoclient.uid and channel in \
|
if user == irc.pseudoclient.uid and channel in \
|
||||||
irc.serverdata['channels']:
|
irc.serverdata.get('channels', []):
|
||||||
continue
|
continue
|
||||||
irc.proto.part(user, channel, 'Channel delinked.')
|
irc.proto.part(user, channel, 'Channel delinked.')
|
||||||
# Don't ever quit it either...
|
# Don't ever quit it either...
|
||||||
@ -632,14 +642,17 @@ def relayJoins(irc, channel, users, ts, burst=True):
|
|||||||
# SJOIN will be used if either the amount of users to join is > 1, or there are modes
|
# SJOIN will be used if either the amount of users to join is > 1, or there are modes
|
||||||
# to be set on the joining user.
|
# to be set on the joining user.
|
||||||
if burst or len(queued_users) > 1 or queued_users[0][0]:
|
if burst or len(queued_users) > 1 or queued_users[0][0]:
|
||||||
# Send the SJOIN from the relay subserver on the target network.
|
modes = getSupportedCmodes(irc, remoteirc, channel, irc.channels[channel].modes)
|
||||||
rsid = getRemoteSid(remoteirc, irc)
|
remoteirc.proto.sjoin(getRemoteSid(remoteirc, irc), remotechan, queued_users, ts=ts, modes=modes)
|
||||||
remoteirc.proto.sjoin(rsid, remotechan, queued_users, ts=ts)
|
|
||||||
relayModes(irc, remoteirc, getRemoteSid(irc, remoteirc), channel, irc.channels[channel].modes)
|
|
||||||
else:
|
else:
|
||||||
# A regular JOIN only needs the user and the channel. TS, source SID, etc., can all be omitted.
|
# A regular JOIN only needs the user and the channel. TS, source SID, etc., can all be omitted.
|
||||||
remoteirc.proto.join(queued_users[0][1], remotechan)
|
remoteirc.proto.join(queued_users[0][1], remotechan)
|
||||||
|
|
||||||
|
# Announce this JOIN as a hook, for plugins like Automode. Is this a hack? Yeah.
|
||||||
|
# Plugins can communicate with each other using hooks.
|
||||||
|
remoteirc.callHooks([remoteirc.sid, 'PYLINK_RELAY_JOIN',
|
||||||
|
{'channel': remotechan, 'users': [u[-1] for u in queued_users]}])
|
||||||
|
|
||||||
def relayPart(irc, channel, user):
|
def relayPart(irc, channel, user):
|
||||||
"""
|
"""
|
||||||
Relays a user part from a channel to its relay links, as part of a channel delink.
|
Relays a user part from a channel to its relay links, as part of a channel delink.
|
||||||
@ -682,22 +695,15 @@ whitelisted_cmodes = {'admin', 'allowinvite', 'autoop', 'ban', 'banexception',
|
|||||||
whitelisted_umodes = {'bot', 'hidechans', 'hideoper', 'invisible', 'oper',
|
whitelisted_umodes = {'bot', 'hidechans', 'hideoper', 'invisible', 'oper',
|
||||||
'regdeaf', 'stripcolor', 'noctcp', 'wallops',
|
'regdeaf', 'stripcolor', 'noctcp', 'wallops',
|
||||||
'hideidle'}
|
'hideidle'}
|
||||||
def relayModes(irc, remoteirc, sender, channel, modes=None):
|
def getSupportedCmodes(irc, remoteirc, channel, modes):
|
||||||
"""
|
"""
|
||||||
Relays a mode change on a channel to its relay links.
|
Filters a channel mode change to the modes supported by the target IRCd.
|
||||||
"""
|
"""
|
||||||
remotechan = getRemoteChan(irc, remoteirc, channel)
|
remotechan = getRemoteChan(irc, remoteirc, channel)
|
||||||
log.debug('(%s) relay.relayModes: remotechan for %s on %s is %s', irc.name, channel, irc.name, remotechan)
|
if not remotechan: # Not a relay channel
|
||||||
|
return []
|
||||||
|
|
||||||
if remotechan is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if modes is None:
|
|
||||||
modes = irc.channels[channel].modes
|
|
||||||
log.debug('(%s) relay.relayModes: channel data for %s%s: %s', irc.name, remoteirc.name, remotechan, remoteirc.channels[remotechan])
|
|
||||||
supported_modes = []
|
supported_modes = []
|
||||||
log.debug('(%s) relay.relayModes: initial modelist for %s is %s', irc.name, channel, modes)
|
|
||||||
|
|
||||||
for modepair in modes:
|
for modepair in modes:
|
||||||
try:
|
try:
|
||||||
prefix, modechar = modepair[0]
|
prefix, modechar = modepair[0]
|
||||||
@ -717,7 +723,7 @@ def relayModes(irc, remoteirc, sender, channel, modes=None):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if name not in whitelisted_cmodes:
|
if name not in whitelisted_cmodes:
|
||||||
log.debug("(%s) relay.relayModes: skipping mode (%r, %r) because "
|
log.debug("(%s) relay.getSupportedCmodes: skipping mode (%r, %r) because "
|
||||||
"it isn't a whitelisted (safe) mode for relay.",
|
"it isn't a whitelisted (safe) mode for relay.",
|
||||||
irc.name, modechar, arg)
|
irc.name, modechar, arg)
|
||||||
break
|
break
|
||||||
@ -725,7 +731,7 @@ def relayModes(irc, remoteirc, sender, channel, modes=None):
|
|||||||
if modechar in irc.prefixmodes:
|
if modechar in irc.prefixmodes:
|
||||||
# This is a prefix mode (e.g. +o). We must coerse the argument
|
# This is a prefix mode (e.g. +o). We must coerse the argument
|
||||||
# so that the target exists on the remote relay network.
|
# so that the target exists on the remote relay network.
|
||||||
log.debug("(%s) relay.relayModes: coersing argument of (%r, %r) "
|
log.debug("(%s) relay.getSupportedCmodes: coersing argument of (%r, %r) "
|
||||||
"for network %r.",
|
"for network %r.",
|
||||||
irc.name, modechar, arg, remoteirc.name)
|
irc.name, modechar, arg, remoteirc.name)
|
||||||
|
|
||||||
@ -734,18 +740,21 @@ def relayModes(irc, remoteirc, sender, channel, modes=None):
|
|||||||
arg = getOrigUser(irc, arg, targetirc=remoteirc) or \
|
arg = getOrigUser(irc, arg, targetirc=remoteirc) or \
|
||||||
getRemoteUser(irc, remoteirc, arg, spawnIfMissing=False)
|
getRemoteUser(irc, remoteirc, arg, spawnIfMissing=False)
|
||||||
|
|
||||||
log.debug("(%s) relay.relayModes: argument found as (%r, %r) "
|
if arg is None:
|
||||||
|
# Relay client for target user doesn't exist yet. Drop the mode.
|
||||||
|
break
|
||||||
|
|
||||||
|
log.debug("(%s) relay.getSupportedCmodes: argument found as (%r, %r) "
|
||||||
"for network %r.",
|
"for network %r.",
|
||||||
irc.name, modechar, arg, remoteirc.name)
|
irc.name, modechar, arg, remoteirc.name)
|
||||||
oplist = remoteirc.channels[remotechan].prefixmodes[name]
|
oplist = remoteirc.channels[remotechan].prefixmodes[name]
|
||||||
|
|
||||||
log.debug("(%s) relay.relayModes: list of %ss on %r is: %s",
|
log.debug("(%s) relay.getSupportedCmodes: list of %ss on %r is: %s",
|
||||||
irc.name, name, remotechan, oplist)
|
irc.name, name, remotechan, oplist)
|
||||||
|
|
||||||
if prefix == '+' and arg in oplist:
|
if prefix == '+' and arg in oplist:
|
||||||
|
|
||||||
# Don't set prefix modes that are already set.
|
# Don't set prefix modes that are already set.
|
||||||
log.debug("(%s) relay.relayModes: skipping setting %s on %s/%s because it appears to be already set.",
|
log.debug("(%s) relay.getSupportedCmodes: skipping setting %s on %s/%s because it appears to be already set.",
|
||||||
irc.name, name, arg, remoteirc.name)
|
irc.name, name, arg, remoteirc.name)
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -754,32 +763,22 @@ def relayModes(irc, remoteirc, sender, channel, modes=None):
|
|||||||
if supported_char:
|
if supported_char:
|
||||||
final_modepair = (prefix+supported_char, arg)
|
final_modepair = (prefix+supported_char, arg)
|
||||||
if name in ('ban', 'banexception', 'invex') and not utils.isHostmask(arg):
|
if name in ('ban', 'banexception', 'invex') and not utils.isHostmask(arg):
|
||||||
|
|
||||||
# Don't add bans that don't match n!u@h syntax!
|
# Don't add bans that don't match n!u@h syntax!
|
||||||
log.debug("(%s) relay.relayModes: skipping mode (%r, %r) because it doesn't match nick!user@host syntax.",
|
log.debug("(%s) relay.getSupportedCmodes: skipping mode (%r, %r) because it doesn't match nick!user@host syntax.",
|
||||||
irc.name, modechar, arg)
|
irc.name, modechar, arg)
|
||||||
break
|
break
|
||||||
|
|
||||||
# Don't set modes that are already set, to prevent floods on TS6
|
# Don't set modes that are already set, to prevent floods on TS6
|
||||||
# where the same mode can be set infinite times.
|
# where the same mode can be set infinite times.
|
||||||
if prefix == '+' and final_modepair in remoteirc.channels[remotechan].modes:
|
if prefix == '+' and final_modepair in remoteirc.channels[remotechan].modes:
|
||||||
log.debug("(%s) relay.relayModes: skipping setting mode (%r, %r) on %s%s because it appears to be already set.",
|
log.debug("(%s) relay.getSupportedCmodes: skipping setting mode (%r, %r) on %s%s because it appears to be already set.",
|
||||||
irc.name, supported_char, arg, remoteirc.name, remotechan)
|
irc.name, supported_char, arg, remoteirc.name, remotechan)
|
||||||
break
|
break
|
||||||
|
|
||||||
supported_modes.append(final_modepair)
|
supported_modes.append(final_modepair)
|
||||||
|
|
||||||
log.debug('(%s) relay.relayModes: final modelist (sending to %s%s) is %s', irc.name, remoteirc.name, remotechan, supported_modes)
|
log.debug('(%s) relay.getSupportedCmodes: final modelist (sending to %s%s) is %s', irc.name, remoteirc.name, remotechan, supported_modes)
|
||||||
|
return supported_modes
|
||||||
# Don't send anything if there are no supported modes left after filtering.
|
|
||||||
if supported_modes:
|
|
||||||
# Check if the sender is a user; remember servers are allowed to set modes too.
|
|
||||||
u = getRemoteUser(irc, remoteirc, sender, spawnIfMissing=False)
|
|
||||||
if u:
|
|
||||||
remoteirc.proto.mode(u, remotechan, supported_modes)
|
|
||||||
else:
|
|
||||||
rsid = getRemoteSid(remoteirc, irc)
|
|
||||||
remoteirc.proto.mode(rsid, remotechan, supported_modes)
|
|
||||||
|
|
||||||
### EVENT HANDLERS
|
### EVENT HANDLERS
|
||||||
|
|
||||||
@ -832,7 +831,10 @@ def handle_relay_whois(irc, source, command, args):
|
|||||||
utils.add_hook(handle_relay_whois, 'PYLINK_CUSTOM_WHOIS')
|
utils.add_hook(handle_relay_whois, 'PYLINK_CUSTOM_WHOIS')
|
||||||
|
|
||||||
def handle_operup(irc, numeric, command, args):
|
def handle_operup(irc, numeric, command, args):
|
||||||
newtype = args['text'] + '_(remote)'
|
"""
|
||||||
|
Handles setting oper types on relay clients during oper up.
|
||||||
|
"""
|
||||||
|
newtype = args['text'] + ' (remote)'
|
||||||
for netname, user in relayusers[(irc.name, numeric)].items():
|
for netname, user in relayusers[(irc.name, numeric)].items():
|
||||||
log.debug('(%s) relay.handle_opertype: setting OPERTYPE of %s/%s to %s',
|
log.debug('(%s) relay.handle_opertype: setting OPERTYPE of %s/%s to %s',
|
||||||
irc.name, user, netname, newtype)
|
irc.name, user, netname, newtype)
|
||||||
@ -854,7 +856,7 @@ def handle_quit(irc, numeric, command, args):
|
|||||||
# Lock the user spawning mechanism before proceeding, since we're going to be
|
# Lock the user spawning mechanism before proceeding, since we're going to be
|
||||||
# deleting client from the relayusers cache.
|
# deleting client from the relayusers cache.
|
||||||
log.debug('(%s) Grabbing spawnlocks[%s]', irc.name, irc.name)
|
log.debug('(%s) Grabbing spawnlocks[%s]', irc.name, irc.name)
|
||||||
with spawnlocks[irc.name]:
|
if spawnlocks[irc.name].acquire(5):
|
||||||
for netname, user in relayusers[(irc.name, numeric)].copy().items():
|
for netname, user in relayusers[(irc.name, numeric)].copy().items():
|
||||||
remoteirc = world.networkobjects[netname]
|
remoteirc = world.networkobjects[netname]
|
||||||
try: # Try to quit the client. If this fails because they're missing, bail.
|
try: # Try to quit the client. If this fails because they're missing, bail.
|
||||||
@ -862,28 +864,52 @@ def handle_quit(irc, numeric, command, args):
|
|||||||
except LookupError:
|
except LookupError:
|
||||||
pass
|
pass
|
||||||
del relayusers[(irc.name, numeric)]
|
del relayusers[(irc.name, numeric)]
|
||||||
|
spawnlocks[irc.name].release()
|
||||||
|
|
||||||
utils.add_hook(handle_quit, 'QUIT')
|
utils.add_hook(handle_quit, 'QUIT')
|
||||||
|
|
||||||
def handle_squit(irc, numeric, command, args):
|
def handle_squit(irc, numeric, command, args):
|
||||||
|
"""
|
||||||
|
Handles SQUITs over relay.
|
||||||
|
"""
|
||||||
users = args['users']
|
users = args['users']
|
||||||
target = args['target']
|
target = args['target']
|
||||||
|
|
||||||
# Someone /SQUIT one of our relay subservers. Bad! Rejoin them!
|
# Someone /SQUIT one of our relay subservers. Bad! Rejoin them!
|
||||||
if target in relayservers[irc.name].values():
|
if target in relayservers[irc.name].values():
|
||||||
sname = args['name']
|
sname = args['name']
|
||||||
remotenet = sname.split('.', 1)[0]
|
remotenet = sname.split('.', 1)[0]
|
||||||
del relayservers[irc.name][remotenet]
|
del relayservers[irc.name][remotenet]
|
||||||
|
|
||||||
for userpair in relayusers:
|
for userpair in relayusers:
|
||||||
if userpair[0] == remotenet and irc.name in relayusers[userpair]:
|
if userpair[0] == remotenet and irc.name in relayusers[userpair]:
|
||||||
del relayusers[userpair][irc.name]
|
del relayusers[userpair][irc.name]
|
||||||
|
|
||||||
remoteirc = world.networkobjects[remotenet]
|
remoteirc = world.networkobjects[remotenet]
|
||||||
initializeAll(remoteirc)
|
initializeAll(remoteirc)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Some other netsplit happened on the network, we'll have to fake
|
# Some other netsplit happened on the network, we'll have to fake
|
||||||
# some *.net *.split quits for that.
|
# some *.net *.split quits for that.
|
||||||
for user in users:
|
for user in users:
|
||||||
log.debug('(%s) relay.handle_squit: sending handle_quit on %s', irc.name, user)
|
log.debug('(%s) relay.handle_squit: sending handle_quit on %s', irc.name, user)
|
||||||
handle_quit(irc, user, command, {'text': '*.net *.split'})
|
|
||||||
|
try: # Allow netsplit hiding to be toggled
|
||||||
|
show_splits = irc.conf['relay']['show_netsplits']
|
||||||
|
except KeyError:
|
||||||
|
show_splits = False
|
||||||
|
|
||||||
|
text = '*.net *.split'
|
||||||
|
if show_splits:
|
||||||
|
uplink = args['uplink']
|
||||||
|
try:
|
||||||
|
text = '%s %s' % (irc.servers[uplink].name, args['name'])
|
||||||
|
except (KeyError, AttributeError):
|
||||||
|
log.warning("(%s) relay.handle_squit: Failed to get server name for %s",
|
||||||
|
irc.name, uplink)
|
||||||
|
|
||||||
|
handle_quit(irc, user, command, {'text': text})
|
||||||
|
|
||||||
utils.add_hook(handle_squit, 'SQUIT')
|
utils.add_hook(handle_squit, 'SQUIT')
|
||||||
|
|
||||||
def handle_nick(irc, numeric, command, args):
|
def handle_nick(irc, numeric, command, args):
|
||||||
@ -921,22 +947,11 @@ def handle_messages(irc, numeric, command, args):
|
|||||||
# but whatever).
|
# but whatever).
|
||||||
return
|
return
|
||||||
|
|
||||||
elif numeric in irc.servers:
|
|
||||||
# Sender is a server? This shouldn't be allowed, except for some truly
|
|
||||||
# special cases... We'll route these through the main PyLink client,
|
|
||||||
# tagging the message with the sender name.
|
|
||||||
text = '[from %s] %s' % (irc.servers[numeric].name, text)
|
|
||||||
numeric = irc.pseudoclient.uid
|
|
||||||
|
|
||||||
elif numeric not in irc.users:
|
|
||||||
# Sender didn't pass the check above, AND isn't a user.
|
|
||||||
log.debug('(%s) relay.handle_messages: Unknown message sender %s.', irc.name, numeric)
|
|
||||||
return
|
|
||||||
|
|
||||||
relay = getRelay((irc.name, target))
|
relay = getRelay((irc.name, target))
|
||||||
remoteusers = relayusers[(irc.name, numeric)]
|
remoteusers = relayusers[(irc.name, numeric)]
|
||||||
|
|
||||||
# HACK: Don't break on sending to @#channel or similar.
|
# HACK: Don't break on sending to @#channel or similar. TODO: This should really
|
||||||
|
# be handled more neatly in core.
|
||||||
try:
|
try:
|
||||||
prefix, target = target.split('#', 1)
|
prefix, target = target.split('#', 1)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -944,16 +959,6 @@ def handle_messages(irc, numeric, command, args):
|
|||||||
else:
|
else:
|
||||||
target = '#' + target
|
target = '#' + target
|
||||||
|
|
||||||
log.debug('(%s) relay.handle_messages: prefix is %r, target is %r', irc.name, prefix, target)
|
|
||||||
if utils.isChannel(target) and relay and numeric not in irc.channels[target].users:
|
|
||||||
# The sender must be in the target channel to send messages over the relay;
|
|
||||||
# it's the only way we can make sure they have a spawned client on ALL
|
|
||||||
# of the linked networks. This affects -n channels too; see
|
|
||||||
# https://github.com/GLolol/PyLink/issues/91 for an explanation of why.
|
|
||||||
irc.msg(numeric, 'Error: You must be in %r in order to send '
|
|
||||||
'messages over the relay.' % target, notice=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
if utils.isChannel(target):
|
if utils.isChannel(target):
|
||||||
for name, remoteirc in world.networkobjects.copy().items():
|
for name, remoteirc in world.networkobjects.copy().items():
|
||||||
real_target = getRemoteChan(irc, remoteirc, target)
|
real_target = getRemoteChan(irc, remoteirc, target)
|
||||||
@ -965,14 +970,45 @@ def handle_messages(irc, numeric, command, args):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
|
user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
|
||||||
if user: # If the user doesn't exist, drop the message.
|
|
||||||
|
if not user:
|
||||||
|
# No relay clone exists for the sender; route the message through our
|
||||||
|
# main client.
|
||||||
|
if numeric in irc.servers:
|
||||||
|
displayedname = irc.servers[numeric].name
|
||||||
|
else:
|
||||||
|
displayedname = irc.users[numeric].nick
|
||||||
|
|
||||||
|
real_text = '[from %s/%s] %s' % (displayedname, irc.name, text)
|
||||||
|
try:
|
||||||
|
user = remoteirc.pseudoclient.uid
|
||||||
|
except AttributeError:
|
||||||
|
# Remote main client hasn't spawned yet. Drop the message.
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if remoteirc.pseudoclient.uid not in remoteirc.users:
|
||||||
|
# Remote UID is ghosted, drop message.
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
real_text = text
|
||||||
|
|
||||||
real_target = prefix + real_target
|
real_target = prefix + real_target
|
||||||
log.debug('(%s) relay.handle_messages: sending message to %s from %s on behalf of %s',
|
log.debug('(%s) relay.handle_messages: sending message to %s from %s on behalf of %s',
|
||||||
irc.name, real_target, user, numeric)
|
irc.name, real_target, user, numeric)
|
||||||
|
|
||||||
|
try:
|
||||||
if notice:
|
if notice:
|
||||||
remoteirc.proto.notice(user, real_target, text)
|
remoteirc.proto.notice(user, real_target, real_text)
|
||||||
else:
|
else:
|
||||||
remoteirc.proto.message(user, real_target, text)
|
remoteirc.proto.message(user, real_target, real_text)
|
||||||
|
except LookupError:
|
||||||
|
# Our relay clone disappeared while we were trying to send the message.
|
||||||
|
# This is normally due to a nick conflict with the IRCd.
|
||||||
|
log.warning("(%s) relay: Relay client %s on %s was killed while "
|
||||||
|
"trying to send a message through it!", irc.name,
|
||||||
|
remoteirc.name, user)
|
||||||
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Get the real user that the PM was meant for
|
# Get the real user that the PM was meant for
|
||||||
@ -993,10 +1029,18 @@ def handle_messages(irc, numeric, command, args):
|
|||||||
remoteirc = world.networkobjects[homenet]
|
remoteirc = world.networkobjects[homenet]
|
||||||
user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
|
user = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
|
||||||
|
|
||||||
|
try:
|
||||||
if notice:
|
if notice:
|
||||||
remoteirc.proto.notice(user, real_target, text)
|
remoteirc.proto.notice(user, real_target, text)
|
||||||
else:
|
else:
|
||||||
remoteirc.proto.message(user, real_target, text)
|
remoteirc.proto.message(user, real_target, text)
|
||||||
|
except LookupError:
|
||||||
|
# Our relay clone disappeared while we were trying to send the message.
|
||||||
|
# This is normally due to a nick conflict with the IRCd.
|
||||||
|
log.warning("(%s) relay: Relay client %s on %s was killed while "
|
||||||
|
"trying to send a message through it!", irc.name,
|
||||||
|
remoteirc.name, user)
|
||||||
|
return
|
||||||
|
|
||||||
for cmd in ('PRIVMSG', 'NOTICE', 'PYLINK_SELF_NOTICE', 'PYLINK_SELF_PRIVMSG'):
|
for cmd in ('PRIVMSG', 'NOTICE', 'PYLINK_SELF_NOTICE', 'PYLINK_SELF_PRIVMSG'):
|
||||||
utils.add_hook(handle_messages, cmd)
|
utils.add_hook(handle_messages, cmd)
|
||||||
@ -1122,10 +1166,21 @@ def handle_mode(irc, numeric, command, args):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if utils.isChannel(target):
|
if utils.isChannel(target):
|
||||||
|
# Use the old state of the channel to check for CLAIM access.
|
||||||
oldchan = args.get('oldchan')
|
oldchan = args.get('oldchan')
|
||||||
|
|
||||||
if checkClaim(irc, target, numeric, chanobj=oldchan):
|
if checkClaim(irc, target, numeric, chanobj=oldchan):
|
||||||
relayModes(irc, remoteirc, numeric, target, modes)
|
remotechan = getRemoteChan(irc, remoteirc, target)
|
||||||
|
supported_modes = getSupportedCmodes(irc, remoteirc, target, modes)
|
||||||
|
if supported_modes:
|
||||||
|
# Check if the sender is a user with a relay client; otherwise relay the mode
|
||||||
|
# from the corresponding server.
|
||||||
|
u = getRemoteUser(irc, remoteirc, numeric, spawnIfMissing=False)
|
||||||
|
if u:
|
||||||
|
remoteirc.proto.mode(u, remotechan, supported_modes)
|
||||||
|
else:
|
||||||
|
rsid = getRemoteSid(remoteirc, irc)
|
||||||
|
remoteirc.proto.mode(rsid, remotechan, supported_modes)
|
||||||
else: # Mode change blocked by CLAIM.
|
else: # Mode change blocked by CLAIM.
|
||||||
reversed_modes = irc.reverseModes(target, modes, oldobj=oldchan)
|
reversed_modes = irc.reverseModes(target, modes, oldobj=oldchan)
|
||||||
log.debug('(%s) relay.handle_mode: Reversing mode changes of %r with %r (CLAIM).',
|
log.debug('(%s) relay.handle_mode: Reversing mode changes of %r with %r (CLAIM).',
|
||||||
@ -1261,22 +1316,32 @@ def handle_endburst(irc, numeric, command, args):
|
|||||||
initializeAll(irc)
|
initializeAll(irc)
|
||||||
utils.add_hook(handle_endburst, "ENDBURST")
|
utils.add_hook(handle_endburst, "ENDBURST")
|
||||||
|
|
||||||
|
def handle_services_login(irc, numeric, command, args):
|
||||||
|
"""
|
||||||
|
Relays services account changes as a hook, for integration with plugins like Automode.
|
||||||
|
"""
|
||||||
|
for netname, user in relayusers[(irc.name, numeric)].items():
|
||||||
|
remoteirc = world.networkobjects[netname]
|
||||||
|
remoteirc.callHooks([user, 'PYLINK_RELAY_SERVICES_LOGIN', args])
|
||||||
|
utils.add_hook(handle_services_login, 'CLIENT_SERVICES_LOGIN')
|
||||||
|
|
||||||
def handle_disconnect(irc, numeric, command, args):
|
def handle_disconnect(irc, numeric, command, args):
|
||||||
"""Handles IRC network disconnections (internal hook)."""
|
"""Handles IRC network disconnections (internal hook)."""
|
||||||
# Quit all of our users' representations on other nets, and remove
|
# Quit all of our users' representations on other nets, and remove
|
||||||
# them from our relay clients index.
|
# them from our relay clients index.
|
||||||
log.debug('(%s) Grabbing spawnlocks[%s]', irc.name, irc.name)
|
log.debug('(%s) Grabbing spawnlocks[%s]', irc.name, irc.name)
|
||||||
with spawnlocks[irc.name]:
|
if spawnlocks[irc.name].acquire(5):
|
||||||
for k, v in relayusers.copy().items():
|
for k, v in relayusers.copy().items():
|
||||||
if irc.name in v:
|
if irc.name in v:
|
||||||
del relayusers[k][irc.name]
|
del relayusers[k][irc.name]
|
||||||
if k[0] == irc.name:
|
if k[0] == irc.name:
|
||||||
handle_quit(irc, k[1], 'PYLINK_DISCONNECT', {'text': 'Relay network lost connection.'})
|
del relayusers[k]
|
||||||
|
spawnlocks[irc.name].release()
|
||||||
|
|
||||||
# SQUIT all relay pseudoservers spawned for us, and remove them
|
# SQUIT all relay pseudoservers spawned for us, and remove them
|
||||||
# from our relay subservers index.
|
# from our relay subservers index.
|
||||||
log.debug('(%s) Grabbing spawnlocks_servers[%s]', irc.name, irc.name)
|
log.debug('(%s) Grabbing spawnlocks_servers[%s]', irc.name, irc.name)
|
||||||
with spawnlocks_servers[irc.name]:
|
if spawnlocks_servers[irc.name].acquire(5):
|
||||||
for name, ircobj in world.networkobjects.copy().items():
|
for name, ircobj in world.networkobjects.copy().items():
|
||||||
if name != irc.name:
|
if name != irc.name:
|
||||||
try:
|
try:
|
||||||
@ -1289,7 +1354,12 @@ def handle_disconnect(irc, numeric, command, args):
|
|||||||
if irc.name in relayservers[name]:
|
if irc.name in relayservers[name]:
|
||||||
del relayservers[name][irc.name]
|
del relayservers[name][irc.name]
|
||||||
|
|
||||||
|
try:
|
||||||
del relayservers[irc.name]
|
del relayservers[irc.name]
|
||||||
|
except KeyError: # Already removed; ignore.
|
||||||
|
pass
|
||||||
|
|
||||||
|
spawnlocks_servers[irc.name].release()
|
||||||
|
|
||||||
utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT")
|
utils.add_hook(handle_disconnect, "PYLINK_DISCONNECT")
|
||||||
|
|
||||||
@ -1319,11 +1389,10 @@ utils.add_hook(handle_save, "SAVE")
|
|||||||
|
|
||||||
### PUBLIC COMMANDS
|
### PUBLIC COMMANDS
|
||||||
|
|
||||||
@utils.add_cmd
|
|
||||||
def create(irc, source, args):
|
def create(irc, source, args):
|
||||||
"""<channel>
|
"""<channel>
|
||||||
|
|
||||||
Creates the channel <channel> over the relay."""
|
Opens up the given channel over PyLink Relay."""
|
||||||
try:
|
try:
|
||||||
channel = irc.toLower(args[0])
|
channel = irc.toLower(args[0])
|
||||||
except IndexError:
|
except IndexError:
|
||||||
@ -1353,12 +1422,12 @@ def create(irc, source, args):
|
|||||||
log.info('(%s) relay: Channel %s created by %s.', irc.name, channel, creator)
|
log.info('(%s) relay: Channel %s created by %s.', irc.name, channel, creator)
|
||||||
initializeChannel(irc, channel)
|
initializeChannel(irc, channel)
|
||||||
irc.reply('Done.')
|
irc.reply('Done.')
|
||||||
|
create = utils.add_cmd(create, featured=True)
|
||||||
|
|
||||||
@utils.add_cmd
|
|
||||||
def destroy(irc, source, args):
|
def destroy(irc, source, args):
|
||||||
"""[<home network>] <channel>
|
"""[<home network>] <channel>
|
||||||
|
|
||||||
Removes <channel> from the relay, delinking all networks linked to it. If <home network> is given and you are logged in as admin, this can also remove relay channels from other networks."""
|
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.
|
try: # Two args were given: first one is network name, second is channel.
|
||||||
channel = irc.toLower(args[1])
|
channel = irc.toLower(args[1])
|
||||||
network = args[0]
|
network = args[0]
|
||||||
@ -1397,13 +1466,13 @@ def destroy(irc, source, args):
|
|||||||
irc.reply("Error: No such channel %r exists. If you're trying to delink a channel from "
|
irc.reply("Error: No such channel %r exists. If you're trying to delink a channel from "
|
||||||
"another network, use the DESTROY command." % channel)
|
"another network, use the DESTROY command." % channel)
|
||||||
return
|
return
|
||||||
|
destroy = utils.add_cmd(destroy, featured=True)
|
||||||
|
|
||||||
@utils.add_cmd
|
|
||||||
def link(irc, source, args):
|
def link(irc, source, args):
|
||||||
"""<remotenet> <channel> <local channel>
|
"""<remotenet> <channel> <local channel>
|
||||||
|
|
||||||
Links channel <channel> on <remotenet> over the relay to <local channel>.
|
Links the specified channel on \x02remotenet\x02 over PyLink Relay as \x02local channel\x02.
|
||||||
If <local channel> is not specified, it defaults to the same name as <channel>."""
|
If \x02local channel\x02 is not specified, it defaults to the same name as \x02channel\x02."""
|
||||||
try:
|
try:
|
||||||
channel = irc.toLower(args[1])
|
channel = irc.toLower(args[1])
|
||||||
remotenet = args[0]
|
remotenet = args[0]
|
||||||
@ -1459,13 +1528,13 @@ def link(irc, source, args):
|
|||||||
localchan, remotenet, channel, irc.getHostmask(source))
|
localchan, remotenet, channel, irc.getHostmask(source))
|
||||||
initializeChannel(irc, localchan)
|
initializeChannel(irc, localchan)
|
||||||
irc.reply('Done.')
|
irc.reply('Done.')
|
||||||
|
link = utils.add_cmd(link, featured=True)
|
||||||
|
|
||||||
@utils.add_cmd
|
|
||||||
def delink(irc, source, args):
|
def delink(irc, source, args):
|
||||||
"""<local channel> [<network>]
|
"""<local channel> [<network>]
|
||||||
|
|
||||||
Delinks channel <local channel>. <network> must and can only be specified if you are on the host network for <local channel>, and allows you to pick which network to delink.
|
Delinks the given channel from PyLink Relay. \x02network\x02 must and can only be specified if you are on the host network for the channel given, and allows you to pick which network to delink.
|
||||||
To remove a relay entirely, use the 'destroy' command instead."""
|
To remove a relay channel entirely, use the 'destroy' command instead."""
|
||||||
try:
|
try:
|
||||||
channel = irc.toLower(args[0])
|
channel = irc.toLower(args[0])
|
||||||
except IndexError:
|
except IndexError:
|
||||||
@ -1501,12 +1570,12 @@ def delink(irc, source, args):
|
|||||||
channel, entry[0], entry[1], irc.getHostmask(source))
|
channel, entry[0], entry[1], irc.getHostmask(source))
|
||||||
else:
|
else:
|
||||||
irc.reply('Error: No such relay %r.' % channel)
|
irc.reply('Error: No such relay %r.' % channel)
|
||||||
|
delink = utils.add_cmd(delink, featured=True)
|
||||||
|
|
||||||
@utils.add_cmd
|
|
||||||
def linked(irc, source, args):
|
def linked(irc, source, args):
|
||||||
"""[<network>]
|
"""[<network>]
|
||||||
|
|
||||||
Returns a list of channels shared across the relay. If <network> is given, filters output to channels linked to the given network."""
|
Returns a list of channels shared across PyLink Relay. If \x02network\x02 is given, filters output to channels linked to the given network."""
|
||||||
|
|
||||||
# Only show remote networks that are marked as connected.
|
# Only show remote networks that are marked as connected.
|
||||||
remote_networks = [netname for netname, ircobj in world.networkobjects.copy().items()
|
remote_networks = [netname for netname, ircobj in world.networkobjects.copy().items()
|
||||||
@ -1518,7 +1587,8 @@ def linked(irc, source, args):
|
|||||||
remote_networks.sort()
|
remote_networks.sort()
|
||||||
|
|
||||||
s = 'Connected networks: \x02%s\x02 %s' % (irc.name, ' '.join(remote_networks))
|
s = 'Connected networks: \x02%s\x02 %s' % (irc.name, ' '.join(remote_networks))
|
||||||
irc.msg(source, s)
|
# Always reply in private to prevent floods.
|
||||||
|
irc.reply(s, private=True)
|
||||||
|
|
||||||
net = ''
|
net = ''
|
||||||
try:
|
try:
|
||||||
@ -1526,7 +1596,7 @@ def linked(irc, source, args):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
irc.msg(source, "Showing channels linked to %s:" % net)
|
irc.reply("Showing channels linked to %s:" % net, private=True)
|
||||||
|
|
||||||
# Sort the list of shared channels when displaying
|
# Sort the list of shared channels when displaying
|
||||||
for k, v in sorted(db.items()):
|
for k, v in sorted(db.items()):
|
||||||
@ -1551,13 +1621,16 @@ def linked(irc, source, args):
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if v['links']: # Join up and output all the linked channel names.
|
if v['links']:
|
||||||
s += ' '.join([''.join(link) for link in sorted(v['links'])])
|
# Sort, join up and output all the linked channel names. Silently drop
|
||||||
|
# entries for disconnected networks.
|
||||||
|
s += ' '.join([''.join(link) for link in sorted(v['links']) if link[0] in world.networkobjects
|
||||||
|
and world.networkobjects[link[0]].connected.is_set()])
|
||||||
|
|
||||||
else: # Unless it's empty; then, well... just say no relays yet.
|
else: # Unless it's empty; then, well... just say no relays yet.
|
||||||
s += '(no relays yet)'
|
s += '(no relays yet)'
|
||||||
|
|
||||||
irc.msg(source, s)
|
irc.reply(s, private=True)
|
||||||
|
|
||||||
if irc.isOper(source):
|
if irc.isOper(source):
|
||||||
s = ''
|
s = ''
|
||||||
@ -1576,13 +1649,14 @@ def linked(irc, source, args):
|
|||||||
s += ' on %s' % time.ctime(ts)
|
s += ' on %s' % time.ctime(ts)
|
||||||
|
|
||||||
if s: # Indent to make the list look nicer
|
if s: # Indent to make the list look nicer
|
||||||
irc.msg(source, ' Channel created%s.' % s)
|
irc.reply(' Channel created%s.' % s, private=True)
|
||||||
|
linked = utils.add_cmd(linked, featured=True)
|
||||||
|
|
||||||
@utils.add_cmd
|
@utils.add_cmd
|
||||||
def linkacl(irc, source, args):
|
def linkacl(irc, source, args):
|
||||||
"""ALLOW|DENY|LIST <channel> <remotenet>
|
"""ALLOW|DENY|LIST <channel> <remotenet>
|
||||||
|
|
||||||
Allows blocking / unblocking certain networks from linking to a relay, based on a blacklist.
|
Allows blocking / unblocking certain networks from linking to a relayed channel, based on a blacklist.
|
||||||
LINKACL LIST returns a list of blocked networks for a channel, while the ALLOW and DENY subcommands allow manipulating this blacklist."""
|
LINKACL LIST returns a list of blocked networks for a channel, while the ALLOW and DENY subcommands allow manipulating this blacklist."""
|
||||||
missingargs = "Error: Not enough arguments. Needs 2-3: subcommand (ALLOW/DENY/LIST), channel, remote network (for ALLOW/DENY)."
|
missingargs = "Error: Not enough arguments. Needs 2-3: subcommand (ALLOW/DENY/LIST), channel, remote network (for ALLOW/DENY)."
|
||||||
irc.checkAuthenticated(source)
|
irc.checkAuthenticated(source)
|
||||||
@ -1626,7 +1700,7 @@ def linkacl(irc, source, args):
|
|||||||
def showuser(irc, source, args):
|
def showuser(irc, source, args):
|
||||||
"""<user>
|
"""<user>
|
||||||
|
|
||||||
Shows relay data about user <user>. This supplements the 'showuser' command in the 'commands' plugin, which provides more general information."""
|
Shows relay data about the given user. This supplements the 'showuser' command in the 'commands' plugin, which provides more general information."""
|
||||||
try:
|
try:
|
||||||
target = args[0]
|
target = args[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
@ -1635,7 +1709,7 @@ def showuser(irc, source, args):
|
|||||||
return
|
return
|
||||||
u = irc.nickToUid(target)
|
u = irc.nickToUid(target)
|
||||||
if u:
|
if u:
|
||||||
irc.msg(source, "Showing relay information on user \x02%s\x02:" % irc.users[u].nick)
|
irc.reply("Showing relay information on user \x02%s\x02:" % irc.users[u].nick, private=True)
|
||||||
try:
|
try:
|
||||||
userpair = getOrigUser(irc, u) or (irc.name, u)
|
userpair = getOrigUser(irc, u) or (irc.name, u)
|
||||||
remoteusers = relayusers[userpair].items()
|
remoteusers = relayusers[userpair].items()
|
||||||
@ -1650,14 +1724,14 @@ def showuser(irc, source, args):
|
|||||||
remotenet, remoteuser = r
|
remotenet, remoteuser = r
|
||||||
remoteirc = world.networkobjects[remotenet]
|
remoteirc = world.networkobjects[remotenet]
|
||||||
nicks.append('%s:\x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick))
|
nicks.append('%s:\x02%s\x02' % (remotenet, remoteirc.users[remoteuser].nick))
|
||||||
irc.msg(source, "\x02Relay nicks\x02: %s" % ', '.join(nicks))
|
irc.reply("\x02Relay nicks\x02: %s" % ', '.join(nicks), private=True)
|
||||||
relaychannels = []
|
relaychannels = []
|
||||||
for ch in irc.users[u].channels:
|
for ch in irc.users[u].channels:
|
||||||
relay = getRelay((irc.name, ch))
|
relay = getRelay((irc.name, ch))
|
||||||
if relay:
|
if relay:
|
||||||
relaychannels.append(''.join(relay))
|
relaychannels.append(''.join(relay))
|
||||||
if relaychannels and (irc.isOper(source) or u == source):
|
if relaychannels and (irc.isOper(source) or u == source):
|
||||||
irc.msg(source, "\x02Relay channels\x02: %s" % ' '.join(relaychannels))
|
irc.reply("\x02Relay channels\x02: %s" % ' '.join(relaychannels), private=True)
|
||||||
|
|
||||||
@utils.add_cmd
|
@utils.add_cmd
|
||||||
def save(irc, source, args):
|
def save(irc, source, args):
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
# servprotect.py: Protects against KILL and nick collision floods
|
# servprotect.py: Protects against KILL and nick collision floods
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from expiringdict import ExpiringDict
|
from expiringdict import ExpiringDict
|
||||||
|
|
||||||
import utils
|
from pylinkirc import utils
|
||||||
from log import log
|
from pylinkirc.log import log
|
||||||
|
|
||||||
# TODO: make length and time configurable
|
# TODO: make length and time configurable
|
||||||
savecache = ExpiringDict(max_len=5, max_age_seconds=10)
|
savecache = ExpiringDict(max_len=5, max_age_seconds=10)
|
||||||
|
@ -3,13 +3,10 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
curdir = os.path.dirname(__file__)
|
from pylinkirc import utils
|
||||||
sys.path += [curdir, os.path.dirname(curdir)]
|
from pylinkirc.log import log
|
||||||
import utils
|
from pylinkirc.classes import *
|
||||||
from log import log
|
from pylinkirc.protocols.ts6 import *
|
||||||
|
|
||||||
from classes import *
|
|
||||||
from ts6 import *
|
|
||||||
|
|
||||||
class HybridProtocol(TS6Protocol):
|
class HybridProtocol(TS6Protocol):
|
||||||
def __init__(self, irc):
|
def __init__(self, irc):
|
||||||
|
@ -8,14 +8,10 @@ import os
|
|||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
# Import hacks to access utils and classes...
|
from pylinkirc import utils
|
||||||
curdir = os.path.dirname(__file__)
|
from pylinkirc.classes import *
|
||||||
sys.path += [curdir, os.path.dirname(curdir)]
|
from pylinkirc.log import log
|
||||||
import utils
|
from pylinkirc.protocols.ts6_common import *
|
||||||
from log import log
|
|
||||||
from classes import *
|
|
||||||
|
|
||||||
from ts6_common import *
|
|
||||||
|
|
||||||
class InspIRCdProtocol(TS6BaseProtocol):
|
class InspIRCdProtocol(TS6BaseProtocol):
|
||||||
def __init__(self, irc):
|
def __init__(self, irc):
|
||||||
@ -89,7 +85,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
|||||||
self.irc.channels[channel].users.add(client)
|
self.irc.channels[channel].users.add(client)
|
||||||
self.irc.users[client].channels.add(channel)
|
self.irc.users[client].channels.add(channel)
|
||||||
|
|
||||||
def sjoin(self, server, channel, users, ts=None):
|
def sjoin(self, server, channel, users, ts=None, modes=set()):
|
||||||
"""Sends an SJOIN for a group of users to a channel.
|
"""Sends an SJOIN for a group of users to a channel.
|
||||||
|
|
||||||
The sender should always be a Server ID (SID). TS is optional, and defaults
|
The sender should always be a Server ID (SID). TS is optional, and defaults
|
||||||
@ -104,20 +100,31 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
|||||||
server = server or self.irc.sid
|
server = server or self.irc.sid
|
||||||
assert users, "sjoin: No users sent?"
|
assert users, "sjoin: No users sent?"
|
||||||
log.debug('(%s) sjoin: got %r for users', self.irc.name, users)
|
log.debug('(%s) sjoin: got %r for users', self.irc.name, users)
|
||||||
|
|
||||||
if not server:
|
if not server:
|
||||||
raise LookupError('No such PyLink client exists.')
|
raise LookupError('No such PyLink client exists.')
|
||||||
|
|
||||||
|
# Strip out list-modes, they shouldn't ever be sent in FJOIN (protocol rules).
|
||||||
|
modes = modes or self.irc.channels[channel].modes
|
||||||
orig_ts = self.irc.channels[channel].ts
|
orig_ts = self.irc.channels[channel].ts
|
||||||
ts = ts or orig_ts
|
ts = ts or orig_ts
|
||||||
self.updateTS(channel, ts)
|
|
||||||
|
|
||||||
log.debug("sending SJOIN to %s%s with ts %s (that's %r)", channel, self.irc.name, ts,
|
banmodes = []
|
||||||
time.strftime("%c", time.localtime(ts)))
|
regularmodes = []
|
||||||
# Strip out list-modes, they shouldn't ever be sent in FJOIN (protocol rules).
|
for mode in modes:
|
||||||
modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']]
|
modechar = mode[0][-1]
|
||||||
|
if modechar in self.irc.cmodes['*A']:
|
||||||
|
# Track bans separately (they are sent as a normal FMODE instead of in FJOIN.
|
||||||
|
# However, don't reset bans that have already been set.
|
||||||
|
if (modechar, mode[1]) not in self.irc.channels[channel].modes:
|
||||||
|
banmodes.append(mode)
|
||||||
|
else:
|
||||||
|
regularmodes.append(mode)
|
||||||
|
|
||||||
uids = []
|
uids = []
|
||||||
changedmodes = []
|
changedmodes = set(modes)
|
||||||
namelist = []
|
namelist = []
|
||||||
|
|
||||||
# We take <users> as a list of (prefixmodes, uid) pairs.
|
# We take <users> as a list of (prefixmodes, uid) pairs.
|
||||||
for userpair in users:
|
for userpair in users:
|
||||||
assert len(userpair) == 2, "Incorrect format of userpair: %r" % userpair
|
assert len(userpair) == 2, "Incorrect format of userpair: %r" % userpair
|
||||||
@ -125,20 +132,26 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
|||||||
namelist.append(','.join(userpair))
|
namelist.append(','.join(userpair))
|
||||||
uids.append(user)
|
uids.append(user)
|
||||||
for m in prefixes:
|
for m in prefixes:
|
||||||
changedmodes.append(('+%s' % m, user))
|
changedmodes.add(('+%s' % m, user))
|
||||||
try:
|
try:
|
||||||
self.irc.users[user].channels.add(channel)
|
self.irc.users[user].channels.add(channel)
|
||||||
except KeyError: # Not initialized yet?
|
except KeyError: # Not initialized yet?
|
||||||
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user)
|
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user)
|
||||||
if ts <= orig_ts:
|
|
||||||
# Only save our prefix modes in the channel state if our TS is lower than or equal to theirs.
|
|
||||||
self.irc.applyModes(channel, changedmodes)
|
|
||||||
namelist = ' '.join(namelist)
|
namelist = ' '.join(namelist)
|
||||||
self._send(server, "FJOIN {channel} {ts} {modes} :{users}".format(
|
self._send(server, "FJOIN {channel} {ts} {modes} :{users}".format(
|
||||||
ts=ts, users=namelist, channel=channel,
|
ts=ts, users=namelist, channel=channel,
|
||||||
modes=self.irc.joinModes(modes)))
|
modes=self.irc.joinModes(modes)))
|
||||||
self.irc.channels[channel].users.update(uids)
|
self.irc.channels[channel].users.update(uids)
|
||||||
|
|
||||||
|
if banmodes:
|
||||||
|
# Burst ban modes if there are any.
|
||||||
|
# <- :1ML FMODE #test 1461201525 +bb *!*@bad.user *!*@rly.bad.user
|
||||||
|
self._send(server, "FMODE {channel} {ts} {modes} ".format(
|
||||||
|
ts=ts, channel=channel, modes=self.irc.joinModes(banmodes)))
|
||||||
|
|
||||||
|
self.updateTS(channel, ts, changedmodes)
|
||||||
|
|
||||||
def _operUp(self, target, opertype=None):
|
def _operUp(self, target, opertype=None):
|
||||||
"""Opers a client up (internal function specific to InspIRCd).
|
"""Opers a client up (internal function specific to InspIRCd).
|
||||||
|
|
||||||
@ -280,10 +293,17 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
|||||||
self._send(source, 'PING %s %s' % (source, target))
|
self._send(source, 'PING %s %s' % (source, target))
|
||||||
|
|
||||||
def numeric(self, source, numeric, target, text):
|
def numeric(self, source, numeric, target, text):
|
||||||
raise NotImplementedError("Numeric sending is not yet implemented by this "
|
"""Sends raw numerics from a server to a remote client."""
|
||||||
"protocol module. WHOIS requests are handled "
|
# InspIRCd 2.0 syntax (undocumented):
|
||||||
"locally by InspIRCd servers, so there is no "
|
# Essentially what this does is push the raw numeric text after the first ":" towards the
|
||||||
"need for PyLink to send numerics directly yet.")
|
# given user.
|
||||||
|
# <- :70M PUSH 0ALAAAAAA ::midnight.vpn 422 PyLink-devel :Message of the day file is missing.
|
||||||
|
|
||||||
|
# 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(self.irc.sid, 'PUSH %s ::%s %s %s %s' % (target, source, numeric, target, text))
|
||||||
|
|
||||||
def away(self, source, text):
|
def away(self, source, text):
|
||||||
"""Sends an AWAY message from a PyLink client. <text> can be an empty string
|
"""Sends an AWAY message from a PyLink client. <text> can be an empty string
|
||||||
@ -487,14 +507,14 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
|||||||
# InspIRCd sends each channel's users in the form of 'modeprefix(es),UID'
|
# InspIRCd sends each channel's users in the form of 'modeprefix(es),UID'
|
||||||
userlist = args[-1].split()
|
userlist = args[-1].split()
|
||||||
|
|
||||||
their_ts = int(args[1])
|
|
||||||
our_ts = self.irc.channels[channel].ts
|
|
||||||
self.updateTS(channel, their_ts)
|
|
||||||
|
|
||||||
modestring = args[2:-1] or args[2]
|
modestring = args[2:-1] or args[2]
|
||||||
parsedmodes = self.irc.parseModes(channel, modestring)
|
parsedmodes = self.irc.parseModes(channel, modestring)
|
||||||
self.irc.applyModes(channel, parsedmodes)
|
self.irc.applyModes(channel, parsedmodes)
|
||||||
namelist = []
|
namelist = []
|
||||||
|
|
||||||
|
# Keep track of other modes that are added due to prefix modes being joined too.
|
||||||
|
changedmodes = set(parsedmodes)
|
||||||
|
|
||||||
for user in userlist:
|
for user in userlist:
|
||||||
modeprefix, user = user.split(',', 1)
|
modeprefix, user = user.split(',', 1)
|
||||||
|
|
||||||
@ -506,9 +526,17 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
|||||||
|
|
||||||
namelist.append(user)
|
namelist.append(user)
|
||||||
self.irc.users[user].channels.add(channel)
|
self.irc.users[user].channels.add(channel)
|
||||||
if their_ts <= our_ts:
|
|
||||||
self.irc.applyModes(channel, [('+%s' % mode, user) for mode in modeprefix])
|
# Only save mode changes if the remote has lower TS than us.
|
||||||
|
changedmodes |= {('+%s' % mode, user) for mode in modeprefix}
|
||||||
|
|
||||||
self.irc.channels[channel].users.add(user)
|
self.irc.channels[channel].users.add(user)
|
||||||
|
|
||||||
|
# Statekeeping with timestamps
|
||||||
|
their_ts = int(args[1])
|
||||||
|
our_ts = self.irc.channels[channel].ts
|
||||||
|
self.updateTS(channel, their_ts, changedmodes)
|
||||||
|
|
||||||
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts}
|
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts}
|
||||||
|
|
||||||
def handle_uid(self, numeric, command, args):
|
def handle_uid(self, numeric, command, args):
|
||||||
@ -516,10 +544,16 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
|||||||
# :70M UID 70MAAAAAB 1429934638 GL 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP gl 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]
|
uid, ts, nick, realhost, host, ident, ip = args[0:7]
|
||||||
realname = args[-1]
|
realname = args[-1]
|
||||||
self.irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip)
|
self.irc.users[uid] = userobj = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip)
|
||||||
|
|
||||||
parsedmodes = self.irc.parseModes(uid, [args[8], args[9]])
|
parsedmodes = self.irc.parseModes(uid, [args[8], args[9]])
|
||||||
log.debug('Applying modes %s for %s', parsedmodes, uid)
|
|
||||||
self.irc.applyModes(uid, parsedmodes)
|
self.irc.applyModes(uid, parsedmodes)
|
||||||
|
|
||||||
|
if (self.irc.umodes.get('servprotect'), None) in userobj.modes:
|
||||||
|
# Services are usually given a "Network Service" WHOIS, so
|
||||||
|
# set that as the opertype.
|
||||||
|
self.irc.callHooks([uid, 'CLIENT_OPERED', {'text': 'Network Service'}])
|
||||||
|
|
||||||
self.irc.servers[numeric].users.add(uid)
|
self.irc.servers[numeric].users.add(uid)
|
||||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
|
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
|
||||||
|
|
||||||
@ -579,6 +613,13 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
|||||||
# -> :1MLAAAAIG IDLE 70MAAAAAA 1433036797 319
|
# -> :1MLAAAAIG IDLE 70MAAAAAA 1433036797 319
|
||||||
sourceuser = numeric
|
sourceuser = numeric
|
||||||
targetuser = args[0]
|
targetuser = args[0]
|
||||||
|
|
||||||
|
if self.irc.serverdata.get("use_experimental_whois"):
|
||||||
|
# EXPERIMENTAL HACK: make PyLink handle all WHOIS requests if configured to do so.
|
||||||
|
# This works by silently ignoring the idle time request, and sending our WHOIS data as
|
||||||
|
# raw numerics instead.
|
||||||
|
return {'target': args[0], 'parse_as': 'WHOIS'}
|
||||||
|
else:
|
||||||
self._send(targetuser, 'IDLE %s %s 0' % (sourceuser, self.irc.users[targetuser].ts))
|
self._send(targetuser, 'IDLE %s %s 0' % (sourceuser, self.irc.users[targetuser].ts))
|
||||||
|
|
||||||
def handle_ftopic(self, numeric, command, args):
|
def handle_ftopic(self, numeric, command, args):
|
||||||
@ -603,31 +644,14 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
|||||||
# We don't actually need to process this; just send the hook so plugins can use it
|
# We don't actually need to process this; just send the hook so plugins can use it
|
||||||
return {'target': target, 'channel': channel}
|
return {'target': target, 'channel': channel}
|
||||||
|
|
||||||
def handle_encap(self, numeric, command, args):
|
def handle_knock(self, numeric, command, args):
|
||||||
"""Handles incoming encapsulated commands (ENCAP). Hook arguments
|
"""Handles channel KNOCKs."""
|
||||||
returned by this should have a parse_as field, that sets the correct
|
# <- :70MAAAAAA ENCAP * KNOCK #blah :abcdefg
|
||||||
hook name for the message.
|
channel = self.irc.toLower(args[0])
|
||||||
|
text = args[1]
|
||||||
|
return {'channel': channel, 'text': text}
|
||||||
|
|
||||||
For InspIRCd, the only ENCAP command we handle right now is KNOCK."""
|
def handle_opertype(self, target, command, args):
|
||||||
# <- :70MAAAAAA ENCAP * KNOCK #blah :agsdfas
|
|
||||||
# From charybdis TS6 docs: https://github.com/grawity/irc-docs/blob/03ba884a54f1cef2193cd62b6a86803d89c1ac41/server/ts6.txt
|
|
||||||
|
|
||||||
# ENCAP
|
|
||||||
# source: any
|
|
||||||
# parameters: target server mask, subcommand, opt. parameters...
|
|
||||||
|
|
||||||
# Sends a command to matching servers. Propagation is independent of
|
|
||||||
# understanding the subcommand.
|
|
||||||
|
|
||||||
targetmask = args[0]
|
|
||||||
real_command = args[1]
|
|
||||||
if targetmask == '*' and real_command == 'KNOCK':
|
|
||||||
channel = self.irc.toLower(args[2])
|
|
||||||
text = args[3]
|
|
||||||
return {'parse_as': real_command, 'channel': channel,
|
|
||||||
'text': text}
|
|
||||||
|
|
||||||
def handle_opertype(self, numeric, command, args):
|
|
||||||
"""Handles incoming OPERTYPE, which is used to denote an oper up.
|
"""Handles incoming OPERTYPE, which is used to denote an oper up.
|
||||||
|
|
||||||
This calls the internal hook CLIENT_OPERED, sets the internal
|
This calls the internal hook CLIENT_OPERED, sets the internal
|
||||||
@ -635,13 +659,17 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
|||||||
# This is used by InspIRCd to denote an oper up; there is no MODE
|
# This is used by InspIRCd to denote an oper up; there is no MODE
|
||||||
# command sent for it.
|
# command sent for it.
|
||||||
# <- :70MAAAAAB OPERTYPE Network_Owner
|
# <- :70MAAAAAB OPERTYPE Network_Owner
|
||||||
|
# Replace escaped _ in opertypes with spaces for InspIRCd 2.0.
|
||||||
|
opertype = args[0].replace("_", " ")
|
||||||
|
|
||||||
|
# Set umode +o on the target.
|
||||||
omode = [('+o', None)]
|
omode = [('+o', None)]
|
||||||
self.irc.users[numeric].opertype = opertype = args[0].replace("_", " ")
|
self.irc.applyModes(target, omode)
|
||||||
self.irc.applyModes(numeric, omode)
|
|
||||||
# OPERTYPE is essentially umode +o and metadata in one command;
|
# Call the CLIENT_OPERED hook that protocols use. The MODE hook
|
||||||
# we'll call that too.
|
# payload is returned below.
|
||||||
self.irc.callHooks([numeric, 'CLIENT_OPERED', {'text': opertype}])
|
self.irc.callHooks([target, 'CLIENT_OPERED', {'text': opertype}])
|
||||||
return {'target': numeric, 'modes': omode}
|
return {'target': target, 'modes': omode}
|
||||||
|
|
||||||
def handle_fident(self, numeric, command, args):
|
def handle_fident(self, numeric, command, args):
|
||||||
"""Handles FIDENT, used for denoting ident changes."""
|
"""Handles FIDENT, used for denoting ident changes."""
|
||||||
@ -712,7 +740,7 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
|||||||
"""
|
"""
|
||||||
uid = args[0]
|
uid = args[0]
|
||||||
|
|
||||||
if args[1] == 'accountname':
|
if args[1] == 'accountname' and uid in self.irc.users:
|
||||||
# <- :00A METADATA 1MLAAAJET accountname :
|
# <- :00A METADATA 1MLAAAJET accountname :
|
||||||
# <- :00A METADATA 1MLAAAJET accountname :tester
|
# <- :00A METADATA 1MLAAAJET accountname :tester
|
||||||
# Sets the services login name of the client.
|
# Sets the services login name of the client.
|
||||||
@ -725,4 +753,17 @@ class InspIRCdProtocol(TS6BaseProtocol):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def handle_kill(self, source, command, args):
|
||||||
|
"""Handles incoming KILLs."""
|
||||||
|
killed = args[0]
|
||||||
|
# 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.irc.users.get(killed)
|
||||||
|
if data:
|
||||||
|
self.removeClient(killed)
|
||||||
|
return {'target': killed, 'text': args[1], 'userdata': data}
|
||||||
|
|
||||||
Class = InspIRCdProtocol
|
Class = InspIRCdProtocol
|
||||||
|
79
protocols/ircs2s_common.py
Normal file
79
protocols/ircs2s_common.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
ircs2s_common.py: Common base protocol class with functions shared by TS6 and P10-based protocols.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pylinkirc.classes import Protocol
|
||||||
|
from pylinkirc.log import log
|
||||||
|
|
||||||
|
class IRCS2SProtocol(Protocol):
|
||||||
|
|
||||||
|
def handle_kill(self, source, command, args):
|
||||||
|
"""Handles incoming KILLs."""
|
||||||
|
killed = args[0]
|
||||||
|
# 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.irc.users.get(killed)
|
||||||
|
if data:
|
||||||
|
self.removeClient(killed)
|
||||||
|
|
||||||
|
# TS6-style kills look something like this:
|
||||||
|
# <- :GL KILL 38QAAAAAA :hidden-1C620195!GL (test)
|
||||||
|
# What we actually want is to format a pretty kill message, in the form
|
||||||
|
# "Killed (killername (reason))".
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get the nick or server name of the caller.
|
||||||
|
killer = self.irc.getFriendlyName(source)
|
||||||
|
except KeyError:
|
||||||
|
# Killer was... neither? We must have aliens or something. Fallback
|
||||||
|
# to the given "UID".
|
||||||
|
killer = source
|
||||||
|
|
||||||
|
# Get the reason, which is enclosed in brackets.
|
||||||
|
reason = ' '.join(args[1].split(" ")[1:])
|
||||||
|
|
||||||
|
killmsg = "Killed (%s %s)" % (killer, reason)
|
||||||
|
|
||||||
|
return {'target': killed, 'text': killmsg, 'userdata': data}
|
||||||
|
|
||||||
|
|
||||||
|
def handle_squit(self, numeric, command, args):
|
||||||
|
"""Handles incoming SQUITs."""
|
||||||
|
# <- ABAAE SQ nefarious.midnight.vpn 0 :test
|
||||||
|
|
||||||
|
split_server = self._getSid(args[0])
|
||||||
|
|
||||||
|
affected_users = []
|
||||||
|
log.debug('(%s) Splitting server %s (reason: %s)', self.irc.name, split_server, args[-1])
|
||||||
|
|
||||||
|
if split_server not in self.irc.servers:
|
||||||
|
log.warning("(%s) Tried to split a server (%s) that didn't exist!", self.irc.name, split_server)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Prevent RuntimeError: dictionary changed size during iteration
|
||||||
|
old_servers = self.irc.servers.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.handle_squit(sid, 'SQUIT', [sid, "0",
|
||||||
|
"PyLink: Automatically splitting leaf servers of %s" % sid])
|
||||||
|
affected_users += args['users']
|
||||||
|
|
||||||
|
for user in self.irc.servers[split_server].users.copy():
|
||||||
|
affected_users.append(user)
|
||||||
|
log.debug('Removing client %s (%s)', user, self.irc.users[user].nick)
|
||||||
|
self.removeClient(user)
|
||||||
|
|
||||||
|
sname = self.irc.servers[split_server].name
|
||||||
|
uplink = self.irc.servers[split_server].uplink
|
||||||
|
del self.irc.servers[split_server]
|
||||||
|
log.debug('(%s) Netsplit affected users: %s', self.irc.name, affected_users)
|
||||||
|
|
||||||
|
return {'target': split_server, 'users': affected_users, 'name': sname,
|
||||||
|
'uplink': uplink}
|
@ -2,19 +2,14 @@
|
|||||||
nefarious.py: Nefarious IRCu protocol module for PyLink.
|
nefarious.py: Nefarious IRCu protocol module for PyLink.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import base64
|
import base64
|
||||||
import struct
|
import struct
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
|
|
||||||
# Import hacks to access utils and classes...
|
from pylinkirc import utils, structures
|
||||||
curdir = os.path.dirname(__file__)
|
from pylinkirc.classes import *
|
||||||
sys.path += [curdir, os.path.dirname(curdir)]
|
from pylinkirc.log import log
|
||||||
|
from pylinkirc.protocols.ircs2s_common import *
|
||||||
import utils
|
|
||||||
from log import log
|
|
||||||
from classes import *
|
|
||||||
|
|
||||||
class P10UIDGenerator(utils.IncrementalUIDGenerator):
|
class P10UIDGenerator(utils.IncrementalUIDGenerator):
|
||||||
"""Implements an incremental P10 UID Generator."""
|
"""Implements an incremental P10 UID Generator."""
|
||||||
@ -63,7 +58,7 @@ class P10SIDGenerator():
|
|||||||
self.currentnum += 1
|
self.currentnum += 1
|
||||||
return sid
|
return sid
|
||||||
|
|
||||||
class P10Protocol(Protocol):
|
class P10Protocol(IRCS2SProtocol):
|
||||||
|
|
||||||
def __init__(self, irc):
|
def __init__(self, irc):
|
||||||
super().__init__(irc)
|
super().__init__(irc)
|
||||||
@ -79,6 +74,22 @@ class P10Protocol(Protocol):
|
|||||||
def _send(self, source, text):
|
def _send(self, source, text):
|
||||||
self.irc.send("%s %s" % (source, text))
|
self.irc.send("%s %s" % (source, text))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def access_sort(key):
|
||||||
|
"""
|
||||||
|
Sorts (prefixmode, UID) keys based on the prefix modes given.
|
||||||
|
"""
|
||||||
|
prefixes, user = key
|
||||||
|
# Add the prefixes given for each userpair, giving each one a set value. This ensures
|
||||||
|
# that 'ohv' > 'oh' > 'ov' > 'o' > 'hv' > 'h' > 'v' > ''
|
||||||
|
accesses = {'o': 100, 'h': 10, 'v': 1}
|
||||||
|
|
||||||
|
num = 0
|
||||||
|
for prefix in prefixes:
|
||||||
|
num += accesses.get(prefix, 0)
|
||||||
|
|
||||||
|
return num
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def decode_p10_ip(ip):
|
def decode_p10_ip(ip):
|
||||||
"""Decodes a P10 IP."""
|
"""Decodes a P10 IP."""
|
||||||
@ -454,7 +465,7 @@ class P10Protocol(Protocol):
|
|||||||
else:
|
else:
|
||||||
raise LookupError("No such PyLink client exists.")
|
raise LookupError("No such PyLink client exists.")
|
||||||
|
|
||||||
def sjoin(self, server, channel, users, ts=None):
|
def sjoin(self, server, channel, users, ts=None, modes=set()):
|
||||||
"""Sends an SJOIN for a group of users to a channel.
|
"""Sends an SJOIN for a group of users to a channel.
|
||||||
|
|
||||||
The sender should always be a Server ID (SID). TS is optional, and defaults
|
The sender should always be a Server ID (SID). TS is optional, and defaults
|
||||||
@ -474,32 +485,38 @@ class P10Protocol(Protocol):
|
|||||||
if not server:
|
if not server:
|
||||||
raise LookupError('No such PyLink client exists.')
|
raise LookupError('No such PyLink client exists.')
|
||||||
|
|
||||||
|
# Only send non-list modes in the modes argument BURST. Bans and exempts are formatted differently:
|
||||||
|
# <- AB B #test 1460742014 +tnl 10 ABAAB,ABAAA:o :%*!*@other.bad.host *!*@bad.host
|
||||||
|
# <- AB B #test2 1460743539 +l 10 ABAAA:vo :%*!*@bad.host
|
||||||
|
# <- AB B #test 1460747615 ABAAA:o :% ~ *!*@test.host
|
||||||
|
modes = modes or self.irc.channels[channel].modes
|
||||||
orig_ts = self.irc.channels[channel].ts
|
orig_ts = self.irc.channels[channel].ts
|
||||||
ts = ts or orig_ts
|
ts = ts or orig_ts
|
||||||
self.updateTS(channel, ts)
|
|
||||||
|
|
||||||
# Only send non-list modes in BURST. TODO: burst bans and banexempts too
|
bans = []
|
||||||
modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']]
|
exempts = []
|
||||||
|
regularmodes = []
|
||||||
|
for mode in modes:
|
||||||
|
modechar = mode[0][-1]
|
||||||
|
# Store bans and exempts in separate lists for processing, but don't reset bans that have already been set.
|
||||||
|
if modechar in self.irc.cmodes['*A']:
|
||||||
|
if (modechar, mode[1]) not in self.irc.channels[channel].modes:
|
||||||
|
if modechar == 'b':
|
||||||
|
bans.append(mode[1])
|
||||||
|
elif modechar == 'e':
|
||||||
|
exempts.append(mode[1])
|
||||||
|
else:
|
||||||
|
regularmodes.append(mode)
|
||||||
|
|
||||||
changedmodes = []
|
log.debug('(%s) sjoin: bans: %s, exempts: %s, other modes: %s', self.irc.name, bans, exempts, regularmodes)
|
||||||
|
|
||||||
|
changedmodes = set(modes)
|
||||||
changedusers = []
|
changedusers = []
|
||||||
namelist = []
|
namelist = []
|
||||||
|
|
||||||
# This is annoying because we have to sort our users by access before sending...
|
# This is annoying because we have to sort our users by access before sending...
|
||||||
# Joins should look like: A0AAB,A0AAC,ABAAA:v,ABAAB:o,ABAAD,ACAAA:ov
|
# Joins should look like: A0AAB,A0AAC,ABAAA:v,ABAAB:o,ABAAD,ACAAA:ov
|
||||||
# XXX: there HAS to be a better way of doing this
|
users = sorted(users, key=self.access_sort)
|
||||||
def access_sort(key):
|
|
||||||
prefixes, user = key
|
|
||||||
# This is some hocus pocus. Add the prefixes given for each userpair,
|
|
||||||
# giving each one a set value. This ensures that 'ohv' > 'oh' > 'ov' > 'o' > 'hv' > 'h' > 'v' > ''
|
|
||||||
accesses = {'o': 100, 'h': 10, 'v': 1}
|
|
||||||
|
|
||||||
num = 0
|
|
||||||
for prefix in prefixes:
|
|
||||||
num += accesses.get(prefix, 0)
|
|
||||||
|
|
||||||
return num
|
|
||||||
users = sorted(users, key=access_sort)
|
|
||||||
|
|
||||||
last_prefixes = ''
|
last_prefixes = ''
|
||||||
for userpair in users:
|
for userpair in users:
|
||||||
@ -520,25 +537,35 @@ class P10Protocol(Protocol):
|
|||||||
last_prefixes = prefixes
|
last_prefixes = prefixes
|
||||||
if prefixes:
|
if prefixes:
|
||||||
for prefix in prefixes:
|
for prefix in prefixes:
|
||||||
changedmodes.append(('+%s' % prefix, user))
|
changedmodes.add(('+%s' % prefix, user))
|
||||||
|
|
||||||
self.irc.users[user].channels.add(channel)
|
self.irc.users[user].channels.add(channel)
|
||||||
|
|
||||||
namelist = ','.join(namelist)
|
namelist = ','.join(namelist)
|
||||||
log.debug('(%s) sjoin: got %r for namelist', self.irc.name, namelist)
|
log.debug('(%s) sjoin: got %r for namelist', self.irc.name, namelist)
|
||||||
|
|
||||||
|
# Format bans as the last argument if there are any.
|
||||||
|
banstring = ''
|
||||||
|
if bans or exempts:
|
||||||
|
banstring += ' :%' # Ban string starts with a % if there is anything
|
||||||
|
if bans:
|
||||||
|
banstring += ' '.join(bans) # Join all bans, separated by a space
|
||||||
|
if exempts:
|
||||||
|
# Exempts are separated from the ban list by a single argument "~".
|
||||||
|
banstring += ' ~ '
|
||||||
|
banstring += ' '.join(exempts)
|
||||||
|
|
||||||
if modes: # Only send modes if there are any.
|
if modes: # Only send modes if there are any.
|
||||||
self._send(server, "B {channel} {ts} {modes} :{users}".format(
|
self._send(server, "B {channel} {ts} {modes} {users}{banstring}".format(
|
||||||
ts=ts, users=namelist, channel=channel,
|
ts=ts, users=namelist, channel=channel,
|
||||||
modes=self.irc.joinModes(modes)))
|
modes=self.irc.joinModes(regularmodes), banstring=banstring))
|
||||||
else:
|
else:
|
||||||
self._send(server, "B {channel} {ts} :{users}".format(
|
self._send(server, "B {channel} {ts} {users}{banstring}".format(
|
||||||
ts=ts, users=namelist, channel=channel))
|
ts=ts, users=namelist, channel=channel, banstring=banstring))
|
||||||
|
|
||||||
self.irc.channels[channel].users.update(changedusers)
|
self.irc.channels[channel].users.update(changedusers)
|
||||||
|
|
||||||
if ts <= orig_ts:
|
self.updateTS(channel, ts, changedmodes)
|
||||||
# Only save our prefix modes in the channel state if our TS is lower than or equal to theirs.
|
|
||||||
self.irc.applyModes(channel, changedmodes)
|
|
||||||
|
|
||||||
def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0):
|
def spawnServer(self, name, sid=None, uplink=None, desc=None, endburst_delay=0):
|
||||||
"""
|
"""
|
||||||
@ -945,19 +972,15 @@ class P10Protocol(Protocol):
|
|||||||
|
|
||||||
channel = self.irc.toLower(args[0])
|
channel = self.irc.toLower(args[0])
|
||||||
userlist = args[-1].split()
|
userlist = args[-1].split()
|
||||||
their_ts = int(args[1])
|
|
||||||
our_ts = self.irc.channels[channel].ts
|
|
||||||
|
|
||||||
self.updateTS(channel, their_ts)
|
|
||||||
|
|
||||||
bans = []
|
bans = []
|
||||||
if args[-1].startswith('%'):
|
if args[-1].startswith('%'):
|
||||||
# Ban lists start with a %. However, if one argument is "~",
|
# Ban lists start with a %. However, if one argument is "~",
|
||||||
# Parse everything after it as an exempt (+e).
|
# parse everything after it as an ban exempt (+e).
|
||||||
exempts = False
|
exempts = False
|
||||||
for host in args[-1][1:].split(' '):
|
for host in args[-1][1:].split(' '):
|
||||||
if not host:
|
if not host:
|
||||||
# Space between % and ~ ignore.
|
# Space between % and ~; ignore.
|
||||||
continue
|
continue
|
||||||
elif host == '~':
|
elif host == '~':
|
||||||
exempts = True
|
exempts = True
|
||||||
@ -979,9 +1002,11 @@ class P10Protocol(Protocol):
|
|||||||
else:
|
else:
|
||||||
parsedmodes = []
|
parsedmodes = []
|
||||||
|
|
||||||
# Add the ban list to the list of modes to process.
|
# This list is used to keep track of prefix modes being added to the mode list.
|
||||||
parsedmodes.extend(bans)
|
changedmodes = set(parsedmodes)
|
||||||
|
|
||||||
|
# Also add the the ban list to the list of modes to process internally.
|
||||||
|
parsedmodes.extend(bans)
|
||||||
if parsedmodes:
|
if parsedmodes:
|
||||||
self.irc.applyModes(channel, parsedmodes)
|
self.irc.applyModes(channel, parsedmodes)
|
||||||
|
|
||||||
@ -991,12 +1016,13 @@ class P10Protocol(Protocol):
|
|||||||
prefixes = ''
|
prefixes = ''
|
||||||
|
|
||||||
userlist = args[-1].split(',')
|
userlist = args[-1].split(',')
|
||||||
if args[-1] != args[1]: # Make sure the userlist is the right argument (not the TS).
|
if args[-1] != args[1]: # Make sure the user list is the right argument (not the TS).
|
||||||
for userpair in userlist:
|
for userpair in userlist:
|
||||||
# This is given in the form UID1,UID2:prefixes. However, when one userpair is given
|
# This is given in the form UID1,UID2:prefixes. However, when one userpair is given
|
||||||
# with a certain prefix, it implicitly applies to all other following UIDs, until
|
# with a certain prefix, it implicitly applies to all other following UIDs, until
|
||||||
# another userpair is given with a prefix. For example: UID1,UID3:o,UID4,UID5 would
|
# another userpair is given with a list of prefix modes. For example,
|
||||||
# assume that UID1 has no prefixes, but UID3-5 all have op when joining.
|
# "UID1,UID3:o,UID4,UID5" would assume that UID1 has no prefixes, but that UIDs 3-5
|
||||||
|
# all have op.
|
||||||
try:
|
try:
|
||||||
user, prefixes = userpair.split(':')
|
user, prefixes = userpair.split(':')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -1013,11 +1039,16 @@ class P10Protocol(Protocol):
|
|||||||
|
|
||||||
self.irc.users[user].channels.add(channel)
|
self.irc.users[user].channels.add(channel)
|
||||||
|
|
||||||
if their_ts <= our_ts:
|
# Only save mode changes if the remote has lower TS than us.
|
||||||
self.irc.applyModes(channel, [('+%s' % mode, user) for mode in prefixes])
|
changedmodes |= {('+%s' % mode, user) for mode in prefixes}
|
||||||
|
|
||||||
self.irc.channels[channel].users.add(user)
|
self.irc.channels[channel].users.add(user)
|
||||||
|
|
||||||
|
# Statekeeping with timestamps
|
||||||
|
their_ts = int(args[1])
|
||||||
|
our_ts = self.irc.channels[channel].ts
|
||||||
|
self.updateTS(channel, their_ts, changedmodes)
|
||||||
|
|
||||||
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts}
|
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts}
|
||||||
|
|
||||||
def handle_join(self, source, command, args):
|
def handle_join(self, source, command, args):
|
||||||
@ -1040,6 +1071,7 @@ class P10Protocol(Protocol):
|
|||||||
for channel in oldchans:
|
for channel in oldchans:
|
||||||
self.irc.channels[channel].users.discard(source)
|
self.irc.channels[channel].users.discard(source)
|
||||||
self.irc.users[source].channels.discard(channel)
|
self.irc.users[source].channels.discard(channel)
|
||||||
|
|
||||||
return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'}
|
return {'channels': oldchans, 'text': 'Left all channels.', 'parse_as': 'PART'}
|
||||||
else:
|
else:
|
||||||
channel = self.irc.toLower(args[0])
|
channel = self.irc.toLower(args[0])
|
||||||
@ -1140,54 +1172,6 @@ class P10Protocol(Protocol):
|
|||||||
self.removeClient(numeric)
|
self.removeClient(numeric)
|
||||||
return {'text': args[0]}
|
return {'text': args[0]}
|
||||||
|
|
||||||
def handle_kill(self, numeric, command, args):
|
|
||||||
"""Handles incoming KILLs."""
|
|
||||||
# <- ABAAA D AyAAA :nefarious.midnight.vpn!GL (test)
|
|
||||||
killed = args[0]
|
|
||||||
|
|
||||||
# Back up the target user data before removing it, so we can send it via a hook.
|
|
||||||
data = self.irc.users.get(killed)
|
|
||||||
|
|
||||||
if data:
|
|
||||||
self.removeClient(killed)
|
|
||||||
return {'target': killed, 'text': args[1], 'userdata': data}
|
|
||||||
|
|
||||||
def handle_squit(self, numeric, command, args):
|
|
||||||
"""Handles incoming SQUITs."""
|
|
||||||
# <- ABAAE SQ nefarious.midnight.vpn 0 :test
|
|
||||||
|
|
||||||
split_server = self._getSid(args[0])
|
|
||||||
|
|
||||||
affected_users = []
|
|
||||||
log.debug('(%s) Splitting server %s (reason: %s)', self.irc.name, split_server, args[-1])
|
|
||||||
|
|
||||||
if split_server not in self.irc.servers:
|
|
||||||
log.warning("(%s) Tried to split a server (%s) that didn't exist!", self.irc.name, split_server)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Prevent RuntimeError: dictionary changed size during iteration
|
|
||||||
old_servers = self.irc.servers.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.handle_squit(sid, 'SQUIT', [sid, "0",
|
|
||||||
"PyLink: Automatically splitting leaf servers of %s" % sid])
|
|
||||||
affected_users += args['users']
|
|
||||||
|
|
||||||
for user in self.irc.servers[split_server].users.copy():
|
|
||||||
affected_users.append(user)
|
|
||||||
log.debug('Removing client %s (%s)', user, self.irc.users[user].nick)
|
|
||||||
self.removeClient(user)
|
|
||||||
|
|
||||||
sname = self.irc.servers[split_server].name
|
|
||||||
del self.irc.servers[split_server]
|
|
||||||
log.debug('(%s) Netsplit affected users: %s', self.irc.name, affected_users)
|
|
||||||
|
|
||||||
return {'target': split_server, 'users': affected_users, 'name': sname}
|
|
||||||
|
|
||||||
def handle_topic(self, source, command, args):
|
def handle_topic(self, source, command, args):
|
||||||
"""Handles TOPIC changes."""
|
"""Handles TOPIC changes."""
|
||||||
# <- ABAAA T #test GL!~gl@nefarious.midnight.vpn 1460852591 1460855795 :blah
|
# <- ABAAA T #test GL!~gl@nefarious.midnight.vpn 1460852591 1460855795 :blah
|
||||||
|
119
protocols/ts6.py
119
protocols/ts6.py
@ -7,14 +7,10 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Import hacks to access utils and classes...
|
from pylinkirc import utils
|
||||||
curdir = os.path.dirname(__file__)
|
from pylinkirc.classes import *
|
||||||
sys.path += [curdir, os.path.dirname(curdir)]
|
from pylinkirc.log import log
|
||||||
import utils
|
from pylinkirc.protocols.ts6_common import *
|
||||||
from log import log
|
|
||||||
|
|
||||||
from classes import *
|
|
||||||
from ts6_common import *
|
|
||||||
|
|
||||||
class TS6Protocol(TS6BaseProtocol):
|
class TS6Protocol(TS6BaseProtocol):
|
||||||
def __init__(self, irc):
|
def __init__(self, irc):
|
||||||
@ -77,7 +73,7 @@ class TS6Protocol(TS6BaseProtocol):
|
|||||||
self.irc.channels[channel].users.add(client)
|
self.irc.channels[channel].users.add(client)
|
||||||
self.irc.users[client].channels.add(channel)
|
self.irc.users[client].channels.add(channel)
|
||||||
|
|
||||||
def sjoin(self, server, channel, users, ts=None):
|
def sjoin(self, server, channel, users, ts=None, modes=set()):
|
||||||
"""Sends an SJOIN for a group of users to a channel.
|
"""Sends an SJOIN for a group of users to a channel.
|
||||||
|
|
||||||
The sender should always be a Server ID (SID). TS is optional, and defaults
|
The sender should always be a Server ID (SID). TS is optional, and defaults
|
||||||
@ -104,19 +100,30 @@ class TS6Protocol(TS6BaseProtocol):
|
|||||||
if not server:
|
if not server:
|
||||||
raise LookupError('No such PyLink client exists.')
|
raise LookupError('No such PyLink client exists.')
|
||||||
|
|
||||||
|
modes = set(modes or self.irc.channels[channel].modes)
|
||||||
orig_ts = self.irc.channels[channel].ts
|
orig_ts = self.irc.channels[channel].ts
|
||||||
ts = ts or orig_ts
|
ts = ts or orig_ts
|
||||||
self.updateTS(channel, ts)
|
|
||||||
|
|
||||||
log.debug("(%s) sending SJOIN to %s with ts %s (that's %r)", self.irc.name, channel, ts,
|
# Get all the ban modes in a separate list. These are bursted using a separate BMASK
|
||||||
time.strftime("%c", time.localtime(ts)))
|
# command.
|
||||||
modes = [m for m in self.irc.channels[channel].modes if m[0] not in self.irc.cmodes['*A']]
|
banmodes = {k: set() for k in self.irc.cmodes['*A']}
|
||||||
changedmodes = []
|
regularmodes = []
|
||||||
while users[:10]:
|
log.debug('(%s) Unfiltered SJOIN modes: %s', self.irc.name, modes)
|
||||||
|
for mode in modes:
|
||||||
|
modechar = mode[0][-1]
|
||||||
|
if modechar in self.irc.cmodes['*A']:
|
||||||
|
# Mode character is one of 'beIq'
|
||||||
|
banmodes[modechar].add(mode[1])
|
||||||
|
else:
|
||||||
|
regularmodes.append(mode)
|
||||||
|
log.debug('(%s) Filtered SJOIN modes to be regular modes: %s, banmodes: %s', self.irc.name, regularmodes, banmodes)
|
||||||
|
|
||||||
|
changedmodes = modes
|
||||||
|
while users[:12]:
|
||||||
uids = []
|
uids = []
|
||||||
namelist = []
|
namelist = []
|
||||||
# We take <users> as a list of (prefixmodes, uid) pairs.
|
# We take <users> as a list of (prefixmodes, uid) pairs.
|
||||||
for userpair in users[:10]:
|
for userpair in users[:12]:
|
||||||
assert len(userpair) == 2, "Incorrect format of userpair: %r" % userpair
|
assert len(userpair) == 2, "Incorrect format of userpair: %r" % userpair
|
||||||
prefixes, user = userpair
|
prefixes, user = userpair
|
||||||
prefixchars = ''
|
prefixchars = ''
|
||||||
@ -124,22 +131,34 @@ class TS6Protocol(TS6BaseProtocol):
|
|||||||
pr = self.irc.prefixmodes.get(prefix)
|
pr = self.irc.prefixmodes.get(prefix)
|
||||||
if pr:
|
if pr:
|
||||||
prefixchars += pr
|
prefixchars += pr
|
||||||
changedmodes.append(('+%s' % prefix, user))
|
changedmodes.add(('+%s' % prefix, user))
|
||||||
namelist.append(prefixchars+user)
|
namelist.append(prefixchars+user)
|
||||||
uids.append(user)
|
uids.append(user)
|
||||||
try:
|
try:
|
||||||
self.irc.users[user].channels.add(channel)
|
self.irc.users[user].channels.add(channel)
|
||||||
except KeyError: # Not initialized yet?
|
except KeyError: # Not initialized yet?
|
||||||
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user)
|
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user)
|
||||||
users = users[10:]
|
users = users[12:]
|
||||||
namelist = ' '.join(namelist)
|
namelist = ' '.join(namelist)
|
||||||
self._send(server, "SJOIN {ts} {channel} {modes} :{users}".format(
|
self._send(server, "SJOIN {ts} {channel} {modes} :{users}".format(
|
||||||
ts=ts, users=namelist, channel=channel,
|
ts=ts, users=namelist, channel=channel,
|
||||||
modes=self.irc.joinModes(modes)))
|
modes=self.irc.joinModes(regularmodes)))
|
||||||
self.irc.channels[channel].users.update(uids)
|
self.irc.channels[channel].users.update(uids)
|
||||||
if ts <= orig_ts:
|
|
||||||
# Only save our prefix modes in the channel state if our TS is lower than or equal to theirs.
|
# Now, burst bans.
|
||||||
self.irc.applyModes(channel, changedmodes)
|
# <- :42X BMASK 1424222769 #dev b :*!test@*.isp.net *!badident@*
|
||||||
|
for bmode, bans in banmodes.items():
|
||||||
|
# Max 15-3 = 12 bans per line to prevent cut off. (TS6 allows a max of 15 parameters per
|
||||||
|
# line)
|
||||||
|
if bans:
|
||||||
|
log.debug('(%s) sjoin: bursting mode %s with bans %s, ts:%s', self.irc.name, bmode, bans, ts)
|
||||||
|
bans = list(bans) # Convert into list for splicing
|
||||||
|
while bans[:12]:
|
||||||
|
self._send(server, "BMASK {ts} {channel} {bmode} :{bans}".format(ts=ts,
|
||||||
|
channel=channel, bmode=bmode, bans=' '.join(bans[:12])))
|
||||||
|
bans = bans[12:]
|
||||||
|
|
||||||
|
self.updateTS(channel, ts, changedmodes)
|
||||||
|
|
||||||
def mode(self, numeric, target, modes, ts=None):
|
def mode(self, numeric, target, modes, ts=None):
|
||||||
"""Sends mode changes from a PyLink client/server."""
|
"""Sends mode changes from a PyLink client/server."""
|
||||||
@ -160,34 +179,16 @@ class TS6Protocol(TS6BaseProtocol):
|
|||||||
|
|
||||||
# On output, at most ten cmode parameters should be sent; if there are more,
|
# On output, at most ten cmode parameters should be sent; if there are more,
|
||||||
# multiple TMODE messages should be sent.
|
# multiple TMODE messages should be sent.
|
||||||
while modes[:9]:
|
while modes[:10]:
|
||||||
# Seriously, though. If you send more than 10 mode parameters in
|
# Seriously, though. If you send more than 10 mode parameters in
|
||||||
# a line, charybdis will silently REJECT the entire command!
|
# a line, charybdis will silently REJECT the entire command!
|
||||||
joinedmodes = self.irc.joinModes(modes = [m for m in modes[:9] if m[0] not in self.irc.cmodes['*A']])
|
joinedmodes = self.irc.joinModes(modes = [m for m in modes[:10] if m[0] not in self.irc.cmodes['*A']])
|
||||||
modes = modes[9:]
|
modes = modes[10:]
|
||||||
self._send(numeric, 'TMODE %s %s %s' % (ts, target, joinedmodes))
|
self._send(numeric, 'TMODE %s %s %s' % (ts, target, joinedmodes))
|
||||||
else:
|
else:
|
||||||
joinedmodes = self.irc.joinModes(modes)
|
joinedmodes = self.irc.joinModes(modes)
|
||||||
self._send(numeric, 'MODE %s %s' % (target, joinedmodes))
|
self._send(numeric, 'MODE %s %s' % (target, joinedmodes))
|
||||||
|
|
||||||
def kill(self, numeric, target, reason):
|
|
||||||
"""Sends a kill from a PyLink client/server."""
|
|
||||||
|
|
||||||
if (not self.irc.isInternalClient(numeric)) and \
|
|
||||||
(not self.irc.isInternalServer(numeric)):
|
|
||||||
raise LookupError('No such PyLink client/server exists.')
|
|
||||||
|
|
||||||
# KILL:
|
|
||||||
# parameters: target user, path
|
|
||||||
|
|
||||||
# The format of the path parameter is some sort of description of the source of
|
|
||||||
# the kill followed by a space and a parenthesized reason. To avoid overflow,
|
|
||||||
# it is recommended not to add anything to the path.
|
|
||||||
|
|
||||||
assert target in self.irc.users, "Unknown target %r for kill()!" % target
|
|
||||||
self._send(numeric, 'KILL %s :Killed (%s)' % (target, reason))
|
|
||||||
self.removeClient(target)
|
|
||||||
|
|
||||||
def topicBurst(self, numeric, target, text):
|
def topicBurst(self, numeric, target, text):
|
||||||
"""Sends a topic change from a PyLink server. This is usually used on burst."""
|
"""Sends a topic change from a PyLink server. This is usually used on burst."""
|
||||||
if not self.irc.isInternalServer(numeric):
|
if not self.irc.isInternalServer(numeric):
|
||||||
@ -422,15 +423,15 @@ class TS6Protocol(TS6BaseProtocol):
|
|||||||
# <- :0UY SJOIN 1451041566 #channel +nt :@0UYAAAAAB
|
# <- :0UY SJOIN 1451041566 #channel +nt :@0UYAAAAAB
|
||||||
channel = self.irc.toLower(args[1])
|
channel = self.irc.toLower(args[1])
|
||||||
userlist = args[-1].split()
|
userlist = args[-1].split()
|
||||||
their_ts = int(args[0])
|
|
||||||
our_ts = self.irc.channels[channel].ts
|
|
||||||
|
|
||||||
self.updateTS(channel, their_ts)
|
|
||||||
|
|
||||||
modestring = args[2:-1] or args[2]
|
modestring = args[2:-1] or args[2]
|
||||||
parsedmodes = self.irc.parseModes(channel, modestring)
|
parsedmodes = self.irc.parseModes(channel, modestring)
|
||||||
self.irc.applyModes(channel, parsedmodes)
|
self.irc.applyModes(channel, parsedmodes)
|
||||||
namelist = []
|
namelist = []
|
||||||
|
|
||||||
|
# Keep track of other modes that are added due to prefix modes being joined too.
|
||||||
|
changedmodes = set(parsedmodes)
|
||||||
|
|
||||||
log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel)
|
log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel)
|
||||||
for userpair in userlist:
|
for userpair in userlist:
|
||||||
# charybdis sends this in the form "@+UID1, +UID2, UID3, @UID4"
|
# charybdis sends this in the form "@+UID1, +UID2, UID3, @UID4"
|
||||||
@ -455,9 +456,16 @@ class TS6Protocol(TS6BaseProtocol):
|
|||||||
finalprefix += char
|
finalprefix += char
|
||||||
namelist.append(user)
|
namelist.append(user)
|
||||||
self.irc.users[user].channels.add(channel)
|
self.irc.users[user].channels.add(channel)
|
||||||
if their_ts <= our_ts:
|
|
||||||
self.irc.applyModes(channel, [('+%s' % mode, user) for mode in finalprefix])
|
# Only save mode changes if the remote has lower TS than us.
|
||||||
|
changedmodes |= {('+%s' % mode, user) for mode in finalprefix}
|
||||||
self.irc.channels[channel].users.add(user)
|
self.irc.channels[channel].users.add(user)
|
||||||
|
|
||||||
|
# Statekeeping with timestamps
|
||||||
|
their_ts = int(args[0])
|
||||||
|
our_ts = self.irc.channels[channel].ts
|
||||||
|
self.updateTS(channel, their_ts, changedmodes)
|
||||||
|
|
||||||
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts}
|
return {'channel': channel, 'users': namelist, 'modes': parsedmodes, 'ts': their_ts}
|
||||||
|
|
||||||
def handle_join(self, numeric, command, args):
|
def handle_join(self, numeric, command, args):
|
||||||
@ -658,23 +666,18 @@ class TS6Protocol(TS6BaseProtocol):
|
|||||||
'your IRCd configuration.', self.irc.name, setter, badmode,
|
'your IRCd configuration.', self.irc.name, setter, badmode,
|
||||||
charlist[badmode])
|
charlist[badmode])
|
||||||
|
|
||||||
def handle_encap(self, numeric, command, args):
|
def handle_su(self, numeric, command, args):
|
||||||
"""
|
"""
|
||||||
Handles the ENCAP command - encapsulated TS6 commands with a variety of
|
Handles SU, which is used for setting login information
|
||||||
subcommands used for different purposes.
|
|
||||||
"""
|
"""
|
||||||
commandname = args[1]
|
|
||||||
|
|
||||||
if commandname == 'SU':
|
|
||||||
# Handles SU, which is used for setting login information
|
|
||||||
# <- :00A ENCAP * SU 42XAAAAAC :GLolol
|
# <- :00A ENCAP * SU 42XAAAAAC :GLolol
|
||||||
# <- :00A ENCAP * SU 42XAAAAAC
|
# <- :00A ENCAP * SU 42XAAAAAC
|
||||||
try:
|
try:
|
||||||
account = args[3] # Account name is being set
|
account = args[1] # Account name is being set
|
||||||
except IndexError:
|
except IndexError:
|
||||||
account = '' # No account name means a logout
|
account = '' # No account name means a logout
|
||||||
|
|
||||||
uid = args[2]
|
uid = args[0]
|
||||||
self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
self.irc.callHooks([uid, 'CLIENT_SERVICES_LOGIN', {'text': account}])
|
||||||
|
|
||||||
Class = TS6Protocol
|
Class = TS6Protocol
|
||||||
|
@ -2,18 +2,12 @@
|
|||||||
ts6_common.py: Common base protocol class with functions shared by the UnrealIRCd, InspIRCd, and TS6 protocol modules.
|
ts6_common.py: Common base protocol class with functions shared by the UnrealIRCd, InspIRCd, and TS6 protocol modules.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import string
|
import string
|
||||||
|
|
||||||
# Import hacks to access utils and classes...
|
from pylinkirc import utils, structures
|
||||||
curdir = os.path.dirname(__file__)
|
from pylinkirc.classes import *
|
||||||
sys.path += [curdir, os.path.dirname(curdir)]
|
from pylinkirc.log import log
|
||||||
|
from pylinkirc.protocols.ircs2s_common import *
|
||||||
import utils
|
|
||||||
from log import log
|
|
||||||
from classes import *
|
|
||||||
import structures
|
|
||||||
|
|
||||||
class TS6SIDGenerator():
|
class TS6SIDGenerator():
|
||||||
"""
|
"""
|
||||||
@ -103,7 +97,7 @@ class TS6UIDGenerator(utils.IncrementalUIDGenerator):
|
|||||||
self.length = 6
|
self.length = 6
|
||||||
super().__init__(sid)
|
super().__init__(sid)
|
||||||
|
|
||||||
class TS6BaseProtocol(Protocol):
|
class TS6BaseProtocol(IRCS2SProtocol):
|
||||||
|
|
||||||
def __init__(self, irc):
|
def __init__(self, irc):
|
||||||
super().__init__(irc)
|
super().__init__(irc)
|
||||||
@ -165,6 +159,39 @@ class TS6BaseProtocol(Protocol):
|
|||||||
# handle_part() does that just fine.
|
# handle_part() does that just fine.
|
||||||
self.handle_part(target, 'KICK', [channel])
|
self.handle_part(target, 'KICK', [channel])
|
||||||
|
|
||||||
|
def kill(self, numeric, target, reason):
|
||||||
|
"""Sends a kill from a PyLink client/server."""
|
||||||
|
|
||||||
|
if (not self.irc.isInternalClient(numeric)) and \
|
||||||
|
(not self.irc.isInternalServer(numeric)):
|
||||||
|
raise LookupError('No such PyLink client/server exists.')
|
||||||
|
|
||||||
|
# From TS6 docs:
|
||||||
|
# KILL:
|
||||||
|
# parameters: target user, path
|
||||||
|
|
||||||
|
# The format of the path parameter is some sort of description of the source of
|
||||||
|
# the kill followed by a space and a parenthesized reason. To avoid overflow,
|
||||||
|
# it is recommended not to add anything to the path.
|
||||||
|
|
||||||
|
assert target in self.irc.users, "Unknown target %r for kill()!" % target
|
||||||
|
|
||||||
|
if numeric in self.irc.users:
|
||||||
|
# Killer was an user. Follow examples of setting the path to be "killer.host!killer.nick".
|
||||||
|
userobj = self.irc.users[numeric]
|
||||||
|
killpath = '%s!%s' % (userobj.host, userobj.nick)
|
||||||
|
elif numeric in self.irc.servers:
|
||||||
|
# Sender was a server; killpath is just its name.
|
||||||
|
killpath = self.irc.servers[numeric].name
|
||||||
|
else:
|
||||||
|
# Invalid sender?! This shouldn't happen, but make the killpath our server name anyways.
|
||||||
|
log.warning('(%s) Invalid sender %s for kill(); using our server name instead.',
|
||||||
|
self.irc.name, numeric)
|
||||||
|
killpath = self.irc.servers[self.irc.sid].name
|
||||||
|
|
||||||
|
self._send(numeric, 'KILL %s :%s (%s)' % (target, killpath, reason))
|
||||||
|
self.removeClient(target)
|
||||||
|
|
||||||
def nick(self, numeric, newnick):
|
def nick(self, numeric, newnick):
|
||||||
"""Changes the nick of a PyLink client."""
|
"""Changes the nick of a PyLink client."""
|
||||||
if not self.irc.isInternalClient(numeric):
|
if not self.irc.isInternalClient(numeric):
|
||||||
@ -303,6 +330,13 @@ class TS6BaseProtocol(Protocol):
|
|||||||
command = args[0]
|
command = args[0]
|
||||||
args = args[1:]
|
args = args[1:]
|
||||||
|
|
||||||
|
if command == 'ENCAP':
|
||||||
|
# Special case for encapsulated commands (ENCAP), in forms like this:
|
||||||
|
# <- :00A ENCAP * SU 42XAAAAAC :GLolol
|
||||||
|
command = args[1]
|
||||||
|
args = args[2:]
|
||||||
|
log.debug("(%s) Rewriting incoming ENCAP to command %s (args: %s)", self.irc.name, command, args)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
func = getattr(self, 'handle_'+command.lower())
|
func = getattr(self, 'handle_'+command.lower())
|
||||||
except AttributeError: # unhandled command
|
except AttributeError: # unhandled command
|
||||||
@ -324,19 +358,6 @@ class TS6BaseProtocol(Protocol):
|
|||||||
|
|
||||||
handle_notice = handle_privmsg
|
handle_notice = handle_privmsg
|
||||||
|
|
||||||
def handle_kill(self, source, command, args):
|
|
||||||
"""Handles incoming KILLs."""
|
|
||||||
killed = args[0]
|
|
||||||
# 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.irc.users.get(killed)
|
|
||||||
if data:
|
|
||||||
self.removeClient(killed)
|
|
||||||
return {'target': killed, 'text': args[1], 'userdata': data}
|
|
||||||
|
|
||||||
def handle_kick(self, source, command, args):
|
def handle_kick(self, source, command, args):
|
||||||
"""Handles incoming KICKs."""
|
"""Handles incoming KICKs."""
|
||||||
# :70MAAAAAA KICK #test 70MAAAAAA :some reason
|
# :70MAAAAAA KICK #test 70MAAAAAA :some reason
|
||||||
@ -378,32 +399,6 @@ class TS6BaseProtocol(Protocol):
|
|||||||
self.irc.users[user].nick = user
|
self.irc.users[user].nick = user
|
||||||
return {'target': user, 'ts': int(args[1]), 'oldnick': oldnick}
|
return {'target': user, 'ts': int(args[1]), 'oldnick': oldnick}
|
||||||
|
|
||||||
def handle_squit(self, numeric, command, args):
|
|
||||||
"""Handles incoming SQUITs (netsplits)."""
|
|
||||||
# :70M SQUIT 1ML :Server quit by GL!gl@0::1
|
|
||||||
log.debug('handle_squit args: %s', args)
|
|
||||||
split_server = args[0]
|
|
||||||
affected_users = []
|
|
||||||
log.debug('(%s) Splitting server %s (reason: %s)', self.irc.name, split_server, args[-1])
|
|
||||||
if split_server not in self.irc.servers:
|
|
||||||
log.warning("(%s) Tried to split a server (%s) that didn't exist!", self.irc.name, split_server)
|
|
||||||
return
|
|
||||||
# Prevent RuntimeError: dictionary changed size during iteration
|
|
||||||
old_servers = self.irc.servers.copy()
|
|
||||||
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)
|
|
||||||
args = self.handle_squit(sid, 'SQUIT', [sid, "PyLink: Automatically splitting leaf servers of %s" % sid])
|
|
||||||
affected_users += args['users']
|
|
||||||
for user in self.irc.servers[split_server].users.copy():
|
|
||||||
affected_users.append(user)
|
|
||||||
log.debug('Removing client %s (%s)', user, self.irc.users[user].nick)
|
|
||||||
self.removeClient(user)
|
|
||||||
sname = self.irc.servers[split_server].name
|
|
||||||
del self.irc.servers[split_server]
|
|
||||||
log.debug('(%s) Netsplit affected users: %s', self.irc.name, affected_users)
|
|
||||||
return {'target': split_server, 'users': affected_users, 'name': sname}
|
|
||||||
|
|
||||||
def handle_topic(self, numeric, command, args):
|
def handle_topic(self, numeric, command, args):
|
||||||
"""Handles incoming TOPIC changes from clients. For topic bursts,
|
"""Handles incoming TOPIC changes from clients. For topic bursts,
|
||||||
TB (TS6/charybdis) and FTOPIC (InspIRCd) are used instead."""
|
TB (TS6/charybdis) and FTOPIC (InspIRCd) are used instead."""
|
||||||
|
@ -9,14 +9,10 @@ import codecs
|
|||||||
import socket
|
import socket
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Import hacks to access utils and classes...
|
from pylinkirc import utils
|
||||||
curdir = os.path.dirname(__file__)
|
from pylinkirc.classes import *
|
||||||
sys.path += [curdir, os.path.dirname(curdir)]
|
from pylinkirc.log import log
|
||||||
|
from pylinkirc.protocols.ts6_common import *
|
||||||
import utils
|
|
||||||
from log import log
|
|
||||||
from classes import *
|
|
||||||
from ts6_common import *
|
|
||||||
|
|
||||||
class UnrealProtocol(TS6BaseProtocol):
|
class UnrealProtocol(TS6BaseProtocol):
|
||||||
def __init__(self, irc):
|
def __init__(self, irc):
|
||||||
@ -127,7 +123,7 @@ class UnrealProtocol(TS6BaseProtocol):
|
|||||||
self.irc.channels[channel].users.add(client)
|
self.irc.channels[channel].users.add(client)
|
||||||
self.irc.users[client].channels.add(channel)
|
self.irc.users[client].channels.add(channel)
|
||||||
|
|
||||||
def sjoin(self, server, channel, users, ts=None):
|
def sjoin(self, server, channel, users, ts=None, modes=set()):
|
||||||
"""Sends an SJOIN for a group of users to a channel.
|
"""Sends an SJOIN for a group of users to a channel.
|
||||||
|
|
||||||
The sender should always be a server (SID). TS is optional, and defaults
|
The sender should always be a server (SID). TS is optional, and defaults
|
||||||
@ -151,36 +147,46 @@ class UnrealProtocol(TS6BaseProtocol):
|
|||||||
if not server:
|
if not server:
|
||||||
raise LookupError('No such PyLink server exists.')
|
raise LookupError('No such PyLink server exists.')
|
||||||
|
|
||||||
|
changedmodes = set(modes or self.irc.channels[channel].modes)
|
||||||
orig_ts = self.irc.channels[channel].ts
|
orig_ts = self.irc.channels[channel].ts
|
||||||
ts = ts or orig_ts
|
ts = ts or orig_ts
|
||||||
self.updateTS(channel, ts)
|
|
||||||
|
|
||||||
changedmodes = []
|
|
||||||
uids = []
|
uids = []
|
||||||
namelist = []
|
namelist = []
|
||||||
|
|
||||||
for userpair in users:
|
for userpair in users:
|
||||||
assert len(userpair) == 2, "Incorrect format of userpair: %r" % userpair
|
assert len(userpair) == 2, "Incorrect format of userpair: %r" % userpair
|
||||||
prefixes, user = userpair
|
prefixes, user = userpair
|
||||||
|
|
||||||
# Unreal uses slightly different prefixes in SJOIN. +q is * instead of ~,
|
# Unreal uses slightly different prefixes in SJOIN. +q is * instead of ~,
|
||||||
# and +a is ~ instead of &.
|
# and +a is ~ instead of &.
|
||||||
# &, ", and ' are used for bursting bans.
|
# &, ", and ' are used for bursting bans.
|
||||||
sjoin_prefixes = {'q': '*', 'a': '~', 'o': '@', 'h': '%', 'v': '+'}
|
sjoin_prefixes = {'q': '*', 'a': '~', 'o': '@', 'h': '%', 'v': '+'}
|
||||||
prefixchars = ''.join([sjoin_prefixes.get(prefix, '') for prefix in prefixes])
|
prefixchars = ''.join([sjoin_prefixes.get(prefix, '') for prefix in prefixes])
|
||||||
|
|
||||||
if prefixchars:
|
if prefixchars:
|
||||||
changedmodes + [('+%s' % prefix, user) for prefix in prefixes]
|
changedmodes |= {('+%s' % prefix, user) for prefix in prefixes}
|
||||||
|
|
||||||
namelist.append(prefixchars+user)
|
namelist.append(prefixchars+user)
|
||||||
uids.append(user)
|
uids.append(user)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.irc.users[user].channels.add(channel)
|
self.irc.users[user].channels.add(channel)
|
||||||
except KeyError: # Not initialized yet?
|
except KeyError: # Not initialized yet?
|
||||||
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user)
|
log.debug("(%s) sjoin: KeyError trying to add %r to %r's channel list?", self.irc.name, channel, user)
|
||||||
|
|
||||||
namelist = ' '.join(namelist)
|
namelist = ' '.join(namelist)
|
||||||
|
|
||||||
self._send(server, "SJOIN {ts} {channel} :{users}".format(
|
self._send(server, "SJOIN {ts} {channel} :{users}".format(
|
||||||
ts=ts, users=namelist, channel=channel))
|
ts=ts, users=namelist, channel=channel))
|
||||||
|
|
||||||
|
# Burst modes separately. No really, this is what I see UnrealIRCd do! It sends
|
||||||
|
# JOINs on burst and then MODE!
|
||||||
|
if modes:
|
||||||
|
self.mode(server, channel, modes, ts=ts)
|
||||||
|
|
||||||
self.irc.channels[channel].users.update(uids)
|
self.irc.channels[channel].users.update(uids)
|
||||||
if ts <= orig_ts:
|
|
||||||
# Only save our prefix modes in the channel state if our TS is lower than or equal to theirs.
|
self.updateTS(channel, ts, changedmodes)
|
||||||
self.irc.applyModes(channel, changedmodes)
|
|
||||||
|
|
||||||
def ping(self, source=None, target=None):
|
def ping(self, source=None, target=None):
|
||||||
"""Sends a PING to a target server. Periodic PINGs are sent to our uplink
|
"""Sends a PING to a target server. Periodic PINGs are sent to our uplink
|
||||||
@ -190,20 +196,6 @@ class UnrealProtocol(TS6BaseProtocol):
|
|||||||
if not (target is None or source is None):
|
if not (target is None or source is None):
|
||||||
self._send(source, 'PING %s %s' % (self.irc.servers[source].name, self.irc.servers[target].name))
|
self._send(source, 'PING %s %s' % (self.irc.servers[source].name, self.irc.servers[target].name))
|
||||||
|
|
||||||
def kill(self, numeric, target, reason):
|
|
||||||
"""Sends a kill from a PyLink client/server."""
|
|
||||||
# <- :GL KILL 38QAAAAAA :hidden-1C620195!GL (test)
|
|
||||||
|
|
||||||
if (not self.irc.isInternalClient(numeric)) and \
|
|
||||||
(not self.irc.isInternalServer(numeric)):
|
|
||||||
raise LookupError('No such PyLink client/server exists.')
|
|
||||||
|
|
||||||
assert target in self.irc.users, "Unknown target %r for kill()!" % target
|
|
||||||
|
|
||||||
# The killpath doesn't really matter here...
|
|
||||||
self._send(numeric, 'KILL %s :%s!PyLink (%s)' % (target, self.irc.serverdata['hostname'], reason))
|
|
||||||
self.removeClient(target)
|
|
||||||
|
|
||||||
def mode(self, numeric, target, modes, ts=None):
|
def mode(self, numeric, target, modes, ts=None):
|
||||||
"""
|
"""
|
||||||
Sends mode changes from a PyLink client/server. The mode list should be
|
Sends mode changes from a PyLink client/server. The mode list should be
|
||||||
@ -320,7 +312,7 @@ class UnrealProtocol(TS6BaseProtocol):
|
|||||||
host = self.irc.serverdata["hostname"]
|
host = self.irc.serverdata["hostname"]
|
||||||
|
|
||||||
f('PASS :%s' % self.irc.serverdata["sendpass"])
|
f('PASS :%s' % self.irc.serverdata["sendpass"])
|
||||||
# https://github.com/unrealircd/unrealself.ircd/blob/2f8cb55e/doc/technical/protoctl.txt
|
# https://github.com/unrealircd/unrealircd/blob/2f8cb55e/doc/technical/protoctl.txt
|
||||||
# We support the following protocol features:
|
# We support the following protocol features:
|
||||||
# SJ3 - extended SJOIN
|
# SJ3 - extended SJOIN
|
||||||
# NOQUIT - QUIT messages aren't sent for all users in a netsplit
|
# NOQUIT - QUIT messages aren't sent for all users in a netsplit
|
||||||
@ -550,12 +542,12 @@ class UnrealProtocol(TS6BaseProtocol):
|
|||||||
channel = self.irc.toLower(args[1])
|
channel = self.irc.toLower(args[1])
|
||||||
userlist = args[-1].split()
|
userlist = args[-1].split()
|
||||||
|
|
||||||
our_ts = self.irc.channels[channel].ts
|
|
||||||
their_ts = int(args[0])
|
|
||||||
self.updateTS(channel, their_ts)
|
|
||||||
|
|
||||||
namelist = []
|
namelist = []
|
||||||
log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel)
|
log.debug('(%s) handle_sjoin: got userlist %r for %r', self.irc.name, userlist, channel)
|
||||||
|
|
||||||
|
# Keep track of other modes that are added due to prefix modes being joined too.
|
||||||
|
changedmodes = set(self.irc.channels[channel].modes)
|
||||||
|
|
||||||
for userpair in userlist:
|
for userpair in userlist:
|
||||||
if userpair.startswith("&\"'"): # TODO: handle ban bursts too
|
if userpair.startswith("&\"'"): # TODO: handle ban bursts too
|
||||||
# &, ", and ' entries are used for bursting bans:
|
# &, ", and ' entries are used for bursting bans:
|
||||||
@ -577,10 +569,16 @@ class UnrealProtocol(TS6BaseProtocol):
|
|||||||
finalprefix += char
|
finalprefix += char
|
||||||
namelist.append(user)
|
namelist.append(user)
|
||||||
self.irc.users[user].channels.add(channel)
|
self.irc.users[user].channels.add(channel)
|
||||||
|
|
||||||
# Only merge the remote's prefix modes if their TS is smaller or equal to ours.
|
# Only merge the remote's prefix modes if their TS is smaller or equal to ours.
|
||||||
if their_ts <= our_ts:
|
changedmodes |= {('+%s' % mode, user) for mode in finalprefix}
|
||||||
self.irc.applyModes(channel, [('+%s' % mode, user) for mode in finalprefix])
|
|
||||||
self.irc.channels[channel].users.add(user)
|
self.irc.channels[channel].users.add(user)
|
||||||
|
|
||||||
|
our_ts = self.irc.channels[channel].ts
|
||||||
|
their_ts = int(args[0])
|
||||||
|
self.updateTS(channel, their_ts, changedmodes)
|
||||||
|
|
||||||
return {'channel': channel, 'users': namelist, 'modes': self.irc.channels[channel].modes, 'ts': their_ts}
|
return {'channel': channel, 'users': namelist, 'modes': self.irc.channels[channel].modes, 'ts': their_ts}
|
||||||
|
|
||||||
def handle_nick(self, numeric, command, args):
|
def handle_nick(self, numeric, command, args):
|
||||||
@ -640,10 +638,21 @@ class UnrealProtocol(TS6BaseProtocol):
|
|||||||
if utils.isChannel(args[0]):
|
if utils.isChannel(args[0]):
|
||||||
channel = self.irc.toLower(args[0])
|
channel = self.irc.toLower(args[0])
|
||||||
oldobj = self.irc.channels[channel].deepcopy()
|
oldobj = self.irc.channels[channel].deepcopy()
|
||||||
|
|
||||||
modes = list(filter(None, args[1:])) # normalize whitespace
|
modes = list(filter(None, args[1:])) # normalize whitespace
|
||||||
parsedmodes = self.irc.parseModes(channel, modes)
|
parsedmodes = self.irc.parseModes(channel, modes)
|
||||||
|
|
||||||
if parsedmodes:
|
if parsedmodes:
|
||||||
|
if parsedmodes[0][0] == '+&':
|
||||||
|
# UnrealIRCd uses a & virtual mode to denote mode bounces, meaning that an attempt to set modes
|
||||||
|
# by us was rejected for some reason (usually due to timestamps). Warn about this and drop the
|
||||||
|
# mode change to prevent mode floods.
|
||||||
|
log.warning("(%s) Received mode bounce %s in channel %s! Our TS: %s",
|
||||||
|
self.irc.name, modes, channel, self.irc.channels[channel].ts)
|
||||||
|
return
|
||||||
|
|
||||||
self.irc.applyModes(channel, parsedmodes)
|
self.irc.applyModes(channel, parsedmodes)
|
||||||
|
|
||||||
if numeric in self.irc.servers and args[-1].isdigit():
|
if numeric in self.irc.servers and args[-1].isdigit():
|
||||||
# Sender is a server AND last arg is number. Perform TS updates.
|
# Sender is a server AND last arg is number. Perform TS updates.
|
||||||
their_ts = int(args[-1])
|
their_ts = int(args[-1])
|
||||||
|
47
pylink
47
pylink
@ -1,25 +1,40 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
PyLink IRC Services launcher.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
try:
|
||||||
# Change directory to the folder containing PyLink's source
|
from pylinkirc import world, conf, __version__
|
||||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
except ImportError:
|
||||||
|
sys.stderr.write("ERROR: Failed to import PyLink main module (pylinkirc.world).\n\nIf you are "
|
||||||
# This must be done before conf imports, so we get the real conf instead of testing one.
|
"running PyLink from source, please install PyLink first using 'python3 "
|
||||||
import world
|
"setup.py install [--user]'\n")
|
||||||
world.testing = False
|
sys.exit(1)
|
||||||
|
|
||||||
import conf
|
|
||||||
from log import log
|
|
||||||
import classes
|
|
||||||
import utils
|
|
||||||
import coreplugin
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
log.info('PyLink %s starting...', world.version)
|
import argparse
|
||||||
|
|
||||||
# Write a PID file.
|
parser = argparse.ArgumentParser(description='Starts an instance of PyLink IRC Services.')
|
||||||
|
parser.add_argument('config', help='specifies the path to the config file (defaults to pylink.yml)', nargs='?', default='pylink.yml')
|
||||||
|
parser.add_argument("-v", "--version", help="displays the program version and exits", action='store_true')
|
||||||
|
parser.add_argument("-n", "--no-pid", help="skips generating PID files", action='store_true')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.version: # Display version and exit
|
||||||
|
print('PyLink ' + __version__)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
# Load the config
|
||||||
|
conf.loadConf(args.config)
|
||||||
|
|
||||||
|
from pylinkirc.log import log
|
||||||
|
from pylinkirc import classes, utils, coremods
|
||||||
|
log.info('PyLink %s starting...', __version__)
|
||||||
|
|
||||||
|
# Write a PID file unless specifically told not to.
|
||||||
|
if not args.no_pid:
|
||||||
with open('%s.pid' % conf.confname, 'w') as f:
|
with open('%s.pid' % conf.confname, 'w') as f:
|
||||||
f.write(str(os.getpid()))
|
f.write(str(os.getpid()))
|
||||||
|
|
||||||
@ -30,7 +45,7 @@ if __name__ == '__main__':
|
|||||||
# dynamically depending on which were configured.
|
# dynamically depending on which were configured.
|
||||||
for plugin in to_load:
|
for plugin in to_load:
|
||||||
try:
|
try:
|
||||||
world.plugins[plugin] = pl = utils.loadModuleFromFolder(plugin, world.plugins_folder)
|
world.plugins[plugin] = pl = utils.loadPlugin(plugin)
|
||||||
except (OSError, ImportError) as e:
|
except (OSError, ImportError) as e:
|
||||||
log.exception('Failed to load plugin %r: %s: %s', plugin, type(e).__name__, str(e))
|
log.exception('Failed to load plugin %r: %s: %s', plugin, type(e).__name__, str(e))
|
||||||
else:
|
else:
|
||||||
|
26
runtests
26
runtests
@ -1,26 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
|
||||||
runner = unittest.TextTestRunner(verbosity=2)
|
|
||||||
fails = []
|
|
||||||
suites = []
|
|
||||||
|
|
||||||
# Yay, import hacks!
|
|
||||||
sys.path.append(os.path.join(os.getcwd(), 'tests'))
|
|
||||||
|
|
||||||
query = sys.argv[1:] or glob.glob('tests/test_*.py')
|
|
||||||
|
|
||||||
for testfile in query:
|
|
||||||
# Strip the tests/ and .py extension: tests/test_whatever.py => test_whatever
|
|
||||||
module = testfile.replace('.py', '').replace('tests/', '')
|
|
||||||
module = __import__(module)
|
|
||||||
suites.append(unittest.defaultTestLoader.loadTestsFromModule(module))
|
|
||||||
|
|
||||||
testsuite = unittest.TestSuite(suites)
|
|
||||||
runner.run(testsuite)
|
|
79
setup.py
Normal file
79
setup.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
"""Setup module for PyLink IRC Services."""
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
from codecs import open
|
||||||
|
import subprocess
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
# Get version from Git tags.
|
||||||
|
try:
|
||||||
|
version = subprocess.check_output(['git', 'describe', '--tags']).decode('utf-8').strip()
|
||||||
|
except Exception as e:
|
||||||
|
print('ERROR: Failed to get version from "git describe --tags": %s: %s' % (type(e).__name__, e))
|
||||||
|
from __init__ import __version__ as fallback_version
|
||||||
|
|
||||||
|
# Mark builds with unretrievable version (GitHub tarballs, etc.) as -dirty
|
||||||
|
if not fallback_version.endswith('-dirty'):
|
||||||
|
fallback_version += '-dirty'
|
||||||
|
|
||||||
|
print('Using fallback version of %r.' % fallback_version)
|
||||||
|
version = fallback_version
|
||||||
|
|
||||||
|
# Write the version to disk.
|
||||||
|
with open('__init__.py', 'w') as f:
|
||||||
|
f.write('# Automatically generated by setup.py\n')
|
||||||
|
f.write('__version__ = %r\n' % version)
|
||||||
|
|
||||||
|
curdir = path.abspath(path.dirname(__file__))
|
||||||
|
|
||||||
|
# FIXME: Convert markdown to RST
|
||||||
|
with open(path.join(curdir, 'README.md'), encoding='utf-8') as f:
|
||||||
|
long_description = f.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='pylinkirc',
|
||||||
|
version=version,
|
||||||
|
|
||||||
|
description='PyLink IRC Services',
|
||||||
|
long_description=long_description,
|
||||||
|
|
||||||
|
url='https://github.com/GLolol/PyLink',
|
||||||
|
|
||||||
|
# Author details
|
||||||
|
author='James Lu',
|
||||||
|
author_email='GLolol@overdrivenetworks.com',
|
||||||
|
|
||||||
|
# Choose your license
|
||||||
|
license='MPL 2.0',
|
||||||
|
|
||||||
|
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 3 - Alpha',
|
||||||
|
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'Intended Audience :: System Administrators',
|
||||||
|
'Topic :: Communications :: Chat :: Internet Relay Chat',
|
||||||
|
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||||
|
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
|
||||||
|
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.4',
|
||||||
|
'Programming Language :: Python :: 3.5',
|
||||||
|
],
|
||||||
|
|
||||||
|
keywords='IRC services chat',
|
||||||
|
install_requires=['pyyaml'],
|
||||||
|
|
||||||
|
# Folders (packages of code)
|
||||||
|
packages=['pylinkirc', 'pylinkirc.protocols', 'pylinkirc.plugins', 'pylinkirc.coremods'],
|
||||||
|
|
||||||
|
# Data files
|
||||||
|
package_data={
|
||||||
|
'': ['example-conf.yml'],
|
||||||
|
},
|
||||||
|
|
||||||
|
package_dir = {'pylinkirc': '.'},
|
||||||
|
|
||||||
|
# Executable scripts
|
||||||
|
scripts=["pylink"],
|
||||||
|
)
|
@ -1,18 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Shell script to start PyLink under CPUlimit, throttling it if it starts abusing the CPU.
|
|
||||||
|
|
||||||
# Set this to whatever you want. cpulimit --help
|
|
||||||
LIMIT=35
|
|
||||||
|
|
||||||
# Change to the PyLink root directory.
|
|
||||||
WRAPPER_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
||||||
cd "$WRAPPER_DIR"
|
|
||||||
|
|
||||||
if [[ ! -z "$(which cpulimit)" ]]; then
|
|
||||||
# -z makes cpulimit exit when PyLink dies.
|
|
||||||
cpulimit -l $LIMIT -z ./pylink $*
|
|
||||||
echo "PyLink has been started (daemonized) under cpulimit, and will automatically be throttled if it goes over the CPU limit of ${LIMIT}%."
|
|
||||||
echo "To kill the process manually, run ./kill.sh"
|
|
||||||
else
|
|
||||||
echo 'cpulimit not found in $PATH! Aborting.'
|
|
||||||
fi
|
|
@ -1,36 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import world
|
|
||||||
import coreplugin
|
|
||||||
import utils
|
|
||||||
import tests_common
|
|
||||||
|
|
||||||
inspircd = utils.getProtocolModule('inspircd')
|
|
||||||
world.testing = True
|
|
||||||
|
|
||||||
class CorePluginTestCase(tests_common.PluginTestCase):
|
|
||||||
@unittest.skip("Test doesn't work yet.")
|
|
||||||
def testKillRespawn(self):
|
|
||||||
self.irc.run(':9PY KILL {u} :test'.format(u=self.u))
|
|
||||||
hooks = self.irc.takeHooks()
|
|
||||||
|
|
||||||
# Make sure we're respawning our PseudoClient when its killed
|
|
||||||
print(hooks)
|
|
||||||
spmain = [h for h in hooks if h[1] == 'PYLINK_SPAWNMAIN']
|
|
||||||
self.assertTrue(spmain, 'PYLINK_SPAWNMAIN hook was never sent!')
|
|
||||||
|
|
||||||
msgs = self.irc.takeMsgs()
|
|
||||||
commands = self.irc.takeCommands(msgs)
|
|
||||||
self.assertIn('UID', commands)
|
|
||||||
self.assertIn('FJOIN', commands)
|
|
||||||
|
|
||||||
# Also make sure that we're updating the irc.pseudoclient field
|
|
||||||
self.assertNotEqual(self.irc.pseudoclient.uid, spmain[0]['olduser'])
|
|
||||||
|
|
||||||
def testKickRejoin(self):
|
|
||||||
self.proto.kick(self.u, '#pylink', self.u, 'test')
|
|
||||||
msgs = self.irc.takeMsgs()
|
|
||||||
commands = self.irc.takeCommands(msgs)
|
|
||||||
self.assertIn('FJOIN', commands)
|
|
@ -1,30 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
import classes
|
|
||||||
import unittest
|
|
||||||
import conf
|
|
||||||
|
|
||||||
class TestFakeIRC(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.irc = classes.FakeIRC('unittest', classes.FakeProto, conf.testconf)
|
|
||||||
|
|
||||||
def testFakeIRC(self):
|
|
||||||
self.irc.run('this should do nothing')
|
|
||||||
self.irc.send('ADD this message')
|
|
||||||
self.irc.send(':add THIS message too')
|
|
||||||
msgs = self.irc.takeMsgs()
|
|
||||||
self.assertEqual(['ADD this message', ':add THIS message too'],
|
|
||||||
msgs)
|
|
||||||
# takeMsgs() clears cached messages queue, so the next call should
|
|
||||||
# return an empty list.
|
|
||||||
msgs = self.irc.takeMsgs()
|
|
||||||
self.assertEqual([], msgs)
|
|
||||||
|
|
||||||
def testFakeIRCtakeCommands(self):
|
|
||||||
msgs = ['ADD this message', ':9PY THIS message too']
|
|
||||||
self.assertEqual(['ADD', 'THIS'], self.irc.takeCommands(msgs))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
@ -1,46 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
cwd = os.getcwd()
|
|
||||||
sys.path += [cwd, os.path.join(cwd, 'plugins')]
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import classes
|
|
||||||
import relay
|
|
||||||
import conf
|
|
||||||
|
|
||||||
def dummyf():
|
|
||||||
pass
|
|
||||||
|
|
||||||
class TestRelay(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.irc = classes.FakeIRC('fakeirc', classes.FakeProto, conf.testconf)
|
|
||||||
self.irc.maxnicklen = 20
|
|
||||||
self.f = lambda nick: relay.normalizeNick(self.irc, 'unittest', nick)
|
|
||||||
# Fake our protocol name to something that supports slashes in nicks.
|
|
||||||
# relay uses a whitelist for this to prevent accidentally introducing
|
|
||||||
# bad nicks:
|
|
||||||
self.irc.protoname = "inspircd"
|
|
||||||
|
|
||||||
def testNormalizeNick(self):
|
|
||||||
# Second argument simply states the suffix.
|
|
||||||
self.assertEqual(self.f('helloworld'), 'helloworld/unittest')
|
|
||||||
self.assertEqual(self.f('ObnoxiouslyLongNick'), 'Obnoxiously/unittest')
|
|
||||||
self.assertEqual(self.f('10XAAAAAA'), '_10XAAAAAA/unittest')
|
|
||||||
|
|
||||||
def testNormalizeNickConflict(self):
|
|
||||||
self.assertEqual(self.f('helloworld'), 'helloworld/unittest')
|
|
||||||
self.irc.users['10XAAAAAA'] = classes.IrcUser('helloworld/unittest', 1234, '10XAAAAAA')
|
|
||||||
# Increase amount of /'s by one
|
|
||||||
self.assertEqual(self.f('helloworld'), 'helloworld//unittest')
|
|
||||||
self.irc.users['10XAAAAAB'] = classes.IrcUser('helloworld//unittest', 1234, '10XAAAAAB')
|
|
||||||
# Cut off the nick, not the suffix if the result is too long.
|
|
||||||
self.assertEqual(self.f('helloworld'), 'helloworl///unittest')
|
|
||||||
|
|
||||||
def testNormalizeNickRemovesSlashes(self):
|
|
||||||
self.irc.protoname = "charybdis"
|
|
||||||
try:
|
|
||||||
self.assertEqual(self.f('helloworld'), 'helloworld|unittest')
|
|
||||||
self.assertEqual(self.f('abcde/eJanus'), 'abcde|eJanu|unittest')
|
|
||||||
self.assertEqual(self.f('ObnoxiouslyLongNick'), 'Obnoxiously|unittest')
|
|
||||||
finally:
|
|
||||||
self.irc.protoname = "inspircd"
|
|
@ -1,155 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
sys.path.append(os.getcwd())
|
|
||||||
import unittest
|
|
||||||
import itertools
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
import utils
|
|
||||||
import classes
|
|
||||||
import conf
|
|
||||||
import world
|
|
||||||
|
|
||||||
def dummyf():
|
|
||||||
pass
|
|
||||||
|
|
||||||
class TestUtils(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.irc = classes.FakeIRC('fakeirc', classes.FakeProto, conf.testconf)
|
|
||||||
|
|
||||||
def testTS6UIDGenerator(self):
|
|
||||||
uidgen = utils.TS6UIDGenerator('9PY')
|
|
||||||
self.assertEqual(uidgen.next_uid(), '9PYAAAAAA')
|
|
||||||
self.assertEqual(uidgen.next_uid(), '9PYAAAAAB')
|
|
||||||
|
|
||||||
def test_add_cmd(self):
|
|
||||||
# Without name specified, add_cmd adds a command with the same name
|
|
||||||
# as the function
|
|
||||||
utils.add_cmd(dummyf)
|
|
||||||
utils.add_cmd(dummyf, 'TEST')
|
|
||||||
# All command names should be automatically lowercased.
|
|
||||||
self.assertIn('dummyf', world.commands)
|
|
||||||
self.assertIn('test', world.commands)
|
|
||||||
self.assertNotIn('TEST', world.commands)
|
|
||||||
|
|
||||||
def test_add_hook(self):
|
|
||||||
utils.add_hook(dummyf, 'join')
|
|
||||||
self.assertIn('JOIN', world.hooks)
|
|
||||||
# Command names stored in uppercase.
|
|
||||||
self.assertNotIn('join', world.hooks)
|
|
||||||
self.assertIn(dummyf, world.hooks['JOIN'])
|
|
||||||
|
|
||||||
def testIsNick(self):
|
|
||||||
self.assertFalse(utils.isNick('abcdefgh', nicklen=3))
|
|
||||||
self.assertTrue(utils.isNick('aBcdefgh', nicklen=30))
|
|
||||||
self.assertTrue(utils.isNick('abcdefgh1'))
|
|
||||||
self.assertTrue(utils.isNick('ABC-def'))
|
|
||||||
self.assertFalse(utils.isNick('-_-'))
|
|
||||||
self.assertFalse(utils.isNick(''))
|
|
||||||
self.assertFalse(utils.isNick(' i lost the game'))
|
|
||||||
self.assertFalse(utils.isNick(':aw4t*9e4t84a3t90$&*6'))
|
|
||||||
self.assertFalse(utils.isNick('9PYAAAAAB'))
|
|
||||||
self.assertTrue(utils.isNick('_9PYAAAAAB\\'))
|
|
||||||
|
|
||||||
def testIsChannel(self):
|
|
||||||
self.assertFalse(utils.isChannel(''))
|
|
||||||
self.assertFalse(utils.isChannel('lol'))
|
|
||||||
self.assertTrue(utils.isChannel('#channel'))
|
|
||||||
self.assertTrue(utils.isChannel('##ABCD'))
|
|
||||||
|
|
||||||
def testIsServerName(self):
|
|
||||||
self.assertFalse(utils.isServerName('Invalid'))
|
|
||||||
self.assertTrue(utils.isServerName('services.'))
|
|
||||||
self.assertFalse(utils.isServerName('.s.s.s'))
|
|
||||||
self.assertTrue(utils.isServerName('Hello.world'))
|
|
||||||
self.assertFalse(utils.isServerName(''))
|
|
||||||
self.assertTrue(utils.isServerName('pylink.somenet.local'))
|
|
||||||
self.assertFalse(utils.isServerName(' i lost th.e game'))
|
|
||||||
|
|
||||||
def testJoinModes(self):
|
|
||||||
res = utils.joinModes({('+l', '50'), ('+n', None), ('+t', None)})
|
|
||||||
# Sets are orderless, so the end mode could be scrambled in a number of ways.
|
|
||||||
# Basically, we're looking for a string that looks like '+ntl 50' or '+lnt 50'.
|
|
||||||
possible = ['+%s 50' % ''.join(x) for x in itertools.permutations('lnt', 3)]
|
|
||||||
self.assertIn(res, possible)
|
|
||||||
|
|
||||||
# Without any arguments, make sure there is no trailing space.
|
|
||||||
self.assertEqual(utils.joinModes({('+t', None)}), '+t')
|
|
||||||
|
|
||||||
# The +/- in the mode is not required; if it doesn't exist, assume we're
|
|
||||||
# adding modes always.
|
|
||||||
self.assertEqual(utils.joinModes([('t', None), ('n', None)]), '+tn')
|
|
||||||
|
|
||||||
# An empty query should return just '+'
|
|
||||||
self.assertEqual(utils.joinModes(set()), '+')
|
|
||||||
|
|
||||||
# More complex query now with both + and - modes being set
|
|
||||||
res = utils.joinModes([('+l', '50'), ('-n', None)])
|
|
||||||
self.assertEqual(res, '+l-n 50')
|
|
||||||
|
|
||||||
# If one modepair in the list lacks a +/- prefix, just follow the
|
|
||||||
# previous one's.
|
|
||||||
res = utils.joinModes([('+l', '50'), ('-n', None), ('m', None)])
|
|
||||||
self.assertEqual(res, '+l-nm 50')
|
|
||||||
res = utils.joinModes([('+l', '50'), ('m', None)])
|
|
||||||
self.assertEqual(res, '+lm 50')
|
|
||||||
res = utils.joinModes([('l', '50'), ('-m', None)])
|
|
||||||
self.assertEqual(res, '+l-m 50')
|
|
||||||
|
|
||||||
# Rarely in real life will we get a mode string this complex.
|
|
||||||
# Let's make sure it works, just in case.
|
|
||||||
res = utils.joinModes([('-o', '9PYAAAAAA'), ('+l', '50'), ('-n', None),
|
|
||||||
('-m', None), ('+k', 'hello'),
|
|
||||||
('+b', '*!*@*.badisp.net')])
|
|
||||||
self.assertEqual(res, '-o+l-nm+kb 9PYAAAAAA 50 hello *!*@*.badisp.net')
|
|
||||||
|
|
||||||
def _reverseModes(self, query, expected, target='#PyLink', oldobj=None):
|
|
||||||
res = utils.reverseModes(self.irc, target, query, oldobj=oldobj)
|
|
||||||
self.assertEqual(res, expected)
|
|
||||||
|
|
||||||
def testReverseModes(self):
|
|
||||||
# Initialize the channe, first.
|
|
||||||
utils.applyModes(self.irc, '#PyLink', [])
|
|
||||||
# Strings.
|
|
||||||
self._reverseModes("+mk-t test", "-mk+t test")
|
|
||||||
self._reverseModes("ml-n 111", "-ml+n")
|
|
||||||
# Lists.
|
|
||||||
self._reverseModes([('+m', None), ('+r', None), ('+l', '3')],
|
|
||||||
{('-m', None), ('-r', None), ('-l', None)})
|
|
||||||
# Sets.
|
|
||||||
self._reverseModes({('s', None)}, {('-s', None)})
|
|
||||||
# Combining modes with an initial + and those without
|
|
||||||
self._reverseModes({('s', None), ('+R', None)}, {('-s', None), ('-R', None)})
|
|
||||||
|
|
||||||
def testReverseModesUser(self):
|
|
||||||
self._reverseModes({('+i', None), ('l', 'asfasd')}, {('-i', None), ('-l', 'asfasd')},
|
|
||||||
target=self.irc.pseudoclient.uid)
|
|
||||||
|
|
||||||
def testReverseModesExisting(self):
|
|
||||||
utils.applyModes(self.irc, '#PyLink', [('+m', None), ('+l', '50'), ('+k', 'supersecret'),
|
|
||||||
('+o', '9PYAAAAAA')])
|
|
||||||
|
|
||||||
self._reverseModes({('+i', None), ('+l', '3')}, {('-i', None), ('+l', '50')})
|
|
||||||
self._reverseModes('-n', '+n')
|
|
||||||
self._reverseModes('-l', '+l 50')
|
|
||||||
self._reverseModes('+k derp', '+k supersecret')
|
|
||||||
self._reverseModes('-mk *', '+mk supersecret')
|
|
||||||
|
|
||||||
self.irc.proto.spawnClient("tester2")
|
|
||||||
oldobj = deepcopy(self.irc.channels['#PyLink'])
|
|
||||||
|
|
||||||
# Existing modes are ignored.
|
|
||||||
self._reverseModes([('+t', None)], set())
|
|
||||||
self._reverseModes('+n', '+')
|
|
||||||
#self._reverseModes('+oo 9PYAAAAAB 9PYAAAAAA', '-o 9PYAAAAAB', oldobj=oldobj)
|
|
||||||
self._reverseModes('+o 9PYAAAAAA', '+')
|
|
||||||
self._reverseModes('+vM 9PYAAAAAA', '-M')
|
|
||||||
|
|
||||||
# Ignore unsetting prefixmodes/list modes that were never set.
|
|
||||||
self._reverseModes([('-v', '10XAAAAAA')], set())
|
|
||||||
self._reverseModes('-ob 10XAAAAAA derp!*@*', '+')
|
|
||||||
utils.applyModes(self.irc, '#PyLink', [('+b', '*!user@badisp.tk')])
|
|
||||||
self._reverseModes('-bb *!*@* *!user@badisp.tk', '+b *!user@badisp.tk')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
@ -1,101 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
sys.path += [os.getcwd(), os.path.join(os.getcwd(), 'protocols')]
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import world
|
|
||||||
import utils
|
|
||||||
import classes
|
|
||||||
import conf
|
|
||||||
|
|
||||||
world.started.set()
|
|
||||||
|
|
||||||
class PluginTestCase(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
proto = utils.getProtocolModule(world.testing_ircd)
|
|
||||||
self.irc = classes.FakeIRC('unittest', proto, conf.testconf)
|
|
||||||
self.proto = self.irc.proto
|
|
||||||
self.irc.connect()
|
|
||||||
self.sdata = self.irc.serverdata
|
|
||||||
self.u = self.irc.pseudoclient.uid
|
|
||||||
self.maxDiff = None
|
|
||||||
# Dummy servers/users used in tests below.
|
|
||||||
self.proto.spawnServer('whatever.', sid='10X')
|
|
||||||
for x in range(3):
|
|
||||||
self.proto.spawnClient('user%s' % x, server='10X')
|
|
||||||
|
|
||||||
class CommonProtoTestCase(PluginTestCase):
|
|
||||||
def testJoinClient(self):
|
|
||||||
u = self.u
|
|
||||||
self.proto.join(u, '#Channel')
|
|
||||||
self.assertIn(u, self.irc.channels['#channel'].users)
|
|
||||||
# Non-existant user.
|
|
||||||
self.assertRaises(LookupError, self.proto.join, '9PYZZZZZZ', '#test')
|
|
||||||
|
|
||||||
def testKickClient(self):
|
|
||||||
target = self.proto.spawnClient('soccerball', 'soccerball', 'abcd').uid
|
|
||||||
self.proto.join(target, '#pylink')
|
|
||||||
self.assertIn(self.u, self.irc.channels['#pylink'].users)
|
|
||||||
self.assertIn(target, self.irc.channels['#pylink'].users)
|
|
||||||
self.proto.kick(self.u, '#pylink', target, 'Pow!')
|
|
||||||
self.assertNotIn(target, self.irc.channels['#pylink'].users)
|
|
||||||
|
|
||||||
def testModeClient(self):
|
|
||||||
testuser = self.proto.spawnClient('testcakes')
|
|
||||||
self.irc.takeMsgs()
|
|
||||||
self.proto.mode(self.u, testuser.uid, [('+i', None), ('+w', None)])
|
|
||||||
self.assertEqual({('i', None), ('w', None)}, testuser.modes)
|
|
||||||
|
|
||||||
# Default channels start with +nt
|
|
||||||
self.assertEqual({('n', None), ('t', None)}, self.irc.channels['#pylink'].modes)
|
|
||||||
self.proto.mode(self.u, '#pylink', [('+s', None), ('+l', '30')])
|
|
||||||
self.assertEqual({('s', None), ('l', '30'), ('n', None), ('t', None)}, self.irc.channels['#pylink'].modes)
|
|
||||||
|
|
||||||
cmds = self.irc.takeCommands(self.irc.takeMsgs())
|
|
||||||
self.assertEqual(cmds, ['MODE', 'FMODE'])
|
|
||||||
|
|
||||||
def testNickClient(self):
|
|
||||||
self.proto.nick(self.u, 'NotPyLink')
|
|
||||||
self.assertEqual('NotPyLink', self.irc.users[self.u].nick)
|
|
||||||
|
|
||||||
def testPartClient(self):
|
|
||||||
u = self.u
|
|
||||||
self.proto.join(u, '#channel')
|
|
||||||
self.proto.part(u, '#channel')
|
|
||||||
self.assertNotIn(u, self.irc.channels['#channel'].users)
|
|
||||||
|
|
||||||
def testQuitClient(self):
|
|
||||||
u = self.proto.spawnClient('testuser3', 'moo', 'hello.world').uid
|
|
||||||
self.proto.join(u, '#channel')
|
|
||||||
self.assertRaises(LookupError, self.proto.quit, '9PYZZZZZZ', 'quit reason')
|
|
||||||
self.proto.quit(u, 'quit reason')
|
|
||||||
self.assertNotIn(u, self.irc.channels['#channel'].users)
|
|
||||||
self.assertNotIn(u, self.irc.users)
|
|
||||||
self.assertNotIn(u, self.irc.servers[self.irc.sid].users)
|
|
||||||
|
|
||||||
def testSpawnClient(self):
|
|
||||||
u = self.proto.spawnClient('testuser3', 'moo', 'hello.world').uid
|
|
||||||
# Check the server index and the user index
|
|
||||||
self.assertIn(u, self.irc.servers[self.irc.sid].users)
|
|
||||||
self.assertIn(u, self.irc.users)
|
|
||||||
# Raise ValueError when trying to spawn a client on a server that's not ours
|
|
||||||
self.assertRaises(ValueError, self.proto.spawnClient, 'abcd', 'user', 'dummy.user.net', server='44A')
|
|
||||||
# Unfilled args should get placeholder fields and not error.
|
|
||||||
self.proto.spawnClient('testuser4')
|
|
||||||
|
|
||||||
def testSpawnClientOnServer(self):
|
|
||||||
self.proto.spawnServer('subserver.pylink', '34Q')
|
|
||||||
u = self.proto.spawnClient('person1', 'person', 'users.somenet.local', server='34Q')
|
|
||||||
# We're spawning clients on the right server, hopefully...
|
|
||||||
self.assertIn(u.uid, self.irc.servers['34Q'].users)
|
|
||||||
self.assertNotIn(u.uid, self.irc.servers[self.irc.sid].users)
|
|
||||||
|
|
||||||
def testSpawnServer(self):
|
|
||||||
# Incorrect SID length
|
|
||||||
self.assertRaises(Exception, self.proto.spawnServer, 'subserver.pylink', '34Q0')
|
|
||||||
self.proto.spawnServer('subserver.pylink', '34Q')
|
|
||||||
# Duplicate server name
|
|
||||||
self.assertRaises(Exception, self.proto.spawnServer, 'Subserver.PyLink', '34Z')
|
|
||||||
# Duplicate SID
|
|
||||||
self.assertRaises(Exception, self.proto.spawnServer, 'another.Subserver.PyLink', '34Q')
|
|
||||||
self.assertIn('34Q', self.irc.servers)
|
|
4
update.sh
Executable file
4
update.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Updates a locally installed copy of PyLink and runs it.
|
||||||
|
|
||||||
|
python3 setup.py install --user && pylink
|
141
utils.py
141
utils.py
@ -11,9 +11,10 @@ import importlib
|
|||||||
import os
|
import os
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
from log import log
|
from .log import log
|
||||||
import world
|
from . import world, conf
|
||||||
import conf
|
# This is just so protocols and plugins are importable.
|
||||||
|
from pylinkirc import protocols, plugins
|
||||||
|
|
||||||
class NotAuthenticatedError(Exception):
|
class NotAuthenticatedError(Exception):
|
||||||
"""
|
"""
|
||||||
@ -34,7 +35,7 @@ class IncrementalUIDGenerator():
|
|||||||
"%s by defining self.allowedchars and self.length "
|
"%s by defining self.allowedchars and self.length "
|
||||||
"and then calling super().__init__()." % self.__class__.__name__)
|
"and then calling super().__init__()." % self.__class__.__name__)
|
||||||
self.uidchars = [self.allowedchars[0]]*self.length
|
self.uidchars = [self.allowedchars[0]]*self.length
|
||||||
self.sid = sid
|
self.sid = str(sid)
|
||||||
|
|
||||||
def increment(self, pos=None):
|
def increment(self, pos=None):
|
||||||
"""
|
"""
|
||||||
@ -63,9 +64,9 @@ class IncrementalUIDGenerator():
|
|||||||
self.increment()
|
self.increment()
|
||||||
return uid
|
return uid
|
||||||
|
|
||||||
def add_cmd(func, name=None):
|
def add_cmd(func, name=None, **kwargs):
|
||||||
"""Binds an IRC command function to the given command name."""
|
"""Binds an IRC command function to the given command name."""
|
||||||
world.services['pylink'].add_cmd(func, name=name)
|
world.services['pylink'].add_cmd(func, name=name, **kwargs)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
def add_hook(func, command):
|
def add_hook(func, command):
|
||||||
@ -127,11 +128,17 @@ def loadModuleFromFolder(name, folder):
|
|||||||
m = importlib.machinery.SourceFileLoader(name, fullpath).load_module()
|
m = importlib.machinery.SourceFileLoader(name, fullpath).load_module()
|
||||||
return m
|
return m
|
||||||
|
|
||||||
def getProtocolModule(protoname):
|
def loadPlugin(name):
|
||||||
|
"""
|
||||||
|
Imports and returns the requested plugin.
|
||||||
|
"""
|
||||||
|
return importlib.import_module('pylinkirc.plugins.' + name)
|
||||||
|
|
||||||
|
def getProtocolModule(name):
|
||||||
"""
|
"""
|
||||||
Imports and returns the protocol module requested.
|
Imports and returns the protocol module requested.
|
||||||
"""
|
"""
|
||||||
return loadModuleFromFolder(protoname, world.protocols_folder)
|
return importlib.import_module('pylinkirc.protocols.' + name)
|
||||||
|
|
||||||
def getDatabaseName(dbname):
|
def getDatabaseName(dbname):
|
||||||
"""
|
"""
|
||||||
@ -139,7 +146,7 @@ def getDatabaseName(dbname):
|
|||||||
current PyLink instance.
|
current PyLink instance.
|
||||||
|
|
||||||
This returns '<dbname>.db' if the running config name is PyLink's default
|
This returns '<dbname>.db' if the running config name is PyLink's default
|
||||||
(config.yml), and '<dbname>-<config name>.db' for anything else. For example,
|
(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'."""
|
would return '<dbname>-testing.db'."""
|
||||||
if conf.confname != 'pylink':
|
if conf.confname != 'pylink':
|
||||||
@ -148,8 +155,13 @@ def getDatabaseName(dbname):
|
|||||||
return dbname
|
return dbname
|
||||||
|
|
||||||
class ServiceBot():
|
class ServiceBot():
|
||||||
|
"""
|
||||||
|
PyLink IRC Service class.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, name, default_help=True, default_request=False, default_list=True,
|
def __init__(self, name, default_help=True, default_request=False, default_list=True,
|
||||||
nick=None, ident=None, manipulatable=False):
|
nick=None, ident=None, manipulatable=False, extra_channels=None,
|
||||||
|
desc=None):
|
||||||
# Service name
|
# Service name
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
@ -169,6 +181,16 @@ class ServiceBot():
|
|||||||
# spawned.
|
# spawned.
|
||||||
self.uids = {}
|
self.uids = {}
|
||||||
|
|
||||||
|
# Track what channels other than those defined in the config
|
||||||
|
# that the bot should join by default.
|
||||||
|
self.extra_channels = extra_channels or collections.defaultdict(set)
|
||||||
|
|
||||||
|
# Service description, used in the default help command if one is given.
|
||||||
|
self.desc = desc
|
||||||
|
|
||||||
|
# List of command names to "feature"
|
||||||
|
self.featured_cmds = set()
|
||||||
|
|
||||||
if default_help:
|
if default_help:
|
||||||
self.add_cmd(self.help)
|
self.add_cmd(self.help)
|
||||||
|
|
||||||
@ -180,6 +202,9 @@ class ServiceBot():
|
|||||||
self.add_cmd(self.listcommands, 'list')
|
self.add_cmd(self.listcommands, 'list')
|
||||||
|
|
||||||
def spawn(self, irc=None):
|
def spawn(self, irc=None):
|
||||||
|
"""
|
||||||
|
Spawns instances of this service on all connected networks.
|
||||||
|
"""
|
||||||
# Spawn the new service by calling the PYLINK_NEW_SERVICE hook,
|
# Spawn the new service by calling the PYLINK_NEW_SERVICE hook,
|
||||||
# which is handled by coreplugin.
|
# which is handled by coreplugin.
|
||||||
if irc is None:
|
if irc is None:
|
||||||
@ -188,30 +213,29 @@ class ServiceBot():
|
|||||||
else:
|
else:
|
||||||
raise NotImplementedError("Network specific plugins not supported yet.")
|
raise NotImplementedError("Network specific plugins not supported yet.")
|
||||||
|
|
||||||
def reply(self, irc, text):
|
def reply(self, irc, text, notice=False, private=False):
|
||||||
"""Replies to a message using the right service UID."""
|
"""Replies to a message as the service in question."""
|
||||||
servuid = self.uids.get(irc.name)
|
servuid = self.uids.get(irc.name)
|
||||||
if not servuid:
|
if not servuid:
|
||||||
log.warning("(%s) Possible desync? UID for service %s doesn't exist!", irc.name, self.name)
|
log.warning("(%s) Possible desync? UID for service %s doesn't exist!", irc.name, self.name)
|
||||||
return
|
return
|
||||||
|
|
||||||
irc.reply(text, notice=self.use_notice, source=servuid)
|
irc.reply(text, notice=notice, source=servuid, private=private)
|
||||||
|
|
||||||
def call_cmd(self, irc, source, text, called_by=None, notice=True):
|
def call_cmd(self, irc, source, text, called_in=None):
|
||||||
"""
|
"""
|
||||||
Calls a PyLink bot command. source is the caller's UID, and text is the
|
Calls a PyLink bot command. source is the caller's UID, and text is the
|
||||||
full, unparsed text of the message.
|
full, unparsed text of the message.
|
||||||
"""
|
"""
|
||||||
irc.called_by = called_by or source
|
irc.called_in = called_in or source
|
||||||
|
irc.called_by = source
|
||||||
# Store this globally so other commands don't have to worry about whether
|
|
||||||
# we're preferring notices.
|
|
||||||
self.use_notice = notice
|
|
||||||
|
|
||||||
cmd_args = text.strip().split(' ')
|
cmd_args = text.strip().split(' ')
|
||||||
cmd = cmd_args[0].lower()
|
cmd = cmd_args[0].lower()
|
||||||
cmd_args = cmd_args[1:]
|
cmd_args = cmd_args[1:]
|
||||||
if cmd not in self.commands:
|
if cmd not in self.commands:
|
||||||
|
if not cmd.startswith('\x01'):
|
||||||
|
# Ignore invalid command errors from CTCPs.
|
||||||
self.reply(irc, 'Error: Unknown command %r.' % cmd)
|
self.reply(irc, 'Error: Unknown command %r.' % cmd)
|
||||||
log.info('(%s/%s) Received unknown command %r from %s', irc.name, self.name, cmd, irc.getHostmask(source))
|
log.info('(%s/%s) Received unknown command %r from %s', irc.name, self.name, cmd, irc.getHostmask(source))
|
||||||
return
|
return
|
||||||
@ -226,31 +250,36 @@ class ServiceBot():
|
|||||||
log.exception('Unhandled exception caught in command %r', cmd)
|
log.exception('Unhandled exception caught in command %r', cmd)
|
||||||
self.reply(irc, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
|
self.reply(irc, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
|
||||||
|
|
||||||
def add_cmd(self, func, name=None):
|
def add_cmd(self, func, name=None, featured=False):
|
||||||
"""Binds an IRC command function to the given command name."""
|
"""Binds an IRC command function to the given command name."""
|
||||||
if name is None:
|
if name is None:
|
||||||
name = func.__name__
|
name = func.__name__
|
||||||
name = name.lower()
|
name = name.lower()
|
||||||
|
|
||||||
|
# Mark as a featured command if requested to do so.
|
||||||
|
if featured:
|
||||||
|
self.featured_cmds.add(name)
|
||||||
|
|
||||||
self.commands[name].append(func)
|
self.commands[name].append(func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
def help(self, irc, source, args):
|
def _show_command_help(self, irc, command, private=False, shortform=False):
|
||||||
"""<command>
|
"""
|
||||||
|
Shows help for the given command.
|
||||||
|
"""
|
||||||
|
def _reply(text):
|
||||||
|
"""
|
||||||
|
reply() wrapper to handle the private argument.
|
||||||
|
"""
|
||||||
|
self.reply(irc, text, private=private)
|
||||||
|
|
||||||
Gives help for <command>, if it is available."""
|
|
||||||
try:
|
|
||||||
command = args[0].lower()
|
|
||||||
except IndexError: # No argument given, just return 'list' output
|
|
||||||
self.listcommands(irc, source, args)
|
|
||||||
return
|
|
||||||
if command not in self.commands:
|
if command not in self.commands:
|
||||||
self.reply(irc, 'Error: Unknown command %r.' % command)
|
_reply('Error: Unknown command %r.' % command)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
funcs = self.commands[command]
|
funcs = self.commands[command]
|
||||||
if len(funcs) > 1:
|
if len(funcs) > 1:
|
||||||
self.reply(irc, 'The following \x02%s\x02 plugins bind to the \x02%s\x02 command: %s'
|
_reply('The following \x02%s\x02 plugins bind to the \x02%s\x02 command: %s'
|
||||||
% (len(funcs), command, ', '.join([func.__module__ for func in funcs])))
|
% (len(funcs), command, ', '.join([func.__module__ for func in funcs])))
|
||||||
for func in funcs:
|
for func in funcs:
|
||||||
doc = func.__doc__
|
doc = func.__doc__
|
||||||
@ -260,13 +289,35 @@ class ServiceBot():
|
|||||||
# Bold the first line, which usually just tells you what
|
# Bold the first line, which usually just tells you what
|
||||||
# arguments the command takes.
|
# arguments the command takes.
|
||||||
lines[0] = '\x02%s %s\x02' % (command, lines[0])
|
lines[0] = '\x02%s %s\x02' % (command, lines[0])
|
||||||
for line in lines:
|
|
||||||
# Then, just output the rest of the docstring to IRC.
|
if shortform: # Short form is just the command name + args.
|
||||||
self.reply(irc, line.strip())
|
_reply(lines[0].strip())
|
||||||
else:
|
else:
|
||||||
self.reply(irc, "Error: Command %r doesn't offer any help." % command)
|
for line in lines:
|
||||||
|
# Otherwise, just output the rest of the docstring to IRC.
|
||||||
|
_reply(line.strip())
|
||||||
|
else:
|
||||||
|
_reply("Error: Command %r doesn't offer any help." % command)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def help(self, irc, source, args):
|
||||||
|
"""<command>
|
||||||
|
|
||||||
|
Gives help for <command>, if it is available."""
|
||||||
|
try:
|
||||||
|
command = args[0].lower()
|
||||||
|
except IndexError:
|
||||||
|
# No argument given: show service description (if present), 'list' output, and a list
|
||||||
|
# of featured commands.
|
||||||
|
if self.desc:
|
||||||
|
self.reply(irc, self.desc)
|
||||||
|
self.reply(irc, " ") # Extra newline to unclutter the output text
|
||||||
|
|
||||||
|
self.listcommands(irc, source, args)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self._show_command_help(irc, command)
|
||||||
|
|
||||||
def request(self, irc, source, args):
|
def request(self, irc, source, args):
|
||||||
self.reply(irc, "Request command stub called.")
|
self.reply(irc, "Request command stub called.")
|
||||||
|
|
||||||
@ -278,10 +329,27 @@ class ServiceBot():
|
|||||||
|
|
||||||
Returns a list of available commands this service has to offer."""
|
Returns a list of available commands this service has to offer."""
|
||||||
|
|
||||||
cmds = list(self.commands.keys())
|
# Don't show CTCP handlers in the public command list.
|
||||||
cmds.sort()
|
cmds = sorted([cmd for cmd in self.commands.keys() if '\x01' not in cmd])
|
||||||
|
|
||||||
|
if cmds:
|
||||||
self.reply(irc, 'Available commands include: %s' % ', '.join(cmds))
|
self.reply(irc, 'Available commands include: %s' % ', '.join(cmds))
|
||||||
self.reply(irc, 'To see help on a specific command, type \x02help <command>\x02.')
|
self.reply(irc, 'To see help on a specific command, type \x02help <command>\x02.')
|
||||||
|
else:
|
||||||
|
self.reply(irc, 'This service doesn\'t provide any public commands.')
|
||||||
|
|
||||||
|
# If there are featured commands, list them by showing the help for each.
|
||||||
|
# These definitions are sent in private to prevent flooding in channels.
|
||||||
|
if self.featured_cmds:
|
||||||
|
self.reply(irc, " ", private=True)
|
||||||
|
self.reply(irc, 'Featured commands include:', private=True)
|
||||||
|
for cmd in sorted(self.featured_cmds):
|
||||||
|
if self.commands.get(cmd):
|
||||||
|
# Only show featured commands that are both defined and loaded.
|
||||||
|
# TODO: perhaps plugin unload should remove unused featured command
|
||||||
|
# definitions automatically?
|
||||||
|
self._show_command_help(irc, cmd, private=True, shortform=True)
|
||||||
|
self.reply(irc, 'End of command listing.', private=True)
|
||||||
|
|
||||||
def registerService(name, *args, **kwargs):
|
def registerService(name, *args, **kwargs):
|
||||||
"""Registers a service bot."""
|
"""Registers a service bot."""
|
||||||
@ -296,6 +364,7 @@ def registerService(name, *args, **kwargs):
|
|||||||
def unregisterService(name):
|
def unregisterService(name):
|
||||||
"""Unregisters an existing service bot."""
|
"""Unregisters an existing service bot."""
|
||||||
assert name in world.services, "Unknown service %s" % name
|
assert name in world.services, "Unknown service %s" % name
|
||||||
|
name = name.lower()
|
||||||
sbot = world.services[name]
|
sbot = world.services[name]
|
||||||
for ircnet, uid in sbot.uids.items():
|
for ircnet, uid in sbot.uids.items():
|
||||||
world.networkobjects[ircnet].proto.quit(uid, "Service unloaded.")
|
world.networkobjects[ircnet].proto.quit(uid, "Service unloaded.")
|
||||||
|
26
world.py
26
world.py
@ -4,15 +4,10 @@ world.py: Stores global variables for PyLink, including lists of active IRC obje
|
|||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import threading
|
import threading
|
||||||
import subprocess
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Global variable to indicate whether we're being ran directly, or imported
|
# This indicates whether we're running in tests modes. What it actually does
|
||||||
# for a testcase. This defaults to True.
|
# though is control whether IRC connections should be threaded or not.
|
||||||
testing = True
|
testing = False
|
||||||
|
|
||||||
# Sets the default protocol module to use with tests.
|
|
||||||
testing_ircd = 'inspircd'
|
|
||||||
|
|
||||||
# Statekeeping for our hooks list, IRC objects, loaded plugins, and initialized
|
# Statekeeping for our hooks list, IRC objects, loaded plugins, and initialized
|
||||||
# service bots.
|
# service bots.
|
||||||
@ -21,18 +16,9 @@ networkobjects = {}
|
|||||||
plugins = {}
|
plugins = {}
|
||||||
services = {}
|
services = {}
|
||||||
|
|
||||||
|
# Registered extarget handlers. This maps exttarget names (strings) to handling functions.
|
||||||
|
exttarget_handlers = {}
|
||||||
|
|
||||||
started = threading.Event()
|
started = threading.Event()
|
||||||
|
|
||||||
plugins_folder = os.path.join(os.getcwd(), 'plugins')
|
|
||||||
protocols_folder = os.path.join(os.getcwd(), 'protocols')
|
|
||||||
|
|
||||||
version = "<unknown>"
|
|
||||||
source = "https://github.com/GLolol/PyLink" # CHANGE THIS IF YOU'RE FORKING!!
|
source = "https://github.com/GLolol/PyLink" # CHANGE THIS IF YOU'RE FORKING!!
|
||||||
|
|
||||||
# Only run this once.
|
|
||||||
if version == "<unknown>":
|
|
||||||
# Get version from Git tags.
|
|
||||||
try:
|
|
||||||
version = 'v' + subprocess.check_output(['git', 'describe', '--tags']).decode('utf-8').strip()
|
|
||||||
except Exception as e:
|
|
||||||
print('ERROR: Failed to get version from "git describe --tags": %s: %s' % (type(e).__name__, e))
|
|
||||||
|
Loading…
Reference in New Issue
Block a user