3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-11-01 09:19:23 +01:00

Merge commit 'bd755e137ffa034007a77d75fbd00d21e759163e' into wip/logger-module

Conflicts:
	proto.py
This commit is contained in:
James Lu 2015-07-05 13:22:17 -07:00
commit f06bcc7928
17 changed files with 1653 additions and 279 deletions

362
LICENSE Normal file
View 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.

View File

@ -1,17 +1,20 @@
# PyLink # PyLink
<img src="https://dl.dropboxusercontent.com/u/18664770/pylink.png" width="75%" height="75%">
PyLink is an IRC PseudoService written in Python. PyLink is an IRC PseudoService written in Python.
## Dependencies ## Dependencies
PyLink is a serious WIP right now. Dependencies currently include: 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`) * PyYAML (`pip install pyyaml` or `apt-get install python3-yaml`)
## Usage ## 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. 2) Run `main.py` from the command line.

54
classes.py Normal file
View 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
View File

@ -0,0 +1,5 @@
import yaml
with open("config.yml", 'r') as f:
global conf
conf = yaml.load(f)

View File

@ -24,8 +24,10 @@ server:
# SID - required for InspIRCd and TS6 based servers. This must be three characters long. # 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. # The first char must be a digit [0-9], and the remaining two chars may be letters [A-Z] or digits.
sid: "0AL" sid: "0AL"
channel: "#pylink" # Autojoin channels
channels: ["#pylink"]
protocol: "inspircd"
# Plugins to load (omit the .py extension) # Plugins to load (omit the .py extension)
plugins: plugins:
- hello - commands

5
log.py Normal file
View 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
View File

@ -1,48 +1,49 @@
#!/usr/bin/python3 #!/usr/bin/python3
import yaml
import imp import imp
import os import os
import socket import socket
import time import time
import sys import sys
import logging from collections import defaultdict
import proto from log import log
from conf import conf
with open("config.yml", 'r') as f: import classes
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...")
class Irc(): class Irc():
def __init__(self): def __init__(self, proto):
# Initialize some variables # Initialize some variables
self.socket = socket.socket()
self.connected = False self.connected = False
self.users = {}
self.channels = {}
self.name = conf['server']['netname'] self.name = conf['server']['netname']
self.conf = conf self.conf = conf
# Server, channel, and user indexes to be populated by our protocol module
self.servers = {} 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'] self.serverdata = conf['server']
ip = self.serverdata["ip"] ip = self.serverdata["ip"]
port = self.serverdata["port"] port = self.serverdata["port"]
self.sid = self.serverdata["sid"] 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 = socket.socket()
self.socket.connect((ip, port)) self.socket.connect((ip, port))
self.proto = proto
proto.connect(self) proto.connect(self)
self.connected = True
self.loaded = [] self.loaded = []
self.load_plugins() self.load_plugins()
self.connected = True
self.run() self.run()
def run(self): def run(self):
@ -56,16 +57,16 @@ class Irc():
break break
while '\n' in buf: while '\n' in buf:
line, buf = buf.split('\n', 1) line, buf = buf.split('\n', 1)
logger.debug("<- {}".format(line)) log.debug("<- {}".format(line))
proto.handle_events(self, line) proto.handle_events(self, line)
except socket.error as e: except socket.error as e:
logger.error('Received socket.error: %s, exiting.' % str(e)) log.error('Received socket.error: %s, exiting.' % str(e))
break break
sys.exit(1) sys.exit(1)
def send(self, data): def send(self, data):
data = data.encode("utf-8") + b"\n" 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) self.socket.send(data)
def load_plugins(self): def load_plugins(self):
@ -74,9 +75,32 @@ class Irc():
# Here, we override the module lookup and import the plugins # Here, we override the module lookup and import the plugins
# dynamically depending on which were configured. # dynamically depending on which were configured.
for plugin in to_load: for plugin in to_load:
moduleinfo = imp.find_module(plugin, plugins_folder) try:
self.loaded.append(imp.load_source(plugin, moduleinfo[1])) moduleinfo = imp.find_module(plugin, plugins_folder)
logger.info("loaded plugins: %s" % self.loaded) 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
View 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)

View File

@ -4,24 +4,40 @@ import os
import logging import logging
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 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') @utils.add_cmd
@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
def debug(irc, source, args): def debug(irc, source, args):
proto._sendFromUser(irc, 'NOTICE %s :Debug info printed to console.' % (source)) print('user index: %s' % irc.users)
logger.debug(irc.users) print('server index: %s' % irc.servers)
logger.debug(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')

View File

@ -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
View 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
View File

@ -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
View 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)

View 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()

View 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
View 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
View File

@ -1,13 +1,198 @@
import string import string
import re
from collections import defaultdict
# From http://www.inspircd.org/wiki/Modules/spanningtree/UUIDs.html global bot_commands, command_hooks
chars = string.digits + string.ascii_uppercase # This should be a mapping of command names to functions
iters = [iter(chars) for _ in range(6)] bot_commands = {}
a = [next(i) for i in iters] command_hooks = defaultdict(list)
def next_uid(sid, level=-1): class TS6UIDGenerator():
try: """TS6 UID Generator module, adapted from InspIRCd source
a[level] = next(iters[level]) https://github.com/inspircd/inspircd/blob/f449c6b296ab/src/server.cpp#L85-L156
return sid + ''.join(a) """
except StopIteration:
return UID(level-1) 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)