mirror of
https://github.com/jlu5/PyLink.git
synced 2024-11-01 01:09:22 +01:00
Merge commit 'bd755e137ffa034007a77d75fbd00d21e759163e' into wip/logger-module
Conflicts: proto.py
This commit is contained in:
commit
f06bcc7928
362
LICENSE
Normal file
362
LICENSE
Normal file
@ -0,0 +1,362 @@
|
||||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1. "Contributor"
|
||||
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||
Modifications of such Source Code Form, in each case including portions
|
||||
thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described in
|
||||
Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the terms of
|
||||
a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
|
||||
means a work that combines Covered Software with other material, in a
|
||||
separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
|
||||
means having the right to grant, to the maximum extent possible, whether
|
||||
at the time of the initial grant or subsequently, any and all of the
|
||||
rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the License,
|
||||
by the making, using, selling, offering for sale, having made, import,
|
||||
or transfer of either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||
General Public License, Version 2.1, the GNU Affero General Public
|
||||
License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that controls, is
|
||||
controlled by, or is under common control with You. For purposes of this
|
||||
definition, "control" means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||
sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights to
|
||||
grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||
Section 2.1.
|
||||
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code Form,
|
||||
as described in Section 3.1, and You must inform recipients of the
|
||||
Executable Form how they can obtain a copy of such Source Code Form by
|
||||
reasonable means in a timely manner, at a charge no more than the cost
|
||||
of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter the
|
||||
recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty, or
|
||||
limitations of liability) contained within the Source Code Form of the
|
||||
Covered Software, except that You may alter any license notices to the
|
||||
extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute,
|
||||
judicial order, or regulation then You must: (a) comply with the terms of
|
||||
this License to the maximum extent possible; and (b) describe the
|
||||
limitations and the code they affect. Such description must be placed in a
|
||||
text file included with all distributions of the Covered Software under
|
||||
this License. Except to the extent prohibited by statute or regulation,
|
||||
such description must be sufficiently detailed for a recipient of ordinary
|
||||
skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You
|
||||
fail to comply with any of its terms. However, if You become compliant,
|
||||
then the rights granted under this License from a particular Contributor
|
||||
are reinstated (a) provisionally, unless and until such Contributor
|
||||
explicitly and finally terminates Your grants, and (b) on an ongoing
|
||||
basis, if such Contributor fails to notify You of the non-compliance by
|
||||
some reasonable means prior to 60 days after You have come back into
|
||||
compliance. Moreover, Your grants from a particular Contributor are
|
||||
reinstated on an ongoing basis if such Contributor notifies You of the
|
||||
non-compliance by some reasonable means, this is the first time You have
|
||||
received notice of non-compliance with this License from such
|
||||
Contributor, and You become compliant prior to 30 days after Your receipt
|
||||
of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||
license agreements (excluding distributors and resellers) which have been
|
||||
validly granted by You or Your distributors under this License prior to
|
||||
termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an "as is" basis,
|
||||
without warranty of any kind, either expressed, implied, or statutory,
|
||||
including, without limitation, warranties that the Covered Software is free
|
||||
of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
The entire risk as to the quality and performance of the Covered Software
|
||||
is with You. Should any Covered Software prove defective in any respect,
|
||||
You (not any Contributor) assume the cost of any necessary servicing,
|
||||
repair, or correction. This disclaimer of warranty constitutes an essential
|
||||
part of this License. No use of any Covered Software is authorized under
|
||||
this License except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including
|
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||
distributes Covered Software as permitted above, be liable to You for any
|
||||
direct, indirect, special, incidental, or consequential damages of any
|
||||
character including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from
|
||||
such party's negligence to the extent applicable law prohibits such
|
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and limitation may
|
||||
not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts
|
||||
of a jurisdiction where the defendant maintains its principal place of
|
||||
business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing
|
||||
in this Section shall prevent a party's ability to bring cross-claims or
|
||||
counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides that
|
||||
the language of a contract shall be construed against the drafter shall not
|
||||
be used to construe this License against a Contributor.
|
||||
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses If You choose to distribute Source Code Form that is
|
||||
Incompatible With Secondary Licenses under the terms of this version of
|
||||
the License, the notice described in Exhibit B of this License must be
|
||||
attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file,
|
||||
then You may include the notice in a location (such as a LICENSE file in a
|
||||
relevant directory) where a recipient would be likely to look for such a
|
||||
notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible
|
||||
With Secondary Licenses", as defined by
|
||||
the Mozilla Public License, v. 2.0.
|
@ -1,17 +1,20 @@
|
||||
# PyLink
|
||||
|
||||
<img src="https://dl.dropboxusercontent.com/u/18664770/pylink.png" width="75%" height="75%">
|
||||
|
||||
PyLink is an IRC PseudoService written in Python.
|
||||
|
||||
## Dependencies
|
||||
|
||||
PyLink is a serious WIP right now. Dependencies currently include:
|
||||
|
||||
* Python 3.4
|
||||
* InspIRCd 2.0.x: more protocol modules may be implemented in the future...
|
||||
* Python 3.4+
|
||||
* PyYAML (`pip install pyyaml` or `apt-get install python3-yaml`)
|
||||
|
||||
## Usage
|
||||
|
||||
1) Rename `config.yml.example` to `config.yml` and configure your instance there. Of course, most of the options aren't implemented yet!
|
||||
1) Rename `config.yml.example` to `config.yml` and configure your instance there. Not all options are properly implemented yet, and the configuration schema isn't finalized yet.
|
||||
|
||||
2) Run `main.py` from the command line.
|
||||
|
||||
|
54
classes.py
Normal file
54
classes.py
Normal file
@ -0,0 +1,54 @@
|
||||
from collections import defaultdict
|
||||
|
||||
class IrcUser():
|
||||
def __init__(self, nick, ts, uid, ident='null', host='null',
|
||||
realname='PyLink dummy client', realhost='null',
|
||||
ip='0.0.0.0', modes=set()):
|
||||
self.nick = nick
|
||||
self.ts = ts
|
||||
self.uid = uid
|
||||
self.ident = ident
|
||||
self.host = host
|
||||
self.realhost = realhost
|
||||
self.ip = ip
|
||||
self.realname = realname
|
||||
self.modes = modes
|
||||
|
||||
self.identified = False
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
class IrcServer():
|
||||
"""PyLink IRC Server class.
|
||||
|
||||
uplink: The SID of this IrcServer instance's uplink. This is set to None
|
||||
for the main PyLink PseudoServer!
|
||||
name: The name of the server.
|
||||
internal: Whether the server is an internal PyLink PseudoServer.
|
||||
"""
|
||||
def __init__(self, uplink, name, internal=False):
|
||||
self.uplink = uplink
|
||||
self.users = []
|
||||
self.internal = internal
|
||||
self.name = name.lower()
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
class IrcChannel():
|
||||
def __init__(self):
|
||||
self.users = set()
|
||||
self.modes = set()
|
||||
self.prefixmodes = {'ops': set(), 'halfops': set(), 'voices': set(),
|
||||
'owners': set(), 'admins': set()}
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
def removeuser(self, target):
|
||||
for s in self.prefixmodes.values():
|
||||
s.discard(target)
|
||||
self.users.discard(target)
|
||||
|
||||
class ProtocolError(Exception):
|
||||
pass
|
5
conf.py
Normal file
5
conf.py
Normal file
@ -0,0 +1,5 @@
|
||||
import yaml
|
||||
|
||||
with open("config.yml", 'r') as f:
|
||||
global conf
|
||||
conf = yaml.load(f)
|
@ -24,8 +24,10 @@ server:
|
||||
# SID - required for InspIRCd and TS6 based servers. This must be three characters long.
|
||||
# The first char must be a digit [0-9], and the remaining two chars may be letters [A-Z] or digits.
|
||||
sid: "0AL"
|
||||
channel: "#pylink"
|
||||
# Autojoin channels
|
||||
channels: ["#pylink"]
|
||||
protocol: "inspircd"
|
||||
|
||||
# Plugins to load (omit the .py extension)
|
||||
plugins:
|
||||
- hello
|
||||
- commands
|
||||
|
5
log.py
Normal file
5
log.py
Normal file
@ -0,0 +1,5 @@
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s')
|
||||
|
||||
global log
|
||||
log = logging.getLogger()
|
76
main.py
76
main.py
@ -1,48 +1,49 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import yaml
|
||||
import imp
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
import sys
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
import proto
|
||||
|
||||
with open("config.yml", 'r') as f:
|
||||
conf = yaml.load(f)
|
||||
|
||||
logger = logging.getLogger('pylinklogger')
|
||||
# logger.setLevel(getattr(logging, conf['bot']['loglevel']))
|
||||
logger.info('PyLink starting...')
|
||||
|
||||
# if conf['login']['password'] == 'changeme':
|
||||
# print("You have not set the login details correctly! Exiting...")
|
||||
from log import log
|
||||
from conf import conf
|
||||
import classes
|
||||
|
||||
class Irc():
|
||||
def __init__(self):
|
||||
def __init__(self, proto):
|
||||
# Initialize some variables
|
||||
self.socket = socket.socket()
|
||||
self.connected = False
|
||||
self.users = {}
|
||||
self.channels = {}
|
||||
self.name = conf['server']['netname']
|
||||
self.conf = conf
|
||||
# Server, channel, and user indexes to be populated by our protocol module
|
||||
self.servers = {}
|
||||
self.users = {}
|
||||
self.channels = defaultdict(classes.IrcChannel)
|
||||
# Sets flags such as whether to use halfops, etc. The default RFC1459
|
||||
# modes are implied.
|
||||
self.cmodes = {'op': 'o', 'secret': 's', 'private': 'p',
|
||||
'noextmsg': 'n', 'moderated': 'm', 'inviteonly': 'i',
|
||||
'topiclock': 't', 'limit': 'l', 'ban': 'b',
|
||||
'voice': 'v', 'key': 'k'}
|
||||
self.umodes = {'invisible': 'i', 'snomask': 's', 'wallops': 'w',
|
||||
'oper': 'o'}
|
||||
self.maxnicklen = 30
|
||||
|
||||
self.serverdata = conf['server']
|
||||
ip = self.serverdata["ip"]
|
||||
port = self.serverdata["port"]
|
||||
self.sid = self.serverdata["sid"]
|
||||
logger.info("Connecting to network %r on %s:%s" % (self.name, ip, port))
|
||||
log.info("Connecting to network %r on %s:%s" % (self.name, ip, port))
|
||||
|
||||
self.socket = socket.socket()
|
||||
self.socket.connect((ip, port))
|
||||
self.proto = proto
|
||||
proto.connect(self)
|
||||
self.connected = True
|
||||
self.loaded = []
|
||||
self.load_plugins()
|
||||
self.connected = True
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
@ -56,16 +57,16 @@ class Irc():
|
||||
break
|
||||
while '\n' in buf:
|
||||
line, buf = buf.split('\n', 1)
|
||||
logger.debug("<- {}".format(line))
|
||||
log.debug("<- {}".format(line))
|
||||
proto.handle_events(self, line)
|
||||
except socket.error as e:
|
||||
logger.error('Received socket.error: %s, exiting.' % str(e))
|
||||
log.error('Received socket.error: %s, exiting.' % str(e))
|
||||
break
|
||||
sys.exit(1)
|
||||
|
||||
def send(self, data):
|
||||
data = data.encode("utf-8") + b"\n"
|
||||
logger.debug("-> {}".format(data.decode("utf-8").strip("\n")))
|
||||
log.debug("-> {}".format(data.decode("utf-8").strip("\n")))
|
||||
self.socket.send(data)
|
||||
|
||||
def load_plugins(self):
|
||||
@ -74,9 +75,32 @@ class Irc():
|
||||
# Here, we override the module lookup and import the plugins
|
||||
# dynamically depending on which were configured.
|
||||
for plugin in to_load:
|
||||
moduleinfo = imp.find_module(plugin, plugins_folder)
|
||||
self.loaded.append(imp.load_source(plugin, moduleinfo[1]))
|
||||
logger.info("loaded plugins: %s" % self.loaded)
|
||||
try:
|
||||
moduleinfo = imp.find_module(plugin, plugins_folder)
|
||||
self.loaded.append(imp.load_source(plugin, moduleinfo[1]))
|
||||
except ImportError as e:
|
||||
if str(e).startswith('No module named'):
|
||||
log.error('Failed to load plugin %r: the plugin could not be found.' % plugin)
|
||||
else:
|
||||
log.error('Failed to load plugin %r: import error %s' % (plugin, str(e)))
|
||||
print("loaded plugins: %s" % self.loaded)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('PyLink starting...')
|
||||
if conf['login']['password'] == 'changeme':
|
||||
print("You have not set the login details correctly! Exiting...")
|
||||
sys.exit(2)
|
||||
|
||||
irc_obj = Irc()
|
||||
protoname = conf['server']['protocol']
|
||||
protocols_folder = [os.path.join(os.getcwd(), 'protocols')]
|
||||
try:
|
||||
moduleinfo = imp.find_module(protoname, protocols_folder)
|
||||
proto = imp.load_source(protoname, moduleinfo[1])
|
||||
except ImportError as e:
|
||||
if str(e).startswith('No module named'):
|
||||
log.critical('Failed to load protocol module %r: the file could not be found.' % protoname)
|
||||
else:
|
||||
log.critical('Failed to load protocol module: import error %s' % (protoname, str(e)))
|
||||
sys.exit(2)
|
||||
else:
|
||||
irc_obj = Irc(proto)
|
||||
|
143
plugins/admin.py
Normal file
143
plugins/admin.py
Normal file
@ -0,0 +1,143 @@
|
||||
# admin.py: PyLink administrative commands
|
||||
import sys, os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import utils
|
||||
|
||||
class NotAuthenticatedError(Exception):
|
||||
pass
|
||||
|
||||
def checkauthenticated(irc, source):
|
||||
if not irc.users[source].identified:
|
||||
raise NotAuthenticatedError("You are not authenticated!")
|
||||
|
||||
def _exec(irc, source, args):
|
||||
checkauthenticated(irc, source)
|
||||
args = ' '.join(args)
|
||||
if not args.strip():
|
||||
utils.msg(irc, source, 'No code entered!')
|
||||
return
|
||||
exec(args, globals(), locals())
|
||||
utils.add_cmd(_exec, 'exec')
|
||||
|
||||
@utils.add_cmd
|
||||
def spawnclient(irc, source, args):
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
nick, ident, host = args[:3]
|
||||
except ValueError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 3: nick, user, host.")
|
||||
return
|
||||
irc.proto.spawnClient(irc, nick, ident, host)
|
||||
|
||||
@utils.add_cmd
|
||||
def quitclient(irc, source, args):
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
nick = args[0]
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 1: nick.")
|
||||
return
|
||||
if irc.pseudoclient.uid == utils.nickToUid(irc, nick):
|
||||
utils.msg(irc, source, "Error: cannot quit the main PyLink PseudoClient!")
|
||||
return
|
||||
u = utils.nickToUid(irc, nick)
|
||||
quitmsg = ' '.join(args[1:]) or 'Client quit'
|
||||
irc.proto.quitClient(irc, u, quitmsg)
|
||||
|
||||
@utils.add_cmd
|
||||
def joinclient(irc, source, args):
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
nick = args[0]
|
||||
clist = args[1].split(',')
|
||||
if not clist:
|
||||
raise IndexError
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 2: nick, comma separated list of channels.")
|
||||
return
|
||||
u = utils.nickToUid(irc, nick)
|
||||
for channel in clist:
|
||||
if not channel.startswith('#'):
|
||||
utils.msg(irc, source, "Error: channel names must start with #.")
|
||||
return
|
||||
irc.proto.joinClient(irc, u, channel)
|
||||
|
||||
@utils.add_cmd
|
||||
def nickclient(irc, source, args):
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
nick = args[0]
|
||||
newnick = args[1]
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 2: nick, newnick.")
|
||||
return
|
||||
u = utils.nickToUid(irc, nick)
|
||||
irc.proto.nickClient(irc, u, newnick)
|
||||
|
||||
@utils.add_cmd
|
||||
def partclient(irc, source, args):
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
nick = args[0]
|
||||
clist = args[1].split(',')
|
||||
reason = ' '.join(args[2:])
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 2: nick, comma separated list of channels.")
|
||||
return
|
||||
u = utils.nickToUid(irc, nick)
|
||||
for channel in clist:
|
||||
if not channel.startswith('#'):
|
||||
utils.msg(irc, source, "Error: channel names must start with #.")
|
||||
return
|
||||
irc.proto.partClient(irc, u, channel, reason)
|
||||
|
||||
@utils.add_cmd
|
||||
def kickclient(irc, source, args):
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
nick = args[0]
|
||||
channel = args[1]
|
||||
target = args[2]
|
||||
reason = ' '.join(args[3:])
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 3-4: nick, channel, target, reason (optional).")
|
||||
return
|
||||
u = utils.nickToUid(irc, nick)
|
||||
targetu = utils.nickToUid(irc, target)
|
||||
if not channel.startswith('#'):
|
||||
utils.msg(irc, source, "Error: channel names must start with #.")
|
||||
return
|
||||
irc.proto.kickClient(irc, u, channel, targetu, reason)
|
||||
|
||||
@utils.add_cmd
|
||||
def showuser(irc, source, args):
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
target = args[0]
|
||||
except IndexError:
|
||||
utils.msg(irc, source, "Error: not enough arguments. Needs 1: nick.")
|
||||
return
|
||||
u = utils.nickToUid(irc, target)
|
||||
if u is None:
|
||||
utils.msg(irc, source, 'Error: unknown user %r' % target)
|
||||
return
|
||||
s = ['\x02%s\x02: %s' % (k, v) for k, v in irc.users[u].__dict__.items()]
|
||||
s = 'Information on user %s: %s' % (target, '; '.join(s))
|
||||
utils.msg(irc, source, s)
|
||||
|
||||
@utils.add_cmd
|
||||
def tell(irc, source, args):
|
||||
checkauthenticated(irc, source)
|
||||
try:
|
||||
source, target, text = args[0], args[1], ' '.join(args[2:])
|
||||
except IndexError:
|
||||
utils.msg(irc, source, 'Error: not enough arguments.')
|
||||
return
|
||||
targetuid = utils.nickToUid(irc, target)
|
||||
if targetuid is None:
|
||||
utils.msg(irc, source, 'Error: unknown user %r' % target)
|
||||
return
|
||||
if not text:
|
||||
utils.msg(irc, source, "Error: can't send an empty message!")
|
||||
return
|
||||
utils.msg(irc, target, text, notice=True)
|
@ -4,24 +4,40 @@ import os
|
||||
import logging
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import proto
|
||||
import utils
|
||||
from conf import conf
|
||||
|
||||
logger = logging.getLogger('pylinklogger')
|
||||
|
||||
@proto.add_cmd
|
||||
def tell(irc, source, args):
|
||||
try:
|
||||
target, text = args[0], ' '.join(args[1:])
|
||||
except IndexError:
|
||||
proto._sendFromUser(irc, 'PRIVMSG %s :Error: not enough arguments' % source)
|
||||
return
|
||||
try:
|
||||
proto._sendFromUser(irc, 'NOTICE %s :%s' % (irc.users[target], text))
|
||||
except KeyError:
|
||||
proto._sendFromUser(irc, 'PRIVMSG %s :unknown user %r' % (source, target))
|
||||
|
||||
@proto.add_cmd
|
||||
@utils.add_cmd
|
||||
def debug(irc, source, args):
|
||||
proto._sendFromUser(irc, 'NOTICE %s :Debug info printed to console.' % (source))
|
||||
logger.debug(irc.users)
|
||||
logger.debug(irc.servers)
|
||||
print('user index: %s' % irc.users)
|
||||
print('server index: %s' % irc.servers)
|
||||
print('channels index: %s' % irc.channels)
|
||||
utils.msg(irc, source, 'Debug info printed to console.')
|
||||
|
||||
@utils.add_cmd
|
||||
def status(irc, source, args):
|
||||
identified = irc.users[source].identified
|
||||
if identified:
|
||||
utils.msg(irc, source, 'You are identified as %s.' % identified)
|
||||
else:
|
||||
utils.msg(irc, source, 'You are not identified as anyone.')
|
||||
|
||||
@utils.add_cmd
|
||||
def identify(irc, source, args):
|
||||
try:
|
||||
username, password = args[0], args[1]
|
||||
except IndexError:
|
||||
utils.msg(irc, source, 'Error: not enough arguments.')
|
||||
return
|
||||
if username.lower() == conf['login']['user'].lower() and password == conf['login']['password']:
|
||||
realuser = conf['login']['user']
|
||||
irc.users[source].identified = realuser
|
||||
utils.msg(irc, source, 'Successfully logged in as %s.' % realuser)
|
||||
else:
|
||||
utils.msg(irc, source, 'Incorrect credentials.')
|
||||
|
||||
def listcommands(irc, source, args):
|
||||
cmds = list(utils.bot_commands.keys())
|
||||
cmds.sort()
|
||||
utils.msg(irc, source, 'Available commands include: %s' % ', '.join(cmds))
|
||||
utils.add_cmd(listcommands, 'list')
|
||||
|
@ -1,7 +0,0 @@
|
||||
import sys, os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import proto
|
||||
|
||||
@proto.add_cmd
|
||||
def hello(irc, source, args):
|
||||
proto._sendFromUser(irc, 'PRIVMSG %s :hello!' % source)
|
18
plugins/hooks.py
Normal file
18
plugins/hooks.py
Normal file
@ -0,0 +1,18 @@
|
||||
# hooks.py: test of PyLink hooks
|
||||
import sys, os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import utils
|
||||
|
||||
def hook_join(irc, source, command, args):
|
||||
channel = args['channel']
|
||||
users = args['users']
|
||||
print('%s joined channel %s (JOIN hook caught)' % (users, channel))
|
||||
utils.add_hook(hook_join, 'JOIN')
|
||||
|
||||
def hook_privmsg(irc, source, command, args):
|
||||
channel = args['target']
|
||||
text = args['text']
|
||||
if utils.isChannel(channel) and irc.pseudoclient.nick in text:
|
||||
utils.msg(irc, channel, 'hi there!')
|
||||
print('%s said my name on channel %s (PRIVMSG hook caught)' % (source, channel))
|
||||
utils.add_hook(hook_privmsg, 'PRIVMSG')
|
213
proto.py
213
proto.py
@ -1,213 +0,0 @@
|
||||
import socket
|
||||
import time
|
||||
import sys
|
||||
from utils import *
|
||||
import logging
|
||||
from copy import copy
|
||||
|
||||
logger = logging.getLogger('pylinklogger')
|
||||
|
||||
global bot_commands
|
||||
# This should be a mapping of command names to functions
|
||||
bot_commands = {}
|
||||
|
||||
class IrcUser():
|
||||
def __init__(self, nick, ts, uid, ident='null', host='null',
|
||||
realname='PyLink dummy client', realhost='null',
|
||||
ip='0.0.0.0'):
|
||||
self.nick = nick
|
||||
self.ts = ts
|
||||
self.uid = uid
|
||||
self.ident = ident
|
||||
self.host = host
|
||||
self.realhost = realhost
|
||||
self.ip = ip
|
||||
self.realname = realname
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
class IrcServer():
|
||||
def __init__(self, uplink):
|
||||
self.uplink = uplink
|
||||
self.users = []
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
def _sendFromServer(irc, msg):
|
||||
irc.send(':%s %s' % (irc.sid, msg))
|
||||
|
||||
def _sendFromUser(irc, msg, user=None):
|
||||
if user is None:
|
||||
user = irc.pseudoclient.uid
|
||||
irc.send(':%s %s' % (user, msg))
|
||||
|
||||
def _join(irc, channel):
|
||||
_sendFromUser(irc, "JOIN {channel} {ts} +nt :,{uid}".format(sid=irc.sid,
|
||||
ts=int(time.time()), uid=irc.pseudoclient.uid, channel=channel))
|
||||
|
||||
def _nicktoUid(irc, nick):
|
||||
for k, v in irc.users.items():
|
||||
if v.nick == nick:
|
||||
return k
|
||||
|
||||
def connect(irc):
|
||||
ts = int(time.time())
|
||||
host = irc.serverdata["hostname"]
|
||||
uid = next_uid(irc.sid)
|
||||
irc.pseudoclient = IrcUser('PyLink', ts, uid, 'pylink', host,
|
||||
'PyLink Client')
|
||||
irc.users[uid] = irc.pseudoclient
|
||||
|
||||
f = irc.send
|
||||
f('CAPAB START 1203')
|
||||
# This is hard coded atm... We should fix it eventually...
|
||||
f('CAPAB CAPABILITIES :NICKMAX=32 HALFOP=0 CHANMAX=65 MAXMODES=20'
|
||||
' IDENTMAX=12 MAXQUIT=255 PROTOCOL=1203')
|
||||
f('CAPAB END')
|
||||
# TODO: check recvpass here
|
||||
f('SERVER {host} {Pass} 0 {sid} :PyLink Service'.format(host=host,
|
||||
Pass=irc.serverdata["sendpass"], sid=irc.sid))
|
||||
f(':%s BURST %s' % (irc.sid, ts))
|
||||
# InspIRCd documentation:
|
||||
# :751 UID 751AAAAAA 1220196319 Brain brainwave.brainbox.cc
|
||||
# netadmin.chatspike.net brain 192.168.1.10 1220196324 +Siosw
|
||||
# +ACKNOQcdfgklnoqtx :Craig Edwards
|
||||
f(":{sid} UID {uid} {ts} PyLink {host} {host} pylink 127.0.0.1 {ts} +o +"
|
||||
" :PyLink Client".format(sid=irc.sid, ts=ts,
|
||||
host=host,
|
||||
uid=uid))
|
||||
f(':%s ENDBURST' % (irc.sid))
|
||||
_join(irc, irc.serverdata["channel"])
|
||||
|
||||
# :7NU PING 7NU 0AL
|
||||
def handle_ping(irc, servernumeric, command, args):
|
||||
if args[1] == irc.sid:
|
||||
_sendFromServer(irc, 'PONG %s' % args[1])
|
||||
|
||||
def handle_privmsg(irc, source, command, args):
|
||||
prefix = irc.conf['bot']['prefix']
|
||||
if args[0] == irc.pseudoclient.uid:
|
||||
cmd_args = args[1].split(' ')
|
||||
cmd = cmd_args[0]
|
||||
try:
|
||||
cmd_args = cmd_args[1:]
|
||||
except IndexError:
|
||||
cmd_args = []
|
||||
try:
|
||||
bot_commands[cmd](irc, source, cmd_args)
|
||||
except KeyError:
|
||||
_sendFromUser(irc, 'PRIVMSG %s :unknown command %r' % (source, cmd))
|
||||
|
||||
def handle_error(irc, numeric, command, args):
|
||||
logger.error('Received an ERROR, killing!')
|
||||
irc.connected = False
|
||||
sys.exit(1)
|
||||
|
||||
def handle_fjoin(irc, servernumeric, command, args):
|
||||
# :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :o,1SRAABIT4 v,1IOAAF53R <...>
|
||||
channel = args[0]
|
||||
# tl;dr InspIRCd sends each user's channel data in the form of 'modeprefix(es),UID'
|
||||
# We'll save each user in this format too, at least for now.
|
||||
users = args[-1].split()
|
||||
users = [x.split(',') for x in users]
|
||||
|
||||
'''
|
||||
if channel not in irc.channels.keys():
|
||||
irc.channels[channel]['users'] = users
|
||||
else:
|
||||
old_users = irc.channels[channel]['users'].copy()
|
||||
old_users.update(users)
|
||||
'''
|
||||
|
||||
def handle_uid(irc, numeric, command, args):
|
||||
# :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]
|
||||
realname = args[-1]
|
||||
irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip)
|
||||
irc.servers[numeric].users.append(uid)
|
||||
|
||||
def handle_quit(irc, numeric, command, args):
|
||||
# :1SRAAGB4T QUIT :Quit: quit message goes here
|
||||
del irc.users[numeric]
|
||||
sid = numeric[:3]
|
||||
irc.servers[sid].users.remove(numeric)
|
||||
'''
|
||||
for k, v in irc.channels.items():
|
||||
try:
|
||||
del irc.channels[k][users][v]
|
||||
except KeyError:
|
||||
pass
|
||||
'''
|
||||
|
||||
def handle_burst(irc, numeric, command, args):
|
||||
# :70M BURST 1433044587
|
||||
irc.servers[numeric] = IrcServer(None)
|
||||
|
||||
def handle_server(irc, numeric, command, args):
|
||||
# :70M SERVER millennium.overdrive.pw * 1 1ML :a relatively long period of time... (Fremont, California)
|
||||
servername = args[0]
|
||||
sid = args[3]
|
||||
irc.servers[sid] = IrcServer(numeric)
|
||||
|
||||
def handle_nick(irc, numeric, command, args):
|
||||
newnick = args[0]
|
||||
irc.users[numeric].nick = newnick
|
||||
|
||||
def handle_squit(irc, numeric, command, args):
|
||||
# :70M SQUIT 1ML :Server quit by GL!gl@0::1
|
||||
split_server = args[0]
|
||||
logger.info('Splitting server %s' % split_server)
|
||||
# Prevent RuntimeError: dictionary changed size during iteration
|
||||
old_servers = copy(irc.servers)
|
||||
for sid, data in old_servers.items():
|
||||
if data.uplink == split_server:
|
||||
logger.info('Server %s also hosts server %s, splitting that too...' % (split_server, sid))
|
||||
handle_squit(irc, sid, 'SQUIT', [sid, "PyLink: Automatically splitting leaf servers of %s" % sid])
|
||||
for user in irc.servers[split_server].users:
|
||||
logger.debug("Removing user %s from server %s" % (user, split_server))
|
||||
del irc.users[user]
|
||||
del irc.servers[split_server]
|
||||
|
||||
def handle_events(irc, data):
|
||||
# Each server message looks something like this:
|
||||
# :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :v,1SRAAESWE
|
||||
# :<sid> <command> <argument1> <argument2> ... :final multi word argument
|
||||
try:
|
||||
args = data.split()
|
||||
real_args = []
|
||||
for arg in args:
|
||||
real_args.append(arg)
|
||||
# If the argument starts with ':' and ISN'T the first argument.
|
||||
# The first argument is used for denoting the source UID/SID.
|
||||
if arg.startswith(':') and args.index(arg) != 0:
|
||||
# : is used for multi-word arguments that last until the end
|
||||
# of the message. We can use list splicing here to turn them all
|
||||
# into one argument.
|
||||
index = args.index(arg) # Get the array index of the multi-word arg
|
||||
# Set the last arg to a joined version of the remaining args
|
||||
arg = args[index:]
|
||||
arg = ' '.join(arg)[1:]
|
||||
# Cut the original argument list right before the multi-word arg,
|
||||
# and then append the multi-word arg.
|
||||
real_args = args[:index]
|
||||
real_args.append(arg)
|
||||
break
|
||||
real_args[0] = real_args[0].split(':', 1)[1]
|
||||
args = real_args
|
||||
|
||||
numeric = args[0]
|
||||
command = args[1]
|
||||
args = args[2:]
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
# We will do wildcard event handling here. Unhandled events are just ignored, yay!
|
||||
try:
|
||||
func = globals()['handle_'+command.lower()]
|
||||
func(irc, numeric, command, args)
|
||||
except KeyError: # unhandled event
|
||||
pass
|
||||
|
||||
def add_cmd(func):
|
||||
bot_commands[func.__name__.lower()] = func
|
447
protocols/inspircd.py
Normal file
447
protocols/inspircd.py
Normal file
@ -0,0 +1,447 @@
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import re
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import utils
|
||||
from copy import copy
|
||||
from log import log
|
||||
|
||||
from classes import *
|
||||
|
||||
# Raw commands sent from servers vary from protocol to protocol. Here, we map
|
||||
# non-standard names to our hook handlers, so plugins get the information they need.
|
||||
hook_map = {'FJOIN': 'JOIN', 'SAVE': 'NICK',
|
||||
'RSQUIT': 'SQUIT', 'FMODE': 'MODE'}
|
||||
|
||||
def _sendFromServer(irc, sid, msg):
|
||||
irc.send(':%s %s' % (sid, msg))
|
||||
|
||||
def _sendFromUser(irc, numeric, msg):
|
||||
irc.send(':%s %s' % (numeric, msg))
|
||||
|
||||
def spawnClient(irc, nick, ident, host, modes=[], server=None, *args):
|
||||
server = server or irc.sid
|
||||
if not utils.isInternalServer(irc, server):
|
||||
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % server)
|
||||
# We need a separate UID generator instance for every PseudoServer
|
||||
# we spawn. Otherwise, things won't wrap around properly.
|
||||
if server not in irc.uidgen:
|
||||
irc.uidgen[server] = utils.TS6UIDGenerator(server)
|
||||
uid = irc.uidgen[server].next_uid()
|
||||
ts = int(time.time())
|
||||
if modes:
|
||||
modes = utils.joinModes(modes)
|
||||
else:
|
||||
modes = '+'
|
||||
if not utils.isNick(nick):
|
||||
raise ValueError('Invalid nickname %r.' % nick)
|
||||
_sendFromServer(irc, server, "UID {uid} {ts} {nick} {host} {host} {ident} 0.0.0.0 "
|
||||
"{ts} {modes} + :PyLink Client".format(ts=ts, host=host,
|
||||
nick=nick, ident=ident, uid=uid,
|
||||
modes=modes))
|
||||
u = irc.users[uid] = IrcUser(nick, ts, uid, ident, host, *args)
|
||||
irc.servers[server].users.append(uid)
|
||||
return u
|
||||
|
||||
def joinClient(irc, client, channel):
|
||||
channel = channel.lower()
|
||||
server = utils.isInternalClient(irc, client)
|
||||
if not server:
|
||||
raise LookupError('No such PyLink PseudoClient exists.')
|
||||
if not utils.isChannel(channel):
|
||||
raise ValueError('Invalid channel name %r.' % channel)
|
||||
# One channel per line here!
|
||||
_sendFromServer(irc, server, "FJOIN {channel} {ts} + :,{uid}".format(
|
||||
ts=int(time.time()), uid=client, channel=channel))
|
||||
irc.channels[channel].users.add(client)
|
||||
|
||||
def partClient(irc, client, channel, reason=None):
|
||||
channel = channel.lower()
|
||||
if not utils.isInternalClient(irc, client):
|
||||
raise LookupError('No such PyLink PseudoClient exists.')
|
||||
msg = "PART %s" % channel
|
||||
if not utils.isChannel(channel):
|
||||
raise ValueError('Invalid channel name %r.' % channel)
|
||||
if reason:
|
||||
msg += " :%s" % reason
|
||||
_sendFromUser(irc, client, msg)
|
||||
handle_part(irc, client, 'PART', [channel])
|
||||
|
||||
def removeClient(irc, numeric):
|
||||
"""<irc object> <client numeric>
|
||||
|
||||
Removes a client from our internal databases, regardless
|
||||
of whether it's one of our pseudoclients or not."""
|
||||
for v in irc.channels.values():
|
||||
v.removeuser(numeric)
|
||||
sid = numeric[:3]
|
||||
print('Removing client %s from irc.users' % numeric)
|
||||
del irc.users[numeric]
|
||||
print('Removing client %s from irc.servers[%s]' % (numeric, sid))
|
||||
irc.servers[sid].users.remove(numeric)
|
||||
|
||||
def quitClient(irc, numeric, reason):
|
||||
"""<irc object> <client numeric>
|
||||
|
||||
Quits a PyLink PseudoClient."""
|
||||
if utils.isInternalClient(irc, numeric):
|
||||
_sendFromUser(irc, numeric, "QUIT :%s" % reason)
|
||||
removeClient(irc, numeric)
|
||||
else:
|
||||
raise LookupError("No such PyLink PseudoClient exists. If you're trying to remove "
|
||||
"a user that's not a PyLink PseudoClient from "
|
||||
"the internal state, use removeClient() instead.")
|
||||
|
||||
def kickClient(irc, numeric, channel, target, reason=None):
|
||||
"""<irc object> <kicker client numeric>
|
||||
|
||||
Sends a kick from a PyLink PseudoClient."""
|
||||
channel = channel.lower()
|
||||
if not utils.isInternalClient(irc, numeric):
|
||||
raise LookupError('No such PyLink PseudoClient exists.')
|
||||
if not reason:
|
||||
reason = 'No reason given'
|
||||
_sendFromUser(irc, numeric, 'KICK %s %s :%s' % (channel, target, reason))
|
||||
# We can pretend the target left by its own will; all we really care about
|
||||
# is that the target gets removed from the channel userlist, and calling
|
||||
# handle_part() does that just fine.
|
||||
handle_part(irc, target, 'KICK', [channel])
|
||||
|
||||
def nickClient(irc, numeric, newnick):
|
||||
"""<irc object> <client numeric> <new nickname>
|
||||
|
||||
Changes the nick of a PyLink PseudoClient."""
|
||||
if not utils.isInternalClient(irc, numeric):
|
||||
raise LookupError('No such PyLink PseudoClient exists.')
|
||||
if not utils.isNick(newnick):
|
||||
raise ValueError('Invalid nickname %r.' % nick)
|
||||
_sendFromUser(irc, numeric, 'NICK %s %s' % (newnick, int(time.time())))
|
||||
irc.users[numeric].nick = newnick
|
||||
|
||||
def connect(irc):
|
||||
irc.start_ts = ts = int(time.time())
|
||||
irc.uidgen = {}
|
||||
host = irc.serverdata["hostname"]
|
||||
irc.servers[irc.sid] = IrcServer(None, host, internal=True)
|
||||
|
||||
f = irc.send
|
||||
f('CAPAB START 1202')
|
||||
f('CAPAB CAPABILITIES :PROTOCOL=1202')
|
||||
f('CAPAB END')
|
||||
f('SERVER {host} {Pass} 0 {sid} :PyLink Service'.format(host=host,
|
||||
Pass=irc.serverdata["sendpass"], sid=irc.sid))
|
||||
f(':%s BURST %s' % (irc.sid, ts))
|
||||
irc.pseudoclient = spawnClient(irc, 'PyLink', 'pylink', host, modes=set([("+o", None)]))
|
||||
f(':%s ENDBURST' % (irc.sid))
|
||||
for chan in irc.serverdata['channels']:
|
||||
joinClient(irc, irc.pseudoclient.uid, chan)
|
||||
|
||||
def handle_ping(irc, source, command, args):
|
||||
# <- :70M PING 70M 0AL
|
||||
# -> :0AL PONG 0AL 70M
|
||||
if utils.isInternalServer(irc, args[1]):
|
||||
_sendFromServer(irc, args[1], 'PONG %s %s' % (args[1], source))
|
||||
|
||||
def handle_privmsg(irc, source, command, args):
|
||||
prefix = irc.conf['bot']['prefix']
|
||||
if args[0] == irc.pseudoclient.uid:
|
||||
cmd_args = args[1].split(' ')
|
||||
cmd = cmd_args[0].lower()
|
||||
try:
|
||||
cmd_args = cmd_args[1:]
|
||||
except IndexError:
|
||||
cmd_args = []
|
||||
try:
|
||||
func = utils.bot_commands[cmd]
|
||||
except KeyError:
|
||||
utils.msg(irc, source, 'Unknown command %r.' % cmd)
|
||||
return
|
||||
try:
|
||||
func(irc, source, cmd_args)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
utils.msg(irc, source, 'Uncaught exception in command %r: %s: %s' % (cmd, type(e).__name__, str(e)))
|
||||
return
|
||||
return {'target': args[0], 'text': args[1]}
|
||||
|
||||
def handle_kill(irc, source, command, args):
|
||||
killed = args[0]
|
||||
removeClient(irc, killed)
|
||||
if killed == irc.pseudoclient.uid:
|
||||
irc.pseudoclient = spawnClient(irc, 'PyLink', 'pylink', irc.serverdata["hostname"])
|
||||
for chan in irc.serverdata['channels']:
|
||||
joinClient(irc, irc.pseudoclient.uid, chan)
|
||||
return {'target': killed, 'reason': args[1]}
|
||||
|
||||
def handle_kick(irc, source, command, args):
|
||||
# :70MAAAAAA KICK #endlessvoid 70MAAAAAA :some reason
|
||||
channel = args[0].lower()
|
||||
kicked = args[1]
|
||||
handle_part(irc, kicked, 'KICK', [channel, args[2]])
|
||||
if kicked == irc.pseudoclient.uid:
|
||||
joinClient(irc, irc.pseudoclient.uid, channel)
|
||||
return {'channel': channel, 'target': kicked, 'reason': args[2]}
|
||||
|
||||
def handle_part(irc, source, command, args):
|
||||
channel = args[0].lower()
|
||||
# We should only get PART commands for channels that exist, right??
|
||||
irc.channels[channel].removeuser(source)
|
||||
try:
|
||||
reason = args[1]
|
||||
except IndexError:
|
||||
reason = ''
|
||||
return {'channel': channel, 'reason': reason}
|
||||
|
||||
def handle_error(irc, numeric, command, args):
|
||||
irc.connected = False
|
||||
raise ProtocolError('Received an ERROR, killing!')
|
||||
|
||||
def handle_fjoin(irc, servernumeric, command, args):
|
||||
# :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :o,1SRAABIT4 v,1IOAAF53R <...>
|
||||
channel = args[0].lower()
|
||||
# InspIRCd sends each user's channel data in the form of 'modeprefix(es),UID'
|
||||
userlist = args[-1].split()
|
||||
ts = args[1]
|
||||
modestring = args[2:-1] or args[2]
|
||||
utils.applyModes(irc, channel, utils.parseModes(irc, channel, modestring))
|
||||
namelist = []
|
||||
for user in userlist:
|
||||
modeprefix, user = user.split(',', 1)
|
||||
namelist.append(user)
|
||||
utils.applyModes(irc, channel, [('+%s' % mode, user) for mode in modeprefix])
|
||||
irc.channels[channel].users.add(user)
|
||||
return {'channel': channel, 'users': namelist}
|
||||
|
||||
def handle_uid(irc, numeric, command, args):
|
||||
# :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]
|
||||
realname = args[-1]
|
||||
irc.users[uid] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip)
|
||||
parsedmodes = utils.parseModes(irc, uid, [args[8], args[9]])
|
||||
print('Applying modes %s for %s' % (parsedmodes, uid))
|
||||
utils.applyModes(irc, uid, parsedmodes)
|
||||
irc.servers[numeric].users.append(uid)
|
||||
return {'uid': uid, 'ts': ts, 'nick': nick, 'realhost': realhost, 'host': host, 'ident': ident, 'ip': ip}
|
||||
|
||||
def handle_quit(irc, numeric, command, args):
|
||||
# <- :1SRAAGB4T QUIT :Quit: quit message goes here
|
||||
removeClient(irc, numeric)
|
||||
return {'reason': args[0]}
|
||||
|
||||
def handle_burst(irc, numeric, command, args):
|
||||
# BURST is sent by our uplink when we link.
|
||||
# <- :70M BURST 1433044587
|
||||
|
||||
# This is handled in handle_events, since our uplink
|
||||
# only sends its name in the initial authentication phase,
|
||||
# not in any following BURST commands.
|
||||
pass
|
||||
|
||||
def handle_server(irc, numeric, command, args):
|
||||
# SERVER is sent by our uplink or any other server to introduce others.
|
||||
# <- :00A SERVER test.server * 1 00C :testing raw message syntax
|
||||
# <- :70M SERVER millennium.overdrive.pw * 1 1ML :a relatively long period of time... (Fremont, California)
|
||||
servername = args[0].lower()
|
||||
sid = args[3]
|
||||
irc.servers[sid] = IrcServer(numeric, servername)
|
||||
|
||||
def handle_nick(irc, numeric, command, args):
|
||||
# <- :70MAAAAAA NICK GL-devel 1434744242
|
||||
n = irc.users[numeric].nick = args[0]
|
||||
return {'target': n, 'ts': args[1]}
|
||||
|
||||
def handle_save(irc, numeric, command, args):
|
||||
# This is used to handle nick collisions. Here, the client Derp_ already exists,
|
||||
# so trying to change nick to it will cause a nick collision. On InspIRCd,
|
||||
# this will simply set the collided user's nick to its UID.
|
||||
|
||||
# <- :70MAAAAAA PRIVMSG 0AL000001 :nickclient PyLink Derp_
|
||||
# -> :0AL000001 NICK Derp_ 1433728673
|
||||
# <- :70M SAVE 0AL000001 1433728673
|
||||
user = args[0]
|
||||
irc.users[user].nick = user
|
||||
return {'target': user, 'ts': args[1]}
|
||||
|
||||
def handle_fmode(irc, numeric, command, args):
|
||||
# <- :70MAAAAAA FMODE #chat 1433653462 +hhT 70MAAAAAA 70MAAAAAD
|
||||
channel = args[0].lower()
|
||||
modes = args[2:]
|
||||
changedmodes = utils.parseModes(irc, channel, modes)
|
||||
utils.applyModes(irc, channel, changedmodes)
|
||||
return {'target': channel, 'modes': changedmodes}
|
||||
|
||||
def handle_mode(irc, numeric, command, args):
|
||||
# In InspIRCd, MODE is used for setting user modes and
|
||||
# FMODE is used for channel modes:
|
||||
# <- :70MAAAAAA MODE 70MAAAAAA -i+xc
|
||||
target = args[0]
|
||||
modestrings = args[1:]
|
||||
changedmodes = utils.parseModes(irc, numeric, modestrings)
|
||||
utils.applyModes(irc, numeric, changedmodes)
|
||||
return {'target': target, 'modes': changedmodes}
|
||||
|
||||
def handle_squit(irc, numeric, command, args):
|
||||
# :70M SQUIT 1ML :Server quit by GL!gl@0::1
|
||||
split_server = args[0]
|
||||
print('Netsplit on server %s' % split_server)
|
||||
# Prevent RuntimeError: dictionary changed size during iteration
|
||||
old_servers = copy(irc.servers)
|
||||
for sid, data in old_servers.items():
|
||||
if data.uplink == split_server:
|
||||
print('Server %s also hosts server %s, removing those users too...' % (split_server, sid))
|
||||
handle_squit(irc, sid, 'SQUIT', [sid, "PyLink: Automatically splitting leaf servers of %s" % sid])
|
||||
for user in copy(irc.servers[split_server].users):
|
||||
print('Removing client %s (%s)' % (user, irc.users[user].nick))
|
||||
removeClient(irc, user)
|
||||
del irc.servers[split_server]
|
||||
return {'target': split_server}
|
||||
|
||||
def handle_rsquit(irc, numeric, command, args):
|
||||
# <- :1MLAAAAIG RSQUIT :ayy.lmao
|
||||
# <- :1MLAAAAIG RSQUIT ayy.lmao :some reason
|
||||
# RSQUIT is sent by opers to squit remote servers.
|
||||
# Strangely, it takes a server name instead of a SID, and is
|
||||
# allowed to be ignored entirely.
|
||||
# If we receive a remote SQUIT, split the target server
|
||||
# ONLY if the sender is identified with us.
|
||||
target = args[0]
|
||||
for (sid, server) in irc.servers.items():
|
||||
if server.name == target:
|
||||
target = sid
|
||||
if utils.isInternalServer(irc, target):
|
||||
if irc.users[numeric].identified:
|
||||
uplink = irc.servers[target].uplink
|
||||
reason = 'Requested by %s' % irc.users[numeric].nick
|
||||
_sendFromServer(irc, uplink, 'SQUIT %s :%s' % (target, reason))
|
||||
return handle_squit(irc, numeric, 'SQUIT', [target, reason])
|
||||
else:
|
||||
utils.msg(irc, numeric, 'Error: you are not authorized to split servers!', notice=True)
|
||||
|
||||
def handle_idle(irc, numeric, command, args):
|
||||
"""Handle the IDLE command, sent between servers in remote WHOIS queries."""
|
||||
# <- :70MAAAAAA IDLE 1MLAAAAIG
|
||||
# -> :1MLAAAAIG IDLE 70MAAAAAA 1433036797 319
|
||||
sourceuser = numeric
|
||||
targetuser = args[0]
|
||||
_sendFromUser(irc, targetuser, 'IDLE %s %s 0' % (sourceuser, irc.users[targetuser].ts))
|
||||
|
||||
def handle_events(irc, data):
|
||||
# Each server message looks something like this:
|
||||
# :70M FJOIN #chat 1423790411 +AFPfjnt 6:5 7:5 9:5 :v,1SRAAESWE
|
||||
# :<sid> <command> <argument1> <argument2> ... :final multi word argument
|
||||
args = data.split()
|
||||
if not args:
|
||||
# No data??
|
||||
return
|
||||
if args[0] == 'SERVER':
|
||||
# SERVER whatever.net abcdefgh 0 10X :something
|
||||
servername = args[1].lower()
|
||||
numeric = args[4]
|
||||
if args[2] != irc.serverdata['recvpass']:
|
||||
# Check if recvpass is correct
|
||||
raise ProtocolError('Error: recvpass from uplink server %s does not match configuration!' % servername)
|
||||
irc.servers[numeric] = IrcServer(None, servername)
|
||||
return
|
||||
elif args[0] == 'CAPAB':
|
||||
# Capability negotiation with our uplink
|
||||
if args[1] == 'CHANMODES':
|
||||
# CAPAB CHANMODES :admin=&a allowinvite=A autoop=w ban=b banexception=e blockcolor=c c_registered=r exemptchanops=X filter=g flood=f halfop=%h history=H invex=I inviteonly=i joinflood=j key=k kicknorejoin=J limit=l moderated=m nickflood=F noctcp=C noextmsg=n nokick=Q noknock=K nonick=N nonotice=T official-join=!Y op=@o operonly=O opmoderated=U owner=~q permanent=P private=p redirect=L reginvite=R regmoderated=M secret=s sslonly=z stripcolor=S topiclock=t voice=+v
|
||||
|
||||
# Named modes are essential for a cross-protocol IRC service. We
|
||||
# can use InspIRCd as a model here and assign their mode map to our cmodes list.
|
||||
for modepair in args[2:]:
|
||||
name, char = modepair.split('=')
|
||||
# We don't really care about mode prefixes; just the mode char
|
||||
irc.cmodes[name.lstrip(':')] = char[-1]
|
||||
elif args[1] == 'USERMODES':
|
||||
# Ditto above.
|
||||
for modepair in args[2:]:
|
||||
name, char = modepair.split('=')
|
||||
irc.umodes[name.lstrip(':')] = char
|
||||
elif args[1] == 'CAPABILITIES':
|
||||
caps = dict([x.lstrip(':').split('=') for x in args[2:]])
|
||||
irc.maxnicklen = caps['NICKMAX']
|
||||
irc.maxchanlen = caps['CHANMAX']
|
||||
# Modes are divided into A, B, C, and D classes
|
||||
# See http://www.irc.org/tech_docs/005.html
|
||||
# FIXME: Find a better way to assign/store this.
|
||||
irc.cmodes['*A'], irc.cmodes['*B'], irc.cmodes['*C'], irc.cmodes['*D'] \
|
||||
= caps['CHANMODES'].split(',')
|
||||
irc.umodes['*A'], irc.umodes['*B'], irc.umodes['*C'], irc.umodes['*D'] \
|
||||
= caps['USERMODES'].split(',')
|
||||
irc.prefixmodes = re.search(r'\((.*?)\)', caps['PREFIX']).group(1)
|
||||
try:
|
||||
real_args = []
|
||||
for arg in args:
|
||||
real_args.append(arg)
|
||||
# If the argument starts with ':' and ISN'T the first argument.
|
||||
# The first argument is used for denoting the source UID/SID.
|
||||
if arg.startswith(':') and args.index(arg) != 0:
|
||||
# : is used for multi-word arguments that last until the end
|
||||
# of the message. We can use list splicing here to turn them all
|
||||
# into one argument.
|
||||
index = args.index(arg) # Get the array index of the multi-word arg
|
||||
# Set the last arg to a joined version of the remaining args
|
||||
arg = args[index:]
|
||||
arg = ' '.join(arg)[1:]
|
||||
# Cut the original argument list right before the multi-word arg,
|
||||
# and then append the multi-word arg.
|
||||
real_args = args[:index]
|
||||
real_args.append(arg)
|
||||
break
|
||||
real_args[0] = real_args[0].split(':', 1)[1]
|
||||
args = real_args
|
||||
|
||||
numeric = args[0]
|
||||
command = args[1]
|
||||
args = args[2:]
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
# We will do wildcard event handling here. Unhandled events are just ignored.
|
||||
try:
|
||||
func = globals()['handle_'+command.lower()]
|
||||
except KeyError: # unhandled event
|
||||
pass
|
||||
else:
|
||||
parsed_args = func(irc, numeric, command, args)
|
||||
# Only call our hooks if there's data to process. Handlers that support
|
||||
# hooks will return a dict of parsed arguments, which can be passed on
|
||||
# to plugins and the like. For example, the JOIN handler will return
|
||||
# something like: {'channel': '#whatever', 'users': ['UID1', 'UID2',
|
||||
# 'UID3']}, etc.
|
||||
if parsed_args:
|
||||
hook_cmd = command
|
||||
if command in hook_map:
|
||||
hook_cmd = hook_map[command]
|
||||
print('Parsed args %r received from %s handler (calling hook %s)' % (parsed_args, command, hook_cmd))
|
||||
# Iterate over hooked functions, catching errors accordingly
|
||||
for hook_func in utils.command_hooks[hook_cmd]:
|
||||
try:
|
||||
print('Calling function %s' % hook_func)
|
||||
hook_func(irc, numeric, command, parsed_args)
|
||||
except Exception:
|
||||
# We don't want plugins to crash our servers...
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
def spawnServer(irc, name, sid, uplink=None, desc='PyLink Server'):
|
||||
# -> :0AL SERVER test.server * 1 0AM :some silly pseudoserver
|
||||
uplink = uplink or irc.sid
|
||||
name = name.lower()
|
||||
assert len(sid) == 3, "Incorrect SID length"
|
||||
if sid in irc.servers:
|
||||
raise ValueError('A server with SID %r already exists!' % sid)
|
||||
for server in irc.servers.values():
|
||||
if name == server.name:
|
||||
raise ValueError('A server named %r already exists!' % name)
|
||||
if not utils.isInternalServer(irc, uplink):
|
||||
raise ValueError('Server %r is not a PyLink internal PseudoServer!' % uplink)
|
||||
if not utils.isServerName(name):
|
||||
raise ValueError('Invalid server name %r' % name)
|
||||
_sendFromServer(irc, uplink, 'SERVER %s * 1 %s :%s' % (name, sid, desc))
|
||||
_sendFromServer(irc, sid, 'ENDBURST')
|
||||
irc.servers[sid] = IrcServer(uplink, name, internal=True)
|
96
tests/test_proto_common.py
Normal file
96
tests/test_proto_common.py
Normal file
@ -0,0 +1,96 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import main
|
||||
import classes
|
||||
from collections import defaultdict
|
||||
import unittest
|
||||
|
||||
class FakeIRC(main.Irc):
|
||||
def __init__(self, proto):
|
||||
self.connected = False
|
||||
self.users = {}
|
||||
self.channels = defaultdict(classes.IrcChannel)
|
||||
self.name = 'fakeirc'
|
||||
self.servers = {}
|
||||
self.proto = proto
|
||||
|
||||
self.serverdata = {'netname': 'fakeirc',
|
||||
'ip': '0.0.0.0',
|
||||
'port': 7000,
|
||||
'recvpass': "abcd",
|
||||
'sendpass': "abcd",
|
||||
'protocol': "testingonly",
|
||||
'hostname': "pylink.unittest",
|
||||
'sid': "9PY",
|
||||
'channels': ["#pylink"],
|
||||
}
|
||||
self.conf = {'server': self.serverdata}
|
||||
ip = self.serverdata["ip"]
|
||||
port = self.serverdata["port"]
|
||||
self.sid = self.serverdata["sid"]
|
||||
self.socket = None
|
||||
self.messages = []
|
||||
|
||||
def run(self, data):
|
||||
"""Queues a message to the fake IRC server."""
|
||||
print('-> ' + data)
|
||||
self.proto.handle_events(self, data)
|
||||
|
||||
def send(self, data):
|
||||
self.messages.append(data)
|
||||
print('<- ' + data)
|
||||
|
||||
def takeMsgs(self):
|
||||
"""Returns a list of messages sent by the protocol module since
|
||||
the last takeMsgs() call, so we can track what has been sent."""
|
||||
msgs = self.messages
|
||||
self.messages = []
|
||||
return msgs
|
||||
|
||||
def takeCommands(self, msgs):
|
||||
"""Returns a list of commands parsed from the output of takeMsgs()."""
|
||||
sidprefix = ':' + self.sid
|
||||
commands = []
|
||||
for m in msgs:
|
||||
args = m.split()
|
||||
if m.startswith(sidprefix):
|
||||
commands.append(args[1])
|
||||
else:
|
||||
commands.append(args[0])
|
||||
return commands
|
||||
|
||||
class FakeProto():
|
||||
"""Dummy protocol module for testing purposes."""
|
||||
@staticmethod
|
||||
def handle_events(irc, data):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def connect(irc):
|
||||
pass
|
||||
|
||||
# Yes, we're going to even test the testing classes. Testception? I think so.
|
||||
class Test_TestProtoCommon(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.irc = FakeIRC(FakeProto())
|
||||
|
||||
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()
|
172
tests/test_proto_inspircd.py
Normal file
172
tests/test_proto_inspircd.py
Normal file
@ -0,0 +1,172 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path += [os.getcwd(), os.path.join(os.getcwd(), 'protocols')]
|
||||
import unittest
|
||||
import time
|
||||
|
||||
import inspircd
|
||||
from . import test_proto_common
|
||||
from classes import ProtocolError
|
||||
import utils
|
||||
|
||||
class TestInspIRCdProtocol(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.irc = test_proto_common.FakeIRC(inspircd)
|
||||
self.proto = self.irc.proto
|
||||
self.sdata = self.irc.serverdata
|
||||
# This is to initialize ourself as an internal PseudoServer, so we can spawn clients
|
||||
self.proto.connect(self.irc)
|
||||
self.u = self.irc.pseudoclient.uid
|
||||
|
||||
def test_connect(self):
|
||||
initial_messages = self.irc.takeMsgs()
|
||||
commands = self.irc.takeCommands(initial_messages)
|
||||
|
||||
# SERVER pylink.unittest abcd 0 9PY :PyLink Service
|
||||
serverline = 'SERVER %s %s 0 %s :PyLink Service' % (
|
||||
self.sdata['hostname'], self.sdata['sendpass'], self.sdata['sid'])
|
||||
self.assertIn(serverline, initial_messages)
|
||||
self.assertIn('BURST', commands)
|
||||
self.assertIn('ENDBURST', commands)
|
||||
# Is it creating our lovely PyLink PseudoClient?
|
||||
self.assertIn('UID', commands)
|
||||
self.assertIn('FJOIN', commands)
|
||||
|
||||
def testCheckRecvpass(self):
|
||||
# Correct recvpass here.
|
||||
self.irc.run('SERVER somehow.someday abcd 0 0AL :Somehow Server - McMurdo Station, Antarctica')
|
||||
# Incorrect recvpass here; should raise ProtocolError.
|
||||
self.assertRaises(ProtocolError, self.irc.run, 'SERVER somehow.someday BADPASS 0 0AL :Somehow Server - McMurdo Station, Antarctica')
|
||||
|
||||
def testSpawnClient(self):
|
||||
u = self.proto.spawnClient(self.irc, '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, self.irc, 'abcd', 'user', 'dummy.user.net', server='44A')
|
||||
|
||||
def testJoinClient(self):
|
||||
u = self.u
|
||||
self.proto.joinClient(self.irc, u, '#Channel')
|
||||
self.assertIn(u, self.irc.channels['#channel'].users)
|
||||
# Non-existant user.
|
||||
self.assertRaises(LookupError, self.proto.joinClient, self.irc, '9PYZZZZZZ', '#test')
|
||||
# Invalid channel.
|
||||
self.assertRaises(ValueError, self.proto.joinClient, self.irc, u, 'aaaa')
|
||||
|
||||
def testPartClient(self):
|
||||
u = self.u
|
||||
self.proto.joinClient(self.irc, u, '#channel')
|
||||
self.proto.partClient(self.irc, u, '#channel')
|
||||
self.assertNotIn(u, self.irc.channels['#channel'].users)
|
||||
|
||||
def testQuitClient(self):
|
||||
u = self.proto.spawnClient(self.irc, 'testuser3', 'moo', 'hello.world').uid
|
||||
self.proto.joinClient(self.irc, u, '#channel')
|
||||
self.assertRaises(LookupError, self.proto.quitClient, self.irc, '9PYZZZZZZ', 'quit reason')
|
||||
self.proto.quitClient(self.irc, 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)
|
||||
pass
|
||||
|
||||
def testKickClient(self):
|
||||
target = self.proto.spawnClient(self.irc, 'soccerball', 'soccerball', 'abcd').uid
|
||||
self.proto.joinClient(self.irc, target, '#pylink')
|
||||
self.assertIn(self.u, self.irc.channels['#pylink'].users)
|
||||
self.assertIn(target, self.irc.channels['#pylink'].users)
|
||||
self.proto.kickClient(self.irc, self.u, '#pylink', target, 'Pow!')
|
||||
self.assertNotIn(target, self.irc.channels['#pylink'].users)
|
||||
|
||||
def testNickClient(self):
|
||||
self.proto.nickClient(self.irc, self.u, 'NotPyLink')
|
||||
self.assertEqual('NotPyLink', self.irc.users[self.u].nick)
|
||||
|
||||
def testSpawnServer(self):
|
||||
# Incorrect SID length
|
||||
self.assertRaises(Exception, self.proto.spawnServer, self.irc, 'subserver.pylink', '34Q0')
|
||||
self.proto.spawnServer(self.irc, 'subserver.pylink', '34Q')
|
||||
# Duplicate server name
|
||||
self.assertRaises(Exception, self.proto.spawnServer, self.irc, 'Subserver.PyLink', '34Z')
|
||||
# Duplicate SID
|
||||
self.assertRaises(Exception, self.proto.spawnServer, self.irc, 'another.Subserver.PyLink', '34Q')
|
||||
self.assertIn('34Q', self.irc.servers)
|
||||
# Are we bursting properly?
|
||||
self.assertIn(':34Q ENDBURST', self.irc.takeMsgs())
|
||||
|
||||
def testSpawnClientOnServer(self):
|
||||
self.proto.spawnServer(self.irc, 'subserver.pylink', '34Q')
|
||||
u = self.proto.spawnClient(self.irc, 'person1', 'person', 'users.overdrive.pw', 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 testSquit(self):
|
||||
# Spawn a messy network map, just because!
|
||||
self.proto.spawnServer(self.irc, 'level1.pylink', '34P')
|
||||
self.proto.spawnServer(self.irc, 'level2.pylink', '34Q', uplink='34P')
|
||||
self.proto.spawnServer(self.irc, 'level3.pylink', '34Z', uplink='34Q')
|
||||
self.proto.spawnServer(self.irc, 'level4.pylink', '34Y', uplink='34Z')
|
||||
self.assertEqual(self.irc.servers['34Y'].uplink, '34Z')
|
||||
s4u = self.proto.spawnClient(self.irc, 'person1', 'person', 'users.overdrive.pw', server='34Y').uid
|
||||
s3u = self.proto.spawnClient(self.irc, 'person2', 'person', 'users.overdrive.pw', server='34Z').uid
|
||||
self.proto.joinClient(self.irc, s3u, '#pylink')
|
||||
self.proto.joinClient(self.irc, s4u, '#pylink')
|
||||
self.proto.handle_squit(self.irc, '9PY', 'SQUIT', ['34Y'])
|
||||
self.assertNotIn(s4u, self.irc.users)
|
||||
self.assertNotIn('34Y', self.irc.servers)
|
||||
# Netsplits are obviously recursive, so all these should be removed.
|
||||
self.proto.handle_squit(self.irc, '9PY', 'SQUIT', ['34P'])
|
||||
self.assertNotIn(s3u, self.irc.users)
|
||||
self.assertNotIn('34P', self.irc.servers)
|
||||
self.assertNotIn('34Q', self.irc.servers)
|
||||
self.assertNotIn('34Z', self.irc.servers)
|
||||
|
||||
def testRSquit(self):
|
||||
u = self.proto.spawnClient(self.irc, 'person1', 'person', 'users.overdrive.pw')
|
||||
u.identified = 'admin'
|
||||
self.proto.spawnServer(self.irc, 'level1.pylink', '34P')
|
||||
self.irc.run(':%s RSQUIT level1.pylink :some reason' % self.u)
|
||||
# No SQUIT yet, since the 'PyLink' client isn't identified
|
||||
self.assertNotIn('SQUIT', self.irc.takeCommands(self.irc.takeMsgs()))
|
||||
# The one we just spawned however, is.
|
||||
self.irc.run(':%s RSQUIT level1.pylink :some reason' % u.uid)
|
||||
self.assertIn('SQUIT', self.irc.takeCommands(self.irc.takeMsgs()))
|
||||
self.assertNotIn('34P', self.irc.servers)
|
||||
|
||||
def testHandleServer(self):
|
||||
self.irc.run('SERVER whatever.net abcd 0 10X :something')
|
||||
self.assertIn('10X', self.irc.servers)
|
||||
self.assertEqual('whatever.net', self.irc.servers['10X'].name)
|
||||
self.irc.run(':10X SERVER test.server * 1 0AL :testing raw message syntax')
|
||||
self.assertIn('0AL', self.irc.servers)
|
||||
self.assertEqual('test.server', self.irc.servers['0AL'].name)
|
||||
|
||||
def testHandleUID(self):
|
||||
self.irc.run('SERVER whatever.net abcd 0 10X :something')
|
||||
self.irc.run(':10X UID 10XAAAAAB 1429934638 GL 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 +Wioswx +ACGKNOQXacfgklnoqvx :realname')
|
||||
self.assertIn('10XAAAAAB', self.irc.servers['10X'].users)
|
||||
self.assertIn('10XAAAAAB', self.irc.users)
|
||||
u = self.irc.users['10XAAAAAB']
|
||||
self.assertEqual('GL', u.nick)
|
||||
|
||||
def testHandleKill(self):
|
||||
self.irc.takeMsgs() # Ignore the initial connect messages
|
||||
self.irc.run(':9PYAAAAAA KILL 9PYAAAAAA :killed')
|
||||
msgs = self.irc.takeMsgs()
|
||||
commands = self.irc.takeCommands(msgs)
|
||||
# Make sure we're respawning our PseudoClient when its killed
|
||||
self.assertIn('UID', commands)
|
||||
self.assertIn('FJOIN', commands)
|
||||
|
||||
def testHandleKick(self):
|
||||
self.irc.takeMsgs() # Ignore the initial connect messages
|
||||
self.irc.run(':9PYAAAAAA KICK #pylink 9PYAAAAAA :kicked')
|
||||
# Ditto above
|
||||
msgs = self.irc.takeMsgs()
|
||||
commands = self.irc.takeCommands(msgs)
|
||||
self.assertIn('FJOIN', commands)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
62
tests/test_utils.py
Normal file
62
tests/test_utils.py
Normal file
@ -0,0 +1,62 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.getcwd())
|
||||
import unittest
|
||||
|
||||
import utils
|
||||
|
||||
def dummyf():
|
||||
pass
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
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', utils.bot_commands)
|
||||
self.assertIn('test', utils.bot_commands)
|
||||
self.assertNotIn('TEST', utils.bot_commands)
|
||||
|
||||
def test_add_hook(self):
|
||||
utils.add_hook(dummyf, 'join')
|
||||
self.assertIn('JOIN', utils.command_hooks)
|
||||
# Command names stored in uppercase.
|
||||
self.assertNotIn('join', utils.command_hooks)
|
||||
self.assertIn(dummyf, utils.command_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('s'))
|
||||
self.assertFalse(utils.isServerName('s.'))
|
||||
self.assertFalse(utils.isServerName('.s.s.s'))
|
||||
self.assertTrue(utils.isServerName('Hello.world'))
|
||||
self.assertFalse(utils.isServerName(''))
|
||||
self.assertTrue(utils.isServerName('pylink.overdrive.pw'))
|
||||
self.assertFalse(utils.isServerName(' i lost the game'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
205
utils.py
205
utils.py
@ -1,13 +1,198 @@
|
||||
import string
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
# From http://www.inspircd.org/wiki/Modules/spanningtree/UUIDs.html
|
||||
chars = string.digits + string.ascii_uppercase
|
||||
iters = [iter(chars) for _ in range(6)]
|
||||
a = [next(i) for i in iters]
|
||||
global bot_commands, command_hooks
|
||||
# This should be a mapping of command names to functions
|
||||
bot_commands = {}
|
||||
command_hooks = defaultdict(list)
|
||||
|
||||
def next_uid(sid, level=-1):
|
||||
try:
|
||||
a[level] = next(iters[level])
|
||||
return sid + ''.join(a)
|
||||
except StopIteration:
|
||||
return UID(level-1)
|
||||
class TS6UIDGenerator():
|
||||
"""TS6 UID Generator module, adapted from InspIRCd source
|
||||
https://github.com/inspircd/inspircd/blob/f449c6b296ab/src/server.cpp#L85-L156
|
||||
"""
|
||||
|
||||
def __init__(self, sid):
|
||||
# TS6 UIDs are 6 characters in length (9 including the SID).
|
||||
# They wrap from ABCDEFGHIJKLMNOPQRSTUVWXYZ -> 0123456789 -> wrap around:
|
||||
# (e.g. AAAAAA, AAAAAB ..., AAAAA8, AAAAA9, AAAABA)
|
||||
self.allowedchars = string.ascii_uppercase + string.digits
|
||||
self.uidchars = [self.allowedchars[0]]*6
|
||||
self.sid = sid
|
||||
|
||||
def increment(self, pos=5):
|
||||
# If we're at the last character in the list of allowed ones, reset
|
||||
# and increment the next level above.
|
||||
if self.uidchars[pos] == self.allowedchars[-1]:
|
||||
self.uidchars[pos] = self.allowedchars[0]
|
||||
self.increment(pos-1)
|
||||
else:
|
||||
# Find what position in the allowed characters list we're currently
|
||||
# on, and add one.
|
||||
idx = self.allowedchars.find(self.uidchars[pos])
|
||||
self.uidchars[pos] = self.allowedchars[idx+1]
|
||||
|
||||
def next_uid(self):
|
||||
uid = self.sid + ''.join(self.uidchars)
|
||||
self.increment()
|
||||
return uid
|
||||
|
||||
def msg(irc, target, text, notice=False):
|
||||
command = 'NOTICE' if notice else 'PRIVMSG'
|
||||
irc.proto._sendFromUser(irc, irc.pseudoclient.uid, '%s %s :%s' % (command, target, text))
|
||||
|
||||
def add_cmd(func, name=None):
|
||||
if name is None:
|
||||
name = func.__name__
|
||||
name = name.lower()
|
||||
bot_commands[name] = func
|
||||
|
||||
def add_hook(func, command):
|
||||
"""Add a hook <func> for command <command>."""
|
||||
command = command.upper()
|
||||
command_hooks[command].append(func)
|
||||
|
||||
def nickToUid(irc, nick):
|
||||
for k, v in irc.users.items():
|
||||
if v.nick == nick:
|
||||
return k
|
||||
|
||||
def clientToServer(irc, numeric):
|
||||
"""<irc object> <numeric>
|
||||
|
||||
Finds the server SID of user <numeric> and returns it."""
|
||||
for server in irc.servers:
|
||||
if numeric in irc.servers[server].users:
|
||||
return server
|
||||
|
||||
# A+ regex
|
||||
_nickregex = r'^[A-Za-z\|\\_\[\]\{\}\^\`][A-Z0-9a-z\-\|\\_\[\]\{\}\^\`]*$'
|
||||
def isNick(s, nicklen=None):
|
||||
if nicklen and len(s) > nicklen:
|
||||
return False
|
||||
return bool(re.match(_nickregex, s))
|
||||
|
||||
def isChannel(s):
|
||||
return s.startswith('#')
|
||||
|
||||
def _isASCII(s):
|
||||
chars = string.ascii_letters + string.digits + string.punctuation
|
||||
return all(char in chars for char in s)
|
||||
|
||||
def isServerName(s):
|
||||
return _isASCII(s) and '.' in s and not s.startswith('.') \
|
||||
and not s.endswith('.')
|
||||
|
||||
def parseModes(irc, target, args):
|
||||
"""Parses a mode string into a list of (mode, argument) tuples.
|
||||
['+mitl-o', '3', 'person'] => [('+m', None), ('+i', None), ('+t', None), ('+l', '3'), ('-o', 'person')]
|
||||
"""
|
||||
# http://www.irc.org/tech_docs/005.html
|
||||
# A = Mode that adds or removes a nick or address to a list. 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.
|
||||
# D = Mode that changes a setting and never has a parameter.
|
||||
usermodes = not isChannel(target)
|
||||
modestring = args[0]
|
||||
if not modestring:
|
||||
return ValueError('No modes supplied in parseModes query: %r' % modes)
|
||||
args = args[1:]
|
||||
if usermodes:
|
||||
supported_modes = irc.umodes
|
||||
else:
|
||||
supported_modes = irc.cmodes
|
||||
print('supported modes: %s' % supported_modes)
|
||||
res = []
|
||||
for x in ('A', 'B', 'C', 'D'):
|
||||
print('%s modes: %s' % (x, supported_modes['*'+x]))
|
||||
for mode in modestring:
|
||||
if mode in '+-':
|
||||
prefix = mode
|
||||
else:
|
||||
arg = None
|
||||
if mode in (supported_modes['*A'] + supported_modes['*B']):
|
||||
# Must have parameter.
|
||||
print('%s: Must have parameter.' % mode)
|
||||
arg = args.pop(0)
|
||||
elif mode in irc.prefixmodes and not usermodes:
|
||||
# We're setting a prefix mode on someone (e.g. +o user1)
|
||||
print('%s: prefixmode.' % mode)
|
||||
arg = args.pop(0)
|
||||
elif prefix == '+' and mode in supported_modes['*C']:
|
||||
# Only has parameter when setting.
|
||||
print('%s: Only has parameter when setting.' % mode)
|
||||
arg = args.pop(0)
|
||||
res.append((prefix + mode, arg))
|
||||
return res
|
||||
|
||||
def applyModes(irc, target, changedmodes):
|
||||
usermodes = not isChannel(target)
|
||||
print('usermodes? %s' % usermodes)
|
||||
if usermodes:
|
||||
modelist = irc.users[target].modes
|
||||
else:
|
||||
modelist = irc.channels[target].modes
|
||||
print('Initial modelist: %s' % modelist)
|
||||
print('Changedmodes: %r' % changedmodes)
|
||||
for mode in changedmodes:
|
||||
if not usermodes:
|
||||
pmode = ''
|
||||
for m in ('owner', 'admin', 'op', 'halfop', 'voice'):
|
||||
if m in irc.cmodes and mode[0][1] == irc.cmodes[m]:
|
||||
pmode = m+'s'
|
||||
print('pmode? %s' % pmode)
|
||||
if pmode:
|
||||
print('pmode == True')
|
||||
print(mode)
|
||||
print(irc.channels[target].prefixmodes)
|
||||
pmodelist = irc.channels[target].prefixmodes[pmode]
|
||||
print(pmodelist)
|
||||
print('Initial pmodelist: %s' % pmodelist)
|
||||
if mode[0][0] == '+':
|
||||
pmodelist.add(mode[1])
|
||||
print('+')
|
||||
else:
|
||||
pmodelist.discard(mode[1])
|
||||
print('-')
|
||||
print('Final pmodelist: %s' % pmodelist)
|
||||
if mode[0][1] in irc.prefixmodes:
|
||||
# Ignore other prefix modes such as InspIRCd's +Yy
|
||||
continue
|
||||
if mode[0][0] == '+':
|
||||
# We're adding a mode
|
||||
modelist.add(mode)
|
||||
print('Adding mode %r' % str(mode))
|
||||
else:
|
||||
# We're removing a mode
|
||||
mode[0] = mode[0].replace('-', '+')
|
||||
modelist.discard(mode)
|
||||
print('Removing mode %r' % str(mode))
|
||||
print('Final modelist: %s' % modelist)
|
||||
|
||||
def joinModes(modes):
|
||||
modelist = ''
|
||||
args = []
|
||||
for modepair in modes:
|
||||
mode, arg = modepair
|
||||
modelist += mode[1]
|
||||
if arg is not None:
|
||||
args.append(arg)
|
||||
s = '+%s %s' % (modelist, ' '.join(args))
|
||||
return s
|
||||
|
||||
def isInternalClient(irc, numeric):
|
||||
"""<irc object> <client numeric>
|
||||
|
||||
Checks whether <client numeric> is a PyLink PseudoClient,
|
||||
returning the SID of the PseudoClient's server if True.
|
||||
"""
|
||||
for sid in irc.servers:
|
||||
if irc.servers[sid].internal and numeric in irc.servers[sid].users:
|
||||
return sid
|
||||
|
||||
def isInternalServer(irc, sid):
|
||||
"""<irc object> <sid>
|
||||
|
||||
Returns whether <sid> is an internal PyLink PseudoServer.
|
||||
"""
|
||||
return (sid in irc.servers and irc.servers[sid].internal)
|
||||
|
Loading…
Reference in New Issue
Block a user