mirror of
				https://github.com/jlu5/PyLink.git
				synced 2025-10-31 15:07:25 +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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 James Lu
						James Lu