Skip to content

Commit

Permalink
Try and regain current nick (fixes #26)
Browse files Browse the repository at this point in the history
  • Loading branch information
luk3yx committed Dec 14, 2022
1 parent 488c57a commit e0e1483
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 38 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ Notes:
- This changelog may contain typographical errors, it is still a
work-in-progress.

## 1.9.0 - 2022-12-13

### Added

- miniirc will now attempt to regain the originally specified nickname if it
cannot used when connecting. For compatibility, `irc.nick` will return the
current nickname while connected, however changing it will change the
desired nickname. This may change in the future.

### Changed

- The current nickname is now obtained from the 001 response after connecting.

## 1.8.4 - 2022-08-22

### Changed
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ You don't need to add every argument, and the `ip`, `port`, `nick`, and
`channels` arguments should be specified as positional arguments.

```py
irc = miniirc.IRC('irc.example.com', 6697, 'my-bot', ['#my-channel'], ns_identity=('my-bot', 'hunter2'), executor=concurrent.futures.ThreadPoolExecutor())
irc = miniirc.IRC('irc.example.com', 6697, 'my-bot', ['#my-channel'], ns_identity=('my-bot', 'hunter2'), executor=concurrent.futures.ThreadPoolExecutor(), keepnick=True)
```

If you are not doing anything with the main thread after connecting to IRC,
Expand Down Expand Up @@ -64,6 +64,7 @@ irc.wait_until_disconnected()
| `ping_timeout` | The ping timeout used alongside the above `ping_interval` option, if unspecified will default to `ping_interval`. |
| `verify_ssl` | Verifies TLS/SSL certificates. Disabling this is not recommended as it opens the IRC connection up to MiTM attacks. If you have trouble with certificate verification, try running `pip3 install certifi` first. |
| `executor` | An instance of `concurrent.futures.ThreadPoolExecutor` to use when running handlers. |
| `keepnick` | If enabled, miniirc will attempt to obtain the original nick like ZNC's *keepnick. `irc.nick` will be the original nickname rather than the current one. |

*The only mandatory parameters are `ip`, `port`, and `nick`.*

Expand Down Expand Up @@ -121,10 +122,10 @@ space however not interpreted as one).
| ------------- | -------------------------------------------------------- |
| `active_caps` | A `set` of IRCv3 capabilities that have been successfully negotiated with the IRC server. This is empty while disconnected. |
| `connected` | A boolean (or `None`), `True` when miniirc is connected, `False` when miniirc is connecting, and `None` when miniirc is not connected. |
| `current_nick` | *New in v1.4.3.* The bot/client's current nickname, currently an alias for `irc.nick`. Do not modify this, and use this instead of `irc.nick` when getting the bot's current nickname for compatibility with miniirc v2.0.0. |
| `current_nick` | The bot/client's current nickname. Do not modify this, and use this instead of `irc.nick` when getting the bot's current nickname. |
| `isupport` | A `dict` with values (not necessarily strings) from `ISUPPORT` messages sent to the client. |
| `msglen` | The maximum length (in bytes) of messages (including `\r\n`). This is automatically changed if the server supports the `oragono.io/maxline-2` capability. |
| `nick` | The nickname to use when connecting to IRC. Until miniirc v2.0.0, you should only modify this while disconnected, as it is currently automatically updated with nickname changes. |
| `nick` | The nickname to use when connecting to IRC. Until miniirc v2.0.0, you should only use or modify this while disconnected, as it is currently automatically updated with nickname changes. |

The following arguments passed to `miniirc.IRC` are also available: `ip`,
`port`, `channels`, `ssl`, `ident`, `realname`, `persist`, `connect_modes`,
Expand Down Expand Up @@ -354,15 +355,15 @@ is still in beta and there will be breaking API changes in the future.

## Deprecations

When miniirc v2.0.0 is released, the following breaking changes will (probably)
be made:
If miniirc v2.0.0 is ever released, the following breaking changes will
(probably) be made:

- Internal-only attributes `irc.handlers`, `irc.sock`, and `irc.sendq`
(please do not use these) will be renamed. Again, please do not use these.
- `irc.nick` will be the nickname used when connecting to IRC rather than the
current nickname, use `irc.current_nick` for the current nickname (since
v1.4.3). This will stop lots of underscores being automatically appended to
nicknames.
- `irc.nick` will always be the nickname used when connecting to IRC rather
than the current nickname, use `irc.current_nick` for the current nickname
(since v1.4.3).
- The `keepnick` parameter will default to True.
- `irc.ns_identity` will be stored as a tuple instead of a string, for example
`('username', 'password with spaces')` instead of
`'username password with spaces'`. Both formats are currently accepted and
Expand Down
90 changes: 72 additions & 18 deletions miniirc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
# © 2018-2022 by luk3yx and other contributors of miniirc.
#

import atexit, errno, threading, time, select, socket, ssl, sys, warnings
import atexit, threading, time, select, socket, ssl, sys, warnings

# The version string and tuple
ver = __version_info__ = (1, 8, 4)
version = 'miniirc IRC framework v1.8.4'
__version__ = '1.8.4'
ver = __version_info__ = (1, 9, 0)
version = 'miniirc IRC framework v1.9.0'
__version__ = '1.9.0'

# __all__ and _default_caps
__all__ = ['CmdHandler', 'Handler', 'IRC']
Expand Down Expand Up @@ -204,7 +204,17 @@ class IRC:
_unhandled_caps = None

# This will no longer be an alias in miniirc v2.0.0.
current_nick = property(lambda self: self.nick)
# This is still a property to avoid breaking miniirc_matrix
current_nick = property(lambda self: self._current_nick)

# For backwards compatibility, irc.nick will return the current nickname.
# However, changing irc.nick will change the desired nickname as well
# TODO: Consider changing what irc.nick does if it won't break anything or
# making desired_nick public
@current_nick.setter
def nick(self, new_nick):
self._desired_nick = new_nick
self._current_nick = new_nick

