Compare commits

..

No commits in common. "9a4dca80544cd8a64f963e775e0207b4fd9eb61d" and "d435442b39f167509e64b21a9cd1cf3f71e33033" have entirely different histories.

17 changed files with 78 additions and 239 deletions

View File

@ -15,11 +15,7 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- python-version: "3.13.0-alpha.6" - python-version: "3.12.0-alpha.7"
with-opt-deps: false # https://github.com/pyca/cryptography/issues/10806
runs-on: ubuntu-22.04
- python-version: "3.12.0"
with-opt-deps: true with-opt-deps: true
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@ -71,7 +67,7 @@ jobs:
- name: Upgrade pip - name: Upgrade pip
run: | run: |
python3 -m pip install --upgrade pip setuptools python3 -m pip install --upgrade pip
- name: Install optional dependencies - name: Install optional dependencies
if: ${{ matrix.with-opt-deps }} if: ${{ matrix.with-opt-deps }}

View File

@ -15,10 +15,14 @@ Last rule: you shouldn't add a mandatory dependency. Limnoria does not
come with any (besides Python), so please try to keep all dependencies come with any (besides Python), so please try to keep all dependencies
optional. optional.
[Style Guidelines]:https://docs.limnoria.net/develop/style.html [Style Guidelines]:https://limnoria.readthedocs.io/en/latest/develop/style.html
## Sending patches ## Sending patches
When you send a pull request, **send it to the testing branch**.
It will be merged to master when it's considered to be stable enough to be
supported.
Don't fear that you spam Limnoria by sending many pull requests. According Don't fear that you spam Limnoria by sending many pull requests. According
to @ProgVal, it's easier for them to accept pull requests than to to @ProgVal, it's easier for them to accept pull requests than to
cherry-pick everything manually. cherry-pick everything manually.
@ -28,6 +32,6 @@ is very appreciated.
See also [Contributing to Limnoria] at [Limnoria documentation]. See also [Contributing to Limnoria] at [Limnoria documentation].
[Contributing to Limnoria]:https://docs.limnoria.net/contribute/index.html [Contributing to Limnoria]:https://limnoria.readthedocs.io/en/latest/contribute/index.html
[Limnoria documentation]:https://docs.limnoria.net/ [Limnoria documentation]:https://limnoria.readthedocs.io/

View File

