ellinetircd 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ Copyright Julien Castiaux 2020
2
+ Copyright ElliNet13 2026
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,2 @@
1
+ include version.txt
2
+ graft tests
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: ellinetircd
3
+ Version: 1.0.0
4
+ Summary: ElliNetIRCd - an asynchronous IRC server
5
+ Home-page: https://github.com/ElliNet13/ElliNetIRCd
6
+ Download-URL: https://pypi.org/project/ellinetircd/
7
+ Author: ElliNet13
8
+ Author-email: your@email.com
9
+ License: MIT
10
+ Classifier: Environment :: No Input/Output (Daemon)
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Communications :: Chat :: Internet Relay Chat
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: trio
20
+ Dynamic: download-url
21
+ Dynamic: license-file
22
+
23
+ ellinetircd
24
+ =======
25
+
26
+ A python asynchronous IRC server based on aioirc.
27
+
28
+ ### Installation and Usage
29
+
30
+ Download and install the latest stable version using pip.
31
+ Windows users might replace `python3` by `py`.
32
+ ```bash
33
+ python3 -m pip install ellinetircd
34
+ ```
35
+
36
+ Then run the server:
37
+ ```bash
38
+ HOST=0.0.0.0 LOGLEVEL=INFO python3 -m ellinetircd
39
+ ```
40
+ If you are using powershell you can use the following command:
41
+ ```powershell
42
+ $env:HOST="0.0.0.0"
43
+ $env:LOGLEVEL="INFO"
44
+ python -m ellinetircd
45
+ ```
46
+
47
+ The configuration is done via environment variables, see `--help`:
48
+
49
+ optional arguments:
50
+ -h, --help show this help message and exit
51
+ -V, --version show program's version number and exit
52
+
53
+ environment variables:
54
+ HOST public domain name (default: julien-UX410UAR)
55
+ ADDR ip address to bind (default: 127.0.1.1)
56
+ PORT port to bind (default: 6667)
57
+ PASS server password (default: )
58
+ TIMEOUT kick inactive users after x seconds (default: 60)
59
+ PING_TIMEOUT PING inactive users x seconds before timeout (default: 5)
60
+ LOGLEVEL logging verbosity (default: WARNING)
@@ -0,0 +1,38 @@
1
+ ellinetircd
2
+ =======
3
+
4
+ A python asynchronous IRC server based on aioirc.
5
+
6
+ ### Installation and Usage
7
+
8
+ Download and install the latest stable version using pip.
9
+ Windows users might replace `python3` by `py`.
10
+ ```bash
11
+ python3 -m pip install ellinetircd
12
+ ```
13
+
14
+ Then run the server:
15
+ ```bash
16
+ HOST=0.0.0.0 LOGLEVEL=INFO python3 -m ellinetircd
17
+ ```
18
+ If you are using powershell you can use the following command:
19
+ ```powershell
20
+ $env:HOST="0.0.0.0"
21
+ $env:LOGLEVEL="INFO"
22
+ python -m ellinetircd
23
+ ```
24
+
25
+ The configuration is done via environment variables, see `--help`:
26
+
27
+ optional arguments:
28
+ -h, --help show this help message and exit
29
+ -V, --version show program's version number and exit
30
+
31
+ environment variables:
32
+ HOST public domain name (default: julien-UX410UAR)
33
+ ADDR ip address to bind (default: 127.0.1.1)
34
+ PORT port to bind (default: 6667)
35
+ PASS server password (default: )
36
+ TIMEOUT kick inactive users after x seconds (default: 60)
37
+ PING_TIMEOUT PING inactive users x seconds before timeout (default: 5)
38
+ LOGLEVEL logging verbosity (default: WARNING)
@@ -0,0 +1,34 @@
1
+ import contextvars
2
+ import logging
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ try:
6
+ __version__ = version("ellinetircd")
7
+ except PackageNotFoundError:
8
+ __version__ = "unknown"
9
+
10
+ from ellinetircd.config import config as cfg
11
+ logger = logging.getLogger(__package__)
12
+ IO = logging.INFO - 5
13
+ SECURITY = logging.ERROR + 5
14
+ logging.addLevelName(IO, 'IO')
15
+ logging.addLevelName(SECURITY, 'SECURITY')
16
+ logger.setLevel(cfg.LOGLEVEL)
17
+ servlocal = contextvars.ContextVar('servlocal')
18
+ MAXLINELEN = 512
19
+
20
+ import ellinetircd.channel
21
+ import ellinetircd.exceptions
22
+ import ellinetircd.server
23
+ import ellinetircd.sdnotify
24
+ import ellinetircd.states
25
+ import ellinetircd.user
26
+
27
+
28
+ def update_status() -> None:
29
+ sl = servlocal.get()
30
+ ellinetircd.sdnotify.status(
31
+ f"Listening on {cfg.ADDR} ({cfg.HOST}) port {cfg.PORT}. "
32
+ f"Currently {len(sl.users)} registered users"
33
+ f" in {len(sl.channels)} channels."
34
+ )
@@ -0,0 +1,75 @@
1
+ import argparse
2
+ import logging
3
+ import os
4
+ import sys
5
+ import textwrap
6
+ import trio
7
+ from socket import gethostname, gethostbyname
8
+
9
+ import ellinetircd
10
+ from ellinetircd.config import config as cfg
11
+ from ellinetircd.server import Server
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Color the [LEVEL] part of messages, need new terminal on Windows
16
+ # https://github.com/odoo/odoo/blob/13.0/odoo/netsvc.py#L57-L100
17
+ class ColoredFormatter(logging.Formatter):
18
+ colors = {
19
+ logging.DEBUG: (34, 49), # blue
20
+ ellinetircd.IO: (37, 49), # white
21
+ logging.INFO: (32, 49), # green
22
+ logging.WARNING: (33, 49), # yellow
23
+ logging.ERROR: (31, 49), # red
24
+ ellinetircd.SECURITY: (31, 49), # red
25
+ logging.CRITICAL: (37, 41), # white fg, red bg
26
+ }
27
+ def format(self, record: logging.LogRecord) -> str:
28
+ fg, bg = type(self).colors.get(record.levelno, (32, 49))
29
+ record.levelname = f'\033[1;{fg}m\033[1;{bg}m{record.levelname}\033[0m'
30
+ return super().format(record)
31
+
32
+ def main() -> None:
33
+ stderr = logging.StreamHandler()
34
+ stderr.formatter = (
35
+ ColoredFormatter('%(asctime)s [%(levelname)s] %(message)s')
36
+ if hasattr(sys.stderr, 'fileno') and os.isatty(sys.stderr.fileno()) else
37
+ logging.Formatter('[%(levelname)s] %(message)s')
38
+ )
39
+ root_logger = logging.getLogger('')
40
+ root_logger.handlers.clear()
41
+ root_logger.addHandler(stderr)
42
+
43
+ server = Server(cfg.HOST, cfg.ADDR, cfg.PORT, cfg.PASS)
44
+ try:
45
+ trio.run(server.serve)
46
+ except Exception:
47
+ logger.critical("Dead", exc_info=True)
48
+ finally:
49
+ logging.shutdown()
50
+
51
+ # Dummy argparse, used only for --help and --version
52
+ parser = argparse.ArgumentParser(
53
+ prog=ellinetircd.__name__,
54
+ usage=f"{sys.executable} -m {ellinetircd.__name__}",
55
+ description="single-server minimalist IRC",
56
+ formatter_class=argparse.RawDescriptionHelpFormatter,
57
+ epilog=textwrap.dedent(f"""\
58
+ environment variables:
59
+ HOST public domain name (default: {gethostname()})
60
+ ADDR ip address to bind (default: {gethostbyname(cfg.HOST)})
61
+ PORT port to bind (default: 6667)
62
+ PASS server password (default: )
63
+ TIMEOUT kick inactive users after x seconds (default: 60)
64
+ PING_TIMEOUT PING inactive users x seconds before timeout (default: 5)
65
+ LOGLEVEL logging verbosity (default: WARNING)
66
+ """)
67
+ )
68
+ parser.add_argument(
69
+ '-V', '--version',
70
+ action='version',
71
+ version=f'{ellinetircd.__name__} {ellinetircd.__version__}',
72
+ )
73
+ parser.parse_args()
74
+
75
+ main()
@@ -0,0 +1,85 @@
1
+ import logging
2
+ import trio
3
+ from functools import partial
4
+ from typing import List, Set, Union
5
+
6
+ import ellinetircd
7
+
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class Channel:
13
+ """
14
+ The Channel class represents a chat room.
15
+
16
+ Attributes:
17
+ name: str
18
+ the channel name.
19
+ users: Set[User]
20
+ a set of user who is subscribe to the channel.
21
+ """
22
+
23
+ def __init__(self, name: str) -> None:
24
+ """
25
+ Parameters:
26
+ name: str
27
+ the channel name.
28
+ """
29
+ self._name = name
30
+ self.users: Set["ellinetircd.user.User"] = set()
31
+
32
+ def __str__(self) -> str:
33
+ """
34
+ We represents the channel object by his name.
35
+
36
+ Return: str
37
+ the name of the channel.
38
+ """
39
+ return self._name
40
+
41
+ @property
42
+ def name(self) -> str:
43
+ """
44
+ Getter for private attributes channel name.
45
+
46
+ Returns: str
47
+ the name of the channel.
48
+ """
49
+ return self._name
50
+
51
+ def _log_messages(self, messages: List[str]) -> None:
52
+ """
53
+ Log all messages sent to a client.
54
+
55
+ Parameters: List[str]
56
+ A list of messages send to the client.
57
+ """
58
+ for message in messages:
59
+ logger.log(ellinetircd.IO, "send to %s: %s", self, message)
60
+
61
+ async def send(
62
+ self,
63
+ messages: Union[str, List[str]],
64
+ skipusers: Set["ellinetircd.user.User"] = set(),
65
+ ) -> None:
66
+ """"
67
+ Send many messages to each user subscribed to this channel.
68
+
69
+ Parameters:
70
+ messages: Union[str, List[str]]
71
+ Messages to send to the users.
72
+ skipusers: Set[User]
73
+ A set of users to skip sending messages to.
74
+ """
75
+ if isinstance(messages, str):
76
+ messages = [messages]
77
+
78
+ async with trio.open_nursery() as self._nursery:
79
+ self._log_messages(messages)
80
+ for user in self.users.difference(skipusers):
81
+ self._nursery.start_soon(partial(
82
+ user.send,
83
+ messages,
84
+ log=logger.isEnabledFor(logging.DEBUG)
85
+ ))
@@ -0,0 +1,14 @@
1
+ import logging
2
+ import os
3
+ from socket import gethostname, gethostbyname
4
+ from types import SimpleNamespace
5
+
6
+
7
+ config = SimpleNamespace()
8
+ config.HOST = os.getenv('HOST', gethostname())
9
+ config.ADDR = os.getenv('ADDR', gethostbyname(config.HOST))
10
+ config.PORT = int(os.getenv('PORT', 6667))
11
+ config.PASS = os.getenv('PASS')
12
+ config.TIMEOUT = int(os.getenv('TIMEOUT', 60))
13
+ config.PING_TIMEOUT = int(os.getenv('PING_TIMEOUT', 5))
14
+ config.LOGLEVEL = os.getenv('LOGLEVEL', 'WARNING')
@@ -0,0 +1,86 @@
1
+ __ALL__ = [
2
+ 'Disconnect', 'IRCException', 'ErrNoSuchChannel', 'ErrNoRecipient',
3
+ 'ErrNoTextToSend', 'ErrUnknownCommand', 'ErrNoNicknameGiven',
4
+ 'ErrErroneusNickname', 'ErrNicknameInUse', 'ErrNotOnChannel',
5
+ 'ErrNoLogin', 'ErrNeedMoreParams', 'ErrAlreadyRegistred',
6
+ ]
7
+
8
+ import ellinetircd
9
+ from typing import Any
10
+
11
+ class Disconnect(Exception):
12
+ pass
13
+
14
+ class IRCException(Exception):
15
+ """
16
+ Abstract IRC exception
17
+
18
+ They are excepted by dispatch() and forwarded to the user.
19
+ """
20
+ def __init__(self, *args: Any) -> None:
21
+ super().__init__(type(self).format(*args))
22
+
23
+ @classmethod
24
+ def format(cls, *args: Any) -> str:
25
+ return ":{host} {code} {error}".format(
26
+ host=ellinetircd.servlocal.get().host,
27
+ code=cls.code,
28
+ error=cls.msg.format(*args),
29
+ )
30
+
31
+ class ErrUnknownError(IRCException):
32
+ # <user> <command> :<info>
33
+ msg = "{} {} :{}"
34
+ code = 400
35
+
36
+ class ErrNoSuchNick(IRCException):
37
+ msg = "{} :No such nick/channel"
38
+ code = 401
39
+
40
+ class ErrNoSuchChannel(IRCException):
41
+ msg = "{} :No such channel"
42
+ code = 403
43
+
44
+ class ErrNoRecipient(IRCException):
45
+ msg = ":No recipient given ({})"
46
+ code = 411
47
+
48
+ class ErrNoTextToSend(IRCException):
49
+ msg = ":No text to send"
50
+ code = 412
51
+
52
+ class ErrUnknownCommand(IRCException):
53
+ msg = "{} :Unknown command"
54
+ code = 421
55
+
56
+ class ErrNoNicknameGiven(IRCException):
57
+ msg = ":No nickname given"
58
+ code = 431
59
+
60
+ class ErrErroneusNickname(IRCException):
61
+ msg = "{} :Erroneous nickname"
62
+ code = 432
63
+
64
+ class ErrNicknameInUse(IRCException):
65
+ msg = "{} :Nickname is already in use"
66
+ code = 433
67
+
68
+ class ErrNotOnChannel(IRCException):
69
+ msg = "{} :You're not on that channel"
70
+ code = 442
71
+
72
+ class ErrNoLogin(IRCException):
73
+ msg = ":User not logged in"
74
+ code = 444
75
+
76
+ class ErrNeedMoreParams(IRCException):
77
+ msg = "{} :Not enough parameters"
78
+ code = 461
79
+
80
+ class ErrAlreadyRegistred(IRCException):
81
+ msg = ":Unauthorized command (already registered)"
82
+ code = 462
83
+
84
+ class ErrPasswdMismatch(IRCException):
85
+ msg = ":Password incorrect"
86
+ code = 464
@@ -0,0 +1,32 @@
1
+ import socket
2
+ import os
3
+
4
+ def notify(payload: bytes) -> None:
5
+ if _sdsocket:
6
+ _sdsocket.sendall(payload)
7
+
8
+
9
+ def ready() -> None:
10
+ notify(b"READY=1")
11
+
12
+
13
+ def reloading() -> None:
14
+ notify(b"RELOADING=1")
15
+
16
+
17
+ def stopping() -> None:
18
+ notify(b"STOPPING=1")
19
+
20
+
21
+ def status(line: str) -> None:
22
+ notify(b"STATUS=" + line.encode())
23
+
24
+
25
+ # Setup
26
+ _sdsocket = None
27
+ _notify_socket = os.getenv('NOTIFY_SOCKET', '')
28
+ if _notify_socket:
29
+ if _notify_socket.startswith('@'):
30
+ _notify_socket = f'\0{_notify_socket[1:]}'
31
+ _sdsocket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
32
+ _sdsocket.connect(_notify_socket)
@@ -0,0 +1,77 @@
1
+ import dataclasses
2
+ import logging
3
+ import signal
4
+ import trio
5
+ from typing import Any, Dict, Optional
6
+
7
+ import ellinetircd
8
+ from ellinetircd import sdnotify
9
+ from ellinetircd.exceptions import Disconnect
10
+ from ellinetircd.user import User
11
+
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class Server:
17
+ def __init__(self, host: str, addr: str, port: int, pwd: Optional[str]) -> None:
18
+ self.host = host
19
+ self.addr = addr
20
+ self.port = port
21
+ self.pwd = pwd
22
+
23
+ async def handle(self, stream: trio.abc.Stream) -> None:
24
+ servlocal = ellinetircd.servlocal.get()
25
+ async with trio.open_nursery() as nursery:
26
+ user = User(stream, nursery)
27
+ logger.info("Connection with %s established.", user)
28
+ nursery.start_soon(user.ping_forever)
29
+ try:
30
+ await user.serve()
31
+ except Disconnect as exc:
32
+ logger.warning("Protocol violation while serving %s, %s.", user, repr(exc.__cause__ or exc))
33
+ await user.terminate(exc.args[0] if exc.args else "Protocol violation")
34
+ except Exception:
35
+ logger.exception("Error while serving %s.", user)
36
+ await user.terminate("Internal host error")
37
+ else:
38
+ await user.terminate()
39
+
40
+ logger.info("Connection with %s closed.", user)
41
+
42
+ def started(self, _listeners: Any) -> None:
43
+ sdnotify.ready()
44
+ ellinetircd.update_status()
45
+
46
+ async def _onterm(self) -> None:
47
+ with trio.open_signal_receiver(signal.SIGTERM, signal.SIGINT) as signal_aiter:
48
+ async for _ in signal_aiter:
49
+ if self._nursery.cancel_scope.cancel_called:
50
+ raise KeyboardInterrupt()
51
+ sdnotify.stopping()
52
+ sdnotify.status("Terminating connections...")
53
+ self._nursery.cancel_scope.cancel()
54
+
55
+ async def serve(self) -> None:
56
+ ellinetircd.servlocal.set(ServLocal(self.host, self.pwd, {}, {}))
57
+ async with trio.open_nursery() as self._nursery:
58
+ self._nursery.start_soon(self._onterm)
59
+ logger.info("Listening on %s port %s.", self.addr, self.port)
60
+ await trio.serve_tcp(self.handle, self.port, host=self.addr, task_status=self)
61
+
62
+
63
+ @dataclasses.dataclass(eq=False)
64
+ class ServLocal:
65
+ host: str
66
+ pwd: str # password, "pass" is a reserved keyword
67
+ users: Dict[str, User]
68
+ channels: Dict[str, "ellinetircd.channel.Channel"]
69
+
70
+ def __repr__(self) -> str:
71
+ return (
72
+ f'{self.__name__}('
73
+ f'host: {self.host!r}, '
74
+ f'pass: {"yes" if self.pwd else "no"}, '
75
+ f'users: {len(self.users)}, '
76
+ f'channels: {len(self.channels)})'
77
+ )