├── .github └── workflows │ └── publish_pypi.yml ├── .gitignore ├── LICENSE ├── README.md ├── irc_bot ├── __init__.py ├── command_scheduler.py ├── irc_message.py ├── numeric_replies.py ├── rfc2812.txt ├── simple_irc_bot.py └── utils.py └── pyproject.toml /.github/workflows/publish_pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPi 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: '3.8' 18 | 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install wheel build poetry 23 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 24 | 25 | - name: Publish package to PyPI 26 | if: github.repository == 'cvium/irc_bot' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 27 | run: | 28 | poetry version $(git describe --tags --abbrev=0) 29 | poetry build 30 | poetry publish --username __token__ --password "${{ secrets.PYPI_API_TOKEN }}" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | poetry.lock 4 | dist/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Claus Vium 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IRC bot 2 | 3 | Just a small library based on asyncore that allows you to quickly create a small IRC bot 4 | 5 | ## Pull Requests 6 | Pull requests are welcome. The code base is a mess and I'm too lazy to fix it. 7 | -------------------------------------------------------------------------------- /irc_bot/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | import importlib.metadata as importlib_metadata 3 | except ModuleNotFoundError: 4 | import importlib_metadata 5 | 6 | __version__ = importlib_metadata.version(__name__) -------------------------------------------------------------------------------- /irc_bot/command_scheduler.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals, division, absolute_import 2 | from builtins import * # pylint: disable=unused-import, redefined-builtin 3 | 4 | import bisect 5 | import datetime 6 | import logging 7 | 8 | log = logging.getLogger('irc_bot') 9 | 10 | 11 | class CommandScheduler(object): 12 | def __init__(self): 13 | self.queue = [] 14 | 15 | def clear(self): 16 | self.queue = [] 17 | 18 | def peek(self): 19 | if len(self.queue) > 0: 20 | return self.queue[0].scheduled_time 21 | return datetime.datetime.max 22 | 23 | def execute(self): 24 | while self.peek() <= datetime.datetime.now(): 25 | queued_command = self.queue.pop(0) 26 | log.debug('Executing scheduled command %s', queued_command.command.__name__) 27 | queued_command.command() 28 | if queued_command.persists: 29 | self.queue_command(queued_command.after, queued_command.command, queued_command.persists) 30 | 31 | def queue_command(self, after, cmd, persists=False, unique=True): 32 | log.debug('Queueing command "%s" to execute in %s second(s)', cmd.__name__, after) 33 | timestamp = datetime.datetime.now() + datetime.timedelta(seconds=after) 34 | queued_command = QueuedCommand(after, timestamp, cmd, persists) 35 | if not unique or queued_command not in self.queue: 36 | bisect.insort(self.queue, queued_command) 37 | else: 38 | log.warning('Failed to queue command "%s" because it\'s already queued.', cmd.__name__) 39 | 40 | 41 | class QueuedCommand(object): 42 | def __init__(self, after, scheduled_time, command, persists=False): 43 | self.after = after 44 | self.scheduled_time = scheduled_time 45 | self.command = command 46 | self.persists = persists 47 | 48 | def __lt__(self, other): 49 | return self.scheduled_time < other.scheduled_time 50 | 51 | def __eq__(self, other): 52 | commands_match = self.command.__name__ == other.command.__name__ 53 | if hasattr(self.command, 'args') and hasattr(other.command, 'args'): 54 | return commands_match and self.command.args == other.command.args 55 | return commands_match 56 | -------------------------------------------------------------------------------- /irc_bot/irc_message.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals, division, absolute_import 2 | from builtins import * # pylint: disable=unused-import, redefined-builtin 3 | 4 | from future.utils import python_2_unicode_compatible 5 | 6 | import re 7 | 8 | from irc_bot.utils import printable_unicode_list 9 | from irc_bot.numeric_replies import REPLY_CODES 10 | 11 | 12 | @python_2_unicode_compatible 13 | class IRCMessage(object): 14 | def __init__(self, msg): 15 | rfc_1459 = "^(@(?P[^ ]*) )?(:(?P[^ ]+) +)?(?P[^ ]+)( *(?P .+))?" 16 | msg_contents = re.match(rfc_1459, msg) 17 | 18 | self.raw = msg 19 | self.tags = msg_contents.group('tags') 20 | self.prefix = msg_contents.group('prefix') 21 | self.from_nick = self.prefix.split('!')[0] if self.prefix and '!' in self.prefix else None 22 | self.command = msg_contents.group('command') 23 | if self.command.isdigit() and self.command in REPLY_CODES.keys(): 24 | self.command = REPLY_CODES[self.command] 25 | if msg_contents.group('arguments'): 26 | args, sep, ext = msg_contents.group('arguments').partition(' :') 27 | self.arguments = args.split() 28 | if sep: 29 | self.arguments.append(ext) 30 | else: 31 | self.arguments = [] 32 | 33 | def __repr__(self): 34 | return self.__str__() 35 | 36 | def __str__(self): 37 | printable_arguments = printable_unicode_list(self.arguments) 38 | tmpl = ( 39 | "command: {}, " 40 | "prefix: {}, " 41 | "tags: {}, " 42 | "arguments: {}, " 43 | "from_nick: {}, " 44 | "raw: {}" 45 | ) 46 | return tmpl.format(self.command, self.prefix, self.tags, printable_arguments, self.from_nick, self.raw) -------------------------------------------------------------------------------- /irc_bot/numeric_replies.py: -------------------------------------------------------------------------------- 1 | REPLY_CODES = { 2 | '0': 'RPLNONE', 3 | # INITIAL 4 | '001': 'RPLWELCOME', # :WELCOME TO THE INTERNET RELAY NETWORK 5 | '002': 'RPLYOURHOST', # :YOUR HOST IS , RUNNING VERSION 6 | '003': 'RPLCREATED', # :THIS SERVER WAS CREATED 7 | '004': 'RPLMYINFO', # 8 | '005': 'RPLMAP', # :MAP 9 | '007': 'RPLENDOFMAP', # :END OF /MAP 10 | '375': 'RPLMOTDSTART', # :- SERVER MESSAGE OF THE DAY 11 | '372': 'RPLMOTD', # :- 12 | '377': 'RPLMOTDALT', # :- 13 | '378': 'RPLMOTDALT2', # :- 14 | '376': 'RPLMOTDEND', # :END OF /M'OTD COMMAND. 15 | '221': 'RPLUMODEIS', # 16 | 17 | # ISON/USERHOST 18 | '302': 'RPLUSERHOST', # :USERHOSTS 19 | '303': 'RPLISON', # :NICKNAMES 20 | 21 | # AWAY 22 | '301': 'RPLAWAY', # :AWAY 23 | '305': 'RPLUNAWAY', # :YOU ARE NO LONGER MARKED AS BEING AWAY 24 | '306': 'RPLNOWAWAY', # :YOU HAVE BEEN MARKED AS BEING AWAY 25 | 26 | # WHOIS/WHOWAS 27 | '310': 'RPLWHOISHELPER', # :LOOKS VERY HELPFUL 28 | '311': 'RPLWHOISUSER', #
* : 29 | '312': 'RPLWHOISSERVER', # : 30 | '313': 'RPLWHOISOPERATOR', # :IS AN IRC OPERATOR 31 | '317': 'RPLWHOISIDLE', # : 32 | '318': 'RPLENDOFWHOIS', # :END OF /WHOIS LIST. 33 | '319': 'RPLWHOISCHANNELS', # : 34 | '314': 'RPLWHOWASUSER', #
* : 35 | '369': 'RPLENDOFWHOWAS', # :END OF WHOWAS 36 | '352': 'RPLWHOREPLY', #
: 37 | '315': 'RPLENDOFWHO', # :END OF /WHO LIST. 38 | '307': 'RPLUSERIPS', # :USERIPS 39 | '340': 'RPLUSERIP', # :=+@ 40 | 41 | # LIST 42 | '321': 'RPLLISTSTART', # CHANNEL :USERS NAME 43 | '322': 'RPLLIST', # : 44 | '323': 'RPLLISTEND', # :END OF /LIST 45 | '364': 'RPLLINKS', # : 46 | '365': 'RPLENDOFLINKS', # :END OF /LINKS LIST. 47 | 48 | # POST-CHANNEL JOIN 49 | '325': 'RPLUNIQOPIS', # 50 | '324': 'RPLCHANNELMODEIS', # 51 | '328': 'RPLCHANNELURL', # :URL 52 | '329': 'RPLCHANNELCREATED', #