@ -39,7 +39,7 @@ class DDGTestCase(PluginTestCase):
def testSearch(self): def testSearch(self):
self.assertRegexp( self.assertRegexp(
'ddg search wikipedia', r'Wikipedia.*? - .*?https?\:\/\/') 'ddg search wikipedia', 'Wikipedia.*? - .*?https?\:\/\/')
self.assertRegexp( self.assertRegexp(
'ddg search en.wikipedia.org', 'ddg search en.wikipedia.org',
'Wikipedia, the free encyclopedia\x02 - ' 'Wikipedia, the free encyclopedia\x02 - '
@ -47,6 +47,6 @@ class DDGTestCase(PluginTestCase):
with conf.supybot.plugins.DDG.region.context('fr-fr'): with conf.supybot.plugins.DDG.region.context('fr-fr'):
self.assertRegexp( self.assertRegexp(
'ddg search wikipedia', 'ddg search wikipedia',
r'Wikipédia, l\'encyclopédie libre - .*?https?\:\/\/') 'Wikipédia, l\'encyclopédie libre - .*?https?\:\/\/')
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

View File

@ -194,7 +194,7 @@ class Connection:
if code != 151 or code is None: if code != 151 or code is None:
break break
resultword, resultdb = re.search(r'^"(.+)" (\S+)', text).groups() resultword, resultdb = re.search('^"(.+)" (\S+)', text).groups()
defstr = self.get100block() defstr = self.get100block()
retval.append(Definition(self, self.getdbobj(resultdb), retval.append(Definition(self, self.getdbobj(resultdb),
resultword, defstr)) resultword, defstr))

View File

@ -33,11 +33,11 @@ import datetime
# Credits for the regexp and function: https://stackoverflow.com/a/2765366/539465 # Credits for the regexp and function: https://stackoverflow.com/a/2765366/539465
_XSD_DURATION_RE = re.compile( _XSD_DURATION_RE = re.compile(
r"(?P<sign>-?)P" "(?P<sign>-?)P"
r"(?:(?P<years>\d+)Y)?" "(?:(?P<years>\d+)Y)?"
r"(?:(?P<months>\d+)M)?" "(?:(?P<months>\d+)M)?"
r"(?:(?P<days>\d+)D)?" "(?:(?P<days>\d+)D)?"
r"(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?" "(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?"
) )

View File

@ -31,6 +31,7 @@
import time import time
import socket import socket
import telnetlib
import supybot.conf as conf import supybot.conf as conf
import supybot.utils as utils import supybot.utils as utils
@ -157,14 +158,14 @@ class Internet(callbacks.Plugin):
if not status: if not status:
status = 'unknown' status = 'unknown'
try: try:
sock = socket.create_connection(('whois.iana.org', 43)) t = telnetlib.Telnet('whois.iana.org', 43)
except socket.error as e: except socket.error as e:
irc.error(str(e)) irc.error(str(e))
return return
sock.sendall(b'registrar ') t.write(b'registrar ')
sock.sendall(registrar.split('(')[0].strip().encode('ascii')) t.write(registrar.split('(')[0].strip().encode('ascii'))
sock.sendall(b'\n') t.write(b'\n')
s = sock.recv(100000) s = t.read_all()
url = '' url = ''
for line in s.splitlines(): for line in s.splitlines():
line = line.decode('ascii').strip() line = line.decode('ascii').strip()

View File

@ -40,6 +40,7 @@ class InternetTestCase(PluginTestCase):
'Host not found.') 'Host not found.')
def testWhois(self): def testWhois(self):
self.assertNotError('internet whois ohio-state.edu')
self.assertNotError('internet whois microsoft.com') self.assertNotError('internet whois microsoft.com')
self.assertNotError('internet whois inria.fr') self.assertNotError('internet whois inria.fr')
self.assertNotError('internet whois slime.com.au') self.assertNotError('internet whois slime.com.au')

View File

@ -849,7 +849,7 @@ class UnitGroup:
def updateCurrentUnit(self, text, cursorPos): def updateCurrentUnit(self, text, cursorPos):
"Set current unit number" "Set current unit number"
self.currentNum = len(re.findall(r'[\*/]', text[:cursorPos])) self.currentNum = len(re.findall('[\*/]', text[:cursorPos]))
def currentUnit(self): def currentUnit(self):
"Return current unit if its a full match, o/w None" "Return current unit if its a full match, o/w None"
@ -925,7 +925,7 @@ class UnitGroup:
def parseGroup(self, text): def parseGroup(self, text):
"Return list of units from text string" "Return list of units from text string"
unitList = [] unitList = []
parts = [part.strip() for part in re.split(r'([\*/])', text)] parts = [part.strip() for part in re.split('([\*/])', text)]
numerator = 1 numerator = 1
while parts: while parts:
unit = self.parseUnit(parts.pop(0)) unit = self.parseUnit(parts.pop(0))
@ -1180,7 +1180,7 @@ class Unit:
self.equiv = unitList[0].strip() self.equiv = unitList[0].strip()
if self.equiv[0] == '[': # used only for non-linear units if self.equiv[0] == '[': # used only for non-linear units
try: try:
self.equiv, self.fromEqn = re.match(r'\[(.*?)\](.*)', \ self.equiv, self.fromEqn = re.match('\[(.*?)\](.*)', \
self.equiv).groups() self.equiv).groups()
if ';' in self.fromEqn: if ';' in self.fromEqn:
self.fromEqn, self.toEqn = self.fromEqn.split(';', 1) self.fromEqn, self.toEqn = self.fromEqn.split(';', 1)
@ -1190,7 +1190,7 @@ class Unit:
raise UnitDataError('Bad equation for "%s"' % self.name) raise UnitDataError('Bad equation for "%s"' % self.name)
else: # split factor and equiv unit for linear else: # split factor and equiv unit for linear
parts = self.equiv.split(None, 1) parts = self.equiv.split(None, 1)
if len(parts) > 1 and re.search(r'[^\d\.eE\+\-\*/]', parts[0]) \ if len(parts) > 1 and re.search('[^\d\.eE\+\-\*/]', parts[0]) \
== None: # only allowed digits and operators == None: # only allowed digits and operators
try: try:
self.factor = float(eval(parts[0])) self.factor = float(eval(parts[0]))