def __init__(self, ip, port, nick, channels=None, *, ssl=None, ident=None,
realname=None, persist=True, debug=False, ns_identity=None,
Expand All @@ -230,6 +240,7 @@ def __init__(self, ip, port, nick, channels=None, *, ssl=None, ident=None,
self.ping_interval = ping_interval
self.ping_timeout = ping_timeout
self.verify_ssl = verify_ssl
self._keepnick_active = False
self._executor = executor

# Set the NickServ identity
Expand Down Expand Up @@ -387,14 +398,15 @@ def connect(self):
self.sock = ctx.wrap_socket(self.sock, server_hostname=self.ip)

# Begin IRCv3 CAP negotiation.
self._current_nick = self._desired_nick
self._unhandled_caps = None
self.quote('CAP LS 302', force=True)
self.quote('USER', self.ident, '0', '*', ':' + self.realname,
force=True)
self.quote('NICK', self.nick, force=True)
self.quote('NICK', self._desired_nick, force=True)
atexit.register(self.disconnect)
self.debug('Starting main loop...')
self._sasl = self._pinged = False
self._sasl = self._pinged = self._keepnick_active = False
self._start_main_loop()

def _start_main_loop(self):
Expand All @@ -410,6 +422,7 @@ def disconnect(self, msg=None, *, auto_reconnect=False):
self.connected = None
self.active_caps.clear()
atexit.unregister(self.disconnect)
self._current_nick = self._desired_nick
self._unhandled_caps = None
try:
self.quote('QUIT :' + str(msg or self.quit_message), force=True)
Expand Down Expand Up @@ -560,6 +573,12 @@ def _main(self):
self.debug('Ignored message:', line)
del raw

# Attempt to change nicknames every 30 seconds
if (self._keepnick_active and
time.monotonic() > self._last_keepnick_attempt + 30):
self.send('NICK', self._desired_nick, force=True)
self._last_keepnick_attempt = time.monotonic()

def wait_until_disconnected(self, *, _timeout=None):
# The main thread may be replaced on reconnects
while self._main_thread and self._main_thread.is_alive():
Expand All @@ -582,15 +601,27 @@ def _handler(irc, hostmask, args):
irc.isupport.clear()
irc._unhandled_caps = None
irc.debug('Connected!')

# Update the current nickname and activate keepnick if required
irc._last_keepnick_attempt = time.monotonic()
irc._keepnick_active = args[0] != irc._desired_nick
irc._current_nick = args[0]

# Apply connection modes
if irc.connect_modes:
irc.quote('MODE', irc.nick, irc.connect_modes)
irc.quote('MODE', irc.current_nick, irc.connect_modes)

# Log into NickServ if required
if not irc._sasl and irc.ns_identity:
irc.debug('Logging in (no SASL, aww)...')
irc.msg('NickServ', 'identify ' + irc.ns_identity)

# Join channels
if irc.channels:
irc.debug('*** Joining channels...', irc.channels)
irc.quote('JOIN', ','.join(irc.channels))

# Send any queued messages
with irc._send_lock:
sendq, irc.sendq = irc.sendq, None
if sendq:
Expand All @@ -613,21 +644,26 @@ def _handler(irc, hostmask, args):
def _handler(irc, hostmask, args):
if not irc.connected:
try:
return int(irc.nick[0])
return int(irc._current_nick[0])
except ValueError:
pass
if len(irc.nick) >= irc.isupport.get('NICKLEN', 20):
if len(irc._current_nick) >= irc.isupport.get('NICKLEN', 20):
return
irc.debug('WARNING: The requested nickname', repr(irc.nick), 'is '
'invalid. Trying again with', repr(irc.nick + '_') + '...')
irc.nick += '_'
irc.quote('NICK', irc.nick, force=True)
irc.debug('WARNING: The requested nickname', repr(irc._current_nick),
'is invalid. Trying again with',
repr(irc._current_nick + '_') + '...')
irc._current_nick += '_'
irc.quote('NICK', irc.current_nick, force=True)


@Handler('NICK', colon=False)
def _handler(irc, hostmask, args):
if hostmask[0].lower() == irc.nick.lower():
irc.nick = args[-1]
if hostmask[0].lower() == irc._current_nick.lower():
irc._current_nick = args[-1]

# Deactivate keepnick if the client has the right nickname
if irc._current_nick.lower() == irc._desired_nick.lower():
irc._keepnick_active = False


@Handler('PRIVMSG', colon=False)
Expand Down Expand Up @@ -759,8 +795,10 @@ def _handler(irc, hostmask, args):
for key in isupport:
try:
isupport[key] = int(isupport[key])
if key == 'NICKLEN':
irc.nick = irc.nick[:isupport[key]]

# Disable keepnick if the nickname is too long
if key == 'NICKLEN' and len(irc._desired_nick) > isupport[key]:
irc._keepnick_active = False
except ValueError:
if key.endswith('LEN'):
remove.add(key)
Expand All @@ -770,5 +808,21 @@ def _handler(irc, hostmask, args):
irc.isupport.update(isupport)


# Attempt to get the current nickname if the user that currently has it quits
@Handler('QUIT', 'NICK')
def _handler(irc, hostmask, args):
if (irc.connected and irc._keepnick_active and
hostmask[0].lower() == irc._desired_nick.lower()):
irc.send('NICK', irc._desired_nick, force=True)
irc._last_keepnick_attempt = time.monotonic()


# Stop trying to get the desired nickname if it's invalid or if nick changes
# aren't permitted
@Handler('432', '435', '447')
def _handler(irc, hostmask, args):
irc._keepnick_active = False


_colon_warning = True
del _handler
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setup(
name='miniirc',
version='1.8.4',
version='1.9.0',
py_modules=['miniirc'],
author='luk3yx',
description='A lightweight IRC framework.',
Expand Down
41 changes: 31 additions & 10 deletions test_miniirc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/false
import collections, functools, miniirc, pathlib, queue, random, re, select, \
socket, threading, time
import collections, functools, miniirc, pathlib, pytest, queue, random, re, \
select, socket, threading, time

MINIIRC_V2 = miniirc.ver >= (2, 0, 0)
if MINIIRC_V2:
Expand Down Expand Up @@ -241,6 +241,23 @@ def main(self):
assert not hasattr(irc, '_main_lock')


def test_nick_backwards_compatibility():
irc = DummyIRC()

# Changing nick should update everything
irc.nick = 'test1'
assert irc.current_nick == irc.nick == irc._desired_nick == 'test1'

# Changing _current_nick should update current_nick and nick
irc._current_nick = 'test2'
assert irc.current_nick == 'test2'
assert irc.nick == 'test2'
assert irc._desired_nick == 'test1'

irc.disconnect()
assert irc.current_nick == irc.nick == 'test1'


def test_connection(monkeypatch):
irc = err = None

Expand Down Expand Up @@ -269,12 +286,13 @@ def wrapper(self, *args, **kwargs):
'AUTHENTICATE PLAIN': 'AUTHENTICATE +',
'AUTHENTICATE dGVzdAB0ZXN0AGh1bnRlcjI=': '903',
'CAP END': (
'001 parameter test :with colon\n'
'001 miniirc-test_ parameter test :with colon\n'
'005 * CAP=END :isupport description\n'
),
'USER miniirc-test 0 * :miniirc-test':
':a PRIVMSG miniirc-test :\x01VERSION\x01',
'NICK miniirc-test': '432',
'NICK miniirc-test': '433',
'NICK :miniirc-test': '433',
'NICK miniirc-test_': '',
'NOTICE a :\x01VERSION ' + miniirc.version + '\x01':
'005 miniirc-test CTCP=VERSION :are supported by this server',
Expand Down Expand Up @@ -315,7 +333,7 @@ def send(self, data):
msg = msg[:-2]
assert msg in fixed_responses
if self._recvq is None:
return
raise BrokenPipeError
for line in fixed_responses[msg].split('\n'):
self._recvq.put(line.encode('utf-8') +
random.choice((b'\r', b'\n', b'\r\n', b'\n\r')))
Expand Down Expand Up @@ -369,8 +387,8 @@ def fake_select(read, write, err, timeout):
try:
event = threading.Event()
irc = miniirc.IRC('example.com', 6667, 'miniirc-test',
auto_connect=False, ns_identity=('test', 'hunter2'), persist=False,
debug=True)
auto_connect=False, ns_identity=('test', 'hunter2'),
persist=False, debug=True)
assert irc.connected is None

@irc.Handler('001', colon=False)
Expand All @@ -380,7 +398,7 @@ def _handle_001(irc, hostmask, args):
if 'CAP' in irc.isupport and 'CTCP' in irc.isupport:
break
time.sleep(0.001)
assert args == ['parameter', 'test', 'with colon']
assert args == ['miniirc-test_', 'parameter', 'test', 'with colon']
assert irc.isupport == {'CTCP': 'VERSION', 'CAP': 'END'}
event.set()

Expand All @@ -389,11 +407,14 @@ def _handle_001(irc, hostmask, args):
if err is not None:
raise err
assert irc.connected
assert irc._keepnick_active
if MINIIRC_V2:
assert irc.nick == 'miniirc-test'
assert irc.current_nick == 'miniirc-test_'
else:
assert irc.nick == irc.current_nick == 'miniirc-test_'
assert irc._desired_nick == 'miniirc-test'
assert irc._current_nick == 'miniirc-test_'
assert irc.current_nick == 'miniirc-test_'
finally:
irc.disconnect()
socket_event.set()
assert not irc.connected

0 comments on commit e0e1483

Please sign in to comment.