View File

@ -342,31 +342,21 @@ class Misc(callbacks.Plugin):
Returns the version of the current bot. Returns the version of the current bot.
""" """
try: try:
versions = [] newestUrl = 'https://api.github.com/repos/progval/Limnoria/' + \
'commits/%s'
# fetch from PyPI versions = {}
data = json.loads(utils.web.getUrl( for branch in ('master', 'testing'):
'https://pypi.org/pypi/limnoria/json' data = json.loads(utils.web.getUrl(newestUrl % branch)
).decode('utf8')) .decode('utf8'))
release_version = data['info']['version'] version = data['commit']['committer']['date']
# zero-left-pad months and days # Strip the last 'Z':
release_version = re.sub( version = version.rsplit('T', 1)[0].replace('-', '.')
r'\.([0-9])\b', lambda m: '.0' + m.group(1), release_version if minisix.PY2 and isinstance(version, unicode):
) version = version.encode('utf8')
versions[branch] = version
# fetch from Git newest = _('The newest versions available online are %s.') % \
data = json.loads(utils.web.getUrl( ', '.join([_('%s (in %s)') % (y,x)
'https://api.github.com/repos/progval/Limnoria/' for x,y in versions.items()])
'commits/master'
).decode('utf8'))
git_version = data['commit']['committer']['date']
# Strip the last 'Z':
git_version = git_version.rsplit('T', 1)[0].replace('-', '.')
newest = _(
'The newest version available online is %(release_version)s, '
'or %(git_version)s in Git'
) % {'release_version': release_version, 'git_version': git_version}
except utils.web.Error as e: except utils.web.Error as e:
self.log.info('Couldn\'t get website version: %s', e) self.log.info('Couldn\'t get website version: %s', e)
newest = _('I couldn\'t fetch the newest version ' newest = _('I couldn\'t fetch the newest version '

View File

@ -280,7 +280,7 @@ class RSS(callbacks.Plugin):
raise callbacks.Error(s) raise callbacks.Error(s)
if url: if url:
feed = self.feeds.get(url) feed = self.feeds.get(url)
if feed and feed.name != feed.url and feed.name in self.feed_names: if feed and feed.name != feed.url:
s = format(_('I already have a feed with that URL named %s.'), s = format(_('I already have a feed with that URL named %s.'),
feed.name) feed.name)
raise callbacks.Error(s) raise callbacks.Error(s)

View File

@ -84,7 +84,7 @@ def mock_urllib(f):
url = 'http://www.advogato.org/rss/articles.xml' url = 'http://www.advogato.org/rss/articles.xml'
class RSSTestCase(ChannelPluginTestCase): class RSSTestCase(ChannelPluginTestCase):
plugins = ('RSS', 'Plugin') plugins = ('RSS','Plugin')
timeout = 1 timeout = 1
@ -121,27 +121,6 @@ class RSSTestCase(ChannelPluginTestCase):
self.assertEqual(self.irc.getCallback('RSS').feed_names, {}) self.assertEqual(self.irc.getCallback('RSS').feed_names, {})
self.assertTrue(self.irc.getCallback('RSS').get_feed('http://xkcd.com/rss.xml')) self.assertTrue(self.irc.getCallback('RSS').get_feed('http://xkcd.com/rss.xml'))
@mock_urllib
def testChangeUrl(self, mock):
try:
self.assertNotError('rss add xkcd http://xkcd.com/rss.xml')
self.assertNotError('rss remove xkcd')
self.assertNotError('rss add xkcd https://xkcd.com/rss.xml')
self.assertRegexp('help xkcd', 'https://')
finally:
self._feedMsg('rss remove xkcd')
@mock_urllib
def testChangeName(self, mock):
try:
self.assertNotError('rss add xkcd http://xkcd.com/rss.xml')
self.assertNotError('rss remove xkcd')
self.assertNotError('rss add xkcd2 http://xkcd.com/rss.xml')
self.assertRegexp('help xkcd2', 'http://xkcd.com')
finally:
self._feedMsg('rss remove xkcd')
self._feedMsg('rss remove xkcd2')
@mock_urllib @mock_urllib
def testInitialAnnounceNewest(self, mock): def testInitialAnnounceNewest(self, mock):
mock._data = xkcd_new mock._data = xkcd_new

View File

@ -33,6 +33,7 @@ import os
import re import re
import pwd import pwd
import sys import sys
import crypt
import errno import errno
import random import random
import select import select
@ -40,12 +41,6 @@ import struct
import subprocess import subprocess
import shlex import shlex
try:
import crypt
except ImportError:
# Python >= 3.13
crypt = None
import supybot.conf as conf import supybot.conf as conf
import supybot.utils as utils import supybot.utils as utils
from supybot.commands import * from supybot.commands import *
@ -124,26 +119,25 @@ class Unix(callbacks.Plugin):
irc.reply(format('%i', os.getpid()), private=True) irc.reply(format('%i', os.getpid()), private=True)
pid = wrap(pid, [('checkCapability', 'owner')]) pid = wrap(pid, [('checkCapability', 'owner')])
if crypt is not None: # Python < 3.13 _cryptre = re.compile(b'[./0-9A-Za-z]')
_cryptre = re.compile(b'[./0-9A-Za-z]') @internationalizeDocstring
@internationalizeDocstring def crypt(self, irc, msg, args, password, salt):
def crypt(self, irc, msg, args, password, salt): """<password> [<salt>]
"""<password> [<salt>]
Returns the resulting of doing a crypt() on <password>. If <salt> is Returns the resulting of doing a crypt() on <password>. If <salt> is
not given, uses a random salt. If running on a glibc2 system, not given, uses a random salt. If running on a glibc2 system,
prepending '$1$' to your salt will cause crypt to return an MD5sum prepending '$1$' to your salt will cause crypt to return an MD5sum
based crypt rather than the standard DES based crypt. based crypt rather than the standard DES based crypt.
""" """
def makeSalt(): def makeSalt():
s = b'\x00' s = b'\x00'
while self._cryptre.sub(b'', s) != b'': while self._cryptre.sub(b'', s) != b'':
s = struct.pack('<h', random.randrange(-(2**15), 2**15)) s = struct.pack('<h', random.randrange(-(2**15), 2**15))
return s return s
if not salt: if not salt:
salt = makeSalt().decode() salt = makeSalt().decode()
irc.reply(crypt.crypt(password, salt)) irc.reply(crypt.crypt(password, salt))
crypt = wrap(crypt, ['something', additional('something')]) crypt = wrap(crypt, ['something', additional('something')])
@internationalizeDocstring @internationalizeDocstring
def spell(self, irc, msg, args, word): def spell(self, irc, msg, args, word):

View File

@ -31,11 +31,6 @@
import os import os
import socket import socket
try:
import crypt
except ImportError:
crypt = None
from supybot.test import * from supybot.test import *
try: try:
@ -111,9 +106,8 @@ if os.name == 'posix':
def testProgstats(self): def testProgstats(self):
self.assertNotError('progstats') self.assertNotError('progstats')
if crypt is not None: # Python < 3.13 def testCrypt(self):
def testCrypt(self): self.assertNotError('crypt jemfinch')
self.assertNotError('crypt jemfinch')
@skipUnlessFortune @skipUnlessFortune
def testFortune(self): def testFortune(self):

View File

@ -49,12 +49,10 @@ except ImportError:
install. This package is pretty standard, and often installed alongside install. This package is pretty standard, and often installed alongside
Python, but it is missing on your system. Python, but it is missing on your system.
Try installing it with your package manager, it is usually called Try installing it with your package manager, it is usually called
'python3-setuptools'; or with '%s -m pip install setuptools'. 'python3-setuptools'. If that does not work, try installing python3-pip
If that does not work, try installing python3-pip
instead, either with your package manager or by following these instead, either with your package manager or by following these
instructions: https://pip.pypa.io/en/stable/installation/ (replace instructions: https://pip.pypa.io/en/stable/installation/ (replace
'python' with 'python3' in all the commands)""" 'python' with 'python3' in all the commands)""")
% sys.executable)
sys.stderr.write(os.linesep*2) sys.stderr.write(os.linesep*2)
sys.stderr.write(textwrap.fill(s)) sys.stderr.write(textwrap.fill(s))
sys.stderr.write(os.linesep*2) sys.stderr.write(os.linesep*2)

View File

@ -419,15 +419,6 @@ def registerNetwork(name, password='', ssl=True, sasl_username='',
registry.String('', _("""Determines what user modes the bot will request registry.String('', _("""Determines what user modes the bot will request
from the server when it first connects. If empty, defaults to from the server when it first connects. If empty, defaults to
supybot.protocols.irc.umodes"""))) supybot.protocols.irc.umodes""")))
registerGlobalValue(network, 'vhost',
registry.String('', _("""Determines what vhost the bot will bind to before
connecting a server (IRC, HTTP, ...) via IPv4. If empty, defaults to
supybot.protocols.irc.vhost""")))
registerGlobalValue(network, 'vhostv6',
registry.String('', _("""Determines what vhost the bot will bind to before
connecting a server (IRC, HTTP, ...) via IPv6. If empty, defaults to
supybot.protocols.irc.vhostv6""")))
sasl = registerGroup(network, 'sasl') sasl = registerGroup(network, 'sasl')
registerGlobalValue(sasl, 'username', registry.String(sasl_username, registerGlobalValue(sasl, 'username', registry.String(sasl_username,
_("""Determines what SASL username will be used on %s. This should _("""Determines what SASL username will be used on %s. This should

View File

@ -312,10 +312,8 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin):
address, address,
port=self.currentServer.port, port=self.currentServer.port,
socks_proxy=socks_proxy, socks_proxy=socks_proxy,
vhost=self.networkGroup.get('vhost')() vhost=conf.supybot.protocols.irc.vhost(),
or conf.supybot.protocols.irc.vhost(), vhostv6=conf.supybot.protocols.irc.vhostv6(),
vhostv6=self.networkGroup.get('vhostv6')()
or conf.supybot.protocols.irc.vhostv6(),
) )
except socket.error as e: except socket.error as e:
drivers.log.connectError(self.currentServer, e) drivers.log.connectError(self.currentServer, e)

View File

@ -1,5 +1,5 @@
### ###
# Copyright (c) 2011-2024, Valentin Lorentz # Copyright (c) 2011-2021, Valentin Lorentz
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without # Redistribution and use in source and binary forms, with or without
@ -32,8 +32,8 @@ An embedded and centralized HTTP server for Supybot's plugins.
""" """
import os import os
import cgi
import socket import socket
import urllib.parse
from threading import Thread from threading import Thread
import supybot.log as log import supybot.log as log
@ -164,114 +164,6 @@ def get_template(filename):
with open(path + '.example', 'r') as fd: with open(path + '.example', 'r') as fd:
return fd.read() return fd.read()
class HttpHeader:
__slots__ = ('name', 'value')
def __init__(self, name, value):
self.name = name
self.value = value
def __repr__(self):
"""Return printable representation."""
return "HttpHeader(%r, %r)" % (self.name, self.value)
class HttpHeaders:
"""Copy of `cgi.FieldStorage
<https://github.com/python/cpython/blob/v3.12.3/Lib/cgi.py#L512-L594>`
before it was removed from the stdlib.
"""
__slots__ = ('list',)
def __init__(self, headers):
self.list = headers
def __repr__(self):
return 'HttpHeaders(%r)' % self.list
def __iter__(self):
return iter(self.keys())
def __getattr__(self, name):
if name != 'value':
raise AttributeError(name)
if self.file:
self.file.seek(0)
value = self.file.read()
self.file.seek(0)
elif self.list is not None:
value = self.list
else:
value = None
return value
def __getitem__(self, key):
"""Dictionary style indexing."""
if self.list is None:
raise TypeError("not indexable")
found = []
for item in self.list:
if item.name == key: found.append(item)
if not found:
raise KeyError(key)
if len(found) == 1:
return found[0]
else:
return found
def getvalue(self, key, default=None):
"""Dictionary style get() method, including 'value' lookup."""
if key in self:
value = self[key]
if isinstance(value, list):
return [x.value for x in value]
else:
return value.value
else:
return default
def getfirst(self, key, default=None):
""" Return the first value received."""
if key in self:
value = self[key]
if isinstance(value, list):
return value[0].value
else:
return value.value
else:
return default
def getlist(self, key):
""" Return list of received values."""
if key in self:
value = self[key]
if isinstance(value, list):
return [x.value for x in value]
else:
return [value.value]
else:
return []
def keys(self):
"""Dictionary style keys() method."""
if self.list is None:
raise TypeError("not indexable")
return list(set(item.name for item in self.list))
def __contains__(self, key):
"""Dictionary style __contains__ method."""
if self.list is None:
raise TypeError("not indexable")
return any(item.name == key for item in self.list)
def __len__(self):
"""Dictionary style len(x) support."""
return len(self.keys())
def __bool__(self):
if self.list is None:
raise TypeError("Cannot be converted to bool.")
return bool(self.list)
class SupyHTTPRequestHandler(BaseHTTPRequestHandler): class SupyHTTPRequestHandler(BaseHTTPRequestHandler):
def do_X(self, callbackMethod, *args, **kwargs): def do_X(self, callbackMethod, *args, **kwargs):
if self.path == '/': if self.path == '/':
@ -307,11 +199,12 @@ class SupyHTTPRequestHandler(BaseHTTPRequestHandler):
if 'Content-Type' not in self.headers: if 'Content-Type' not in self.headers:
self.headers['Content-Type'] = 'application/x-www-form-urlencoded' self.headers['Content-Type'] = 'application/x-www-form-urlencoded'
if self.headers['Content-Type'] == 'application/x-www-form-urlencoded': if self.headers['Content-Type'] == 'application/x-www-form-urlencoded':
length = min(100000, int(self.headers.get('Content-Length', '100000'))) form = cgi.FieldStorage(
qs = self.rfile.read(length).decode() fp=self.rfile,
form = HttpHeaders([ headers=self.headers,
HttpHeader(k, v) for (k, v) in urllib.parse.parse_qsl(qs) environ={'REQUEST_METHOD':'POST',
]) 'CONTENT_TYPE':self.headers['Content-Type'],
})
else: else:
content_length = int(self.headers.get('Content-Length', '0')) content_length = int(self.headers.get('Content-Length', '0'))
form = self.rfile.read(content_length) form = self.rfile.read(content_length)