mirror of
https://github.com/Mikaela/Limnoria.git
synced 2025-08-17 02:17:32 +02:00
Compare commits
11 Commits
d435442b39
...
9a4dca8054
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9a4dca8054 | ||
![]() |
dcd95d3a77 | ||
![]() |
5b2b38ab37 | ||
![]() |
4898926f20 | ||
![]() |
b1ba8ecb2a | ||
![]() |
9ae7690484 | ||
![]() |
e18332efde | ||
![]() |
0ad61f5791 | ||
![]() |
9bcb21389a | ||
![]() |
f65089af86 | ||
![]() |
07834620f3 |
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@ -15,7 +15,11 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- python-version: "3.12.0-alpha.7"
|
- python-version: "3.13.0-alpha.6"
|
||||||
|
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
|
||||||
|
|
||||||
@ -67,7 +71,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upgrade pip
|
- name: Upgrade pip
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip setuptools
|
||||||
|
|
||||||
- name: Install optional dependencies
|
- name: Install optional dependencies
|
||||||
if: ${{ matrix.with-opt-deps }}
|
if: ${{ matrix.with-opt-deps }}
|
||||||
|
@ -15,14 +15,10 @@ 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://limnoria.readthedocs.io/en/latest/develop/style.html
|
[Style Guidelines]:https://docs.limnoria.net/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.
|
||||||
@ -32,6 +28,6 @@ is very appreciated.
|
|||||||
|
|
||||||
See also [Contributing to Limnoria] at [Limnoria documentation].
|
See also [Contributing to Limnoria] at [Limnoria documentation].
|
||||||
|
|
||||||
[Contributing to Limnoria]:https://limnoria.readthedocs.io/en/latest/contribute/index.html
|
[Contributing to Limnoria]:https://docs.limnoria.net/contribute/index.html
|
||||||
|
|
||||||
[Limnoria documentation]:https://limnoria.readthedocs.io/
|
[Limnoria documentation]:https://docs.limnoria.net/
|
||||||
|
@ -39,7 +39,7 @@ class DDGTestCase(PluginTestCase):
|
|||||||
|
|
||||||
def testSearch(self):
|
def testSearch(self):
|
||||||
self.assertRegexp(
|
self.assertRegexp(
|
||||||
'ddg search wikipedia', 'Wikipedia.*? - .*?https?\:\/\/')
|
'ddg search wikipedia', r'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',
|
||||||
'Wikipédia, l\'encyclopédie libre - .*?https?\:\/\/')
|
r'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:
|
||||||
|
@ -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('^"(.+)" (\S+)', text).groups()
|
resultword, resultdb = re.search(r'^"(.+)" (\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))
|
||||||
|
@ -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(
|
||||||
"(?P<sign>-?)P"
|
r"(?P<sign>-?)P"
|
||||||
"(?:(?P<years>\d+)Y)?"
|
r"(?:(?P<years>\d+)Y)?"
|
||||||
"(?:(?P<months>\d+)M)?"
|
r"(?:(?P<months>\d+)M)?"
|
||||||
"(?:(?P<days>\d+)D)?"
|
r"(?:(?P<days>\d+)D)?"
|
||||||
"(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?"
|
r"(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@
|
|||||||
|
|
||||||
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
|
||||||
@ -158,14 +157,14 @@ class Internet(callbacks.Plugin):
|
|||||||
if not status:
|
if not status:
|
||||||
status = 'unknown'
|
status = 'unknown'
|
||||||
try:
|
try:
|
||||||
t = telnetlib.Telnet('whois.iana.org', 43)
|
sock = socket.create_connection(('whois.iana.org', 43))
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
irc.error(str(e))
|
irc.error(str(e))
|
||||||
return
|
return
|
||||||
t.write(b'registrar ')
|
sock.sendall(b'registrar ')
|
||||||
t.write(registrar.split('(')[0].strip().encode('ascii'))
|
sock.sendall(registrar.split('(')[0].strip().encode('ascii'))
|
||||||
t.write(b'\n')
|
sock.sendall(b'\n')
|
||||||
s = t.read_all()
|
s = sock.recv(100000)
|
||||||
url = ''
|
url = ''
|
||||||
for line in s.splitlines():
|
for line in s.splitlines():
|
||||||
line = line.decode('ascii').strip()
|
line = line.decode('ascii').strip()
|
||||||
|
@ -40,7 +40,6 @@ 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')
|
||||||
|
@ -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('[\*/]', text[:cursorPos]))
|
self.currentNum = len(re.findall(r'[\*/]', 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('([\*/])', text)]
|
parts = [part.strip() for part in re.split(r'([\*/])', 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('\[(.*?)\](.*)', \
|
self.equiv, self.fromEqn = re.match(r'\[(.*?)\](.*)', \
|
||||||
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('[^\d\.eE\+\-\*/]', parts[0]) \
|
if len(parts) > 1 and re.search(r'[^\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]))
|
||||||
|
@ -342,21 +342,31 @@ class Misc(callbacks.Plugin):
|
|||||||
Returns the version of the current bot.
|
Returns the version of the current bot.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
newestUrl = 'https://api.github.com/repos/progval/Limnoria/' + \
|
versions = []
|
||||||
'commits/%s'
|
|
||||||
versions = {}
|
# fetch from PyPI
|
||||||
for branch in ('master', 'testing'):
|
data = json.loads(utils.web.getUrl(
|
||||||
data = json.loads(utils.web.getUrl(newestUrl % branch)
|
'https://pypi.org/pypi/limnoria/json'
|
||||||
.decode('utf8'))
|
).decode('utf8'))
|
||||||
version = data['commit']['committer']['date']
|
release_version = data['info']['version']
|
||||||
|
# zero-left-pad months and days
|
||||||
|
release_version = re.sub(
|
||||||
|
r'\.([0-9])\b', lambda m: '.0' + m.group(1), release_version
|
||||||
|
)
|
||||||
|
|
||||||
|
# fetch from Git
|
||||||
|
data = json.loads(utils.web.getUrl(
|
||||||
|
'https://api.github.com/repos/progval/Limnoria/'
|
||||||
|
'commits/master'
|
||||||
|
).decode('utf8'))
|
||||||
|
git_version = data['commit']['committer']['date']
|
||||||
# Strip the last 'Z':
|
# Strip the last 'Z':
|
||||||
version = version.rsplit('T', 1)[0].replace('-', '.')
|
git_version = git_version.rsplit('T', 1)[0].replace('-', '.')
|
||||||
if minisix.PY2 and isinstance(version, unicode):
|
|
||||||
version = version.encode('utf8')
|
newest = _(
|
||||||
versions[branch] = version
|
'The newest version available online is %(release_version)s, '
|
||||||
newest = _('The newest versions available online are %s.') % \
|
'or %(git_version)s in Git'
|
||||||
', '.join([_('%s (in %s)') % (y,x)
|
) % {'release_version': release_version, 'git_version': git_version}
|
||||||
for x,y in versions.items()])
|
|
||||||
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 '
|
||||||
|
@ -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:
|
if feed and feed.name != feed.url and feed.name in self.feed_names:
|
||||||
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)
|
||||||
|
@ -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,6 +121,27 @@ 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
|
||||||
|
@ -33,7 +33,6 @@ 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
|
||||||
@ -41,6 +40,12 @@ 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 *
|
||||||
@ -119,6 +124,7 @@ 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):
|
||||||
|
@ -31,6 +31,11 @@
|
|||||||
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:
|
||||||
@ -106,6 +111,7 @@ 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')
|
||||||
|
|
||||||
|
6
setup.py
6
setup.py
@ -49,10 +49,12 @@ 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'. If that does not work, try installing python3-pip
|
'python3-setuptools'; or with '%s -m pip install setuptools'.
|
||||||
|
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)
|
||||||
|
@ -419,6 +419,15 @@ 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
|
||||||
|
@ -312,8 +312,10 @@ 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=conf.supybot.protocols.irc.vhost(),
|
vhost=self.networkGroup.get('vhost')()
|
||||||
vhostv6=conf.supybot.protocols.irc.vhostv6(),
|
or conf.supybot.protocols.irc.vhost(),
|
||||||
|
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)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2011-2021, Valentin Lorentz
|
# Copyright (c) 2011-2024, 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,6 +164,114 @@ 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 == '/':
|
||||||
@ -199,12 +307,11 @@ 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':
|
||||||
form = cgi.FieldStorage(
|
length = min(100000, int(self.headers.get('Content-Length', '100000')))
|
||||||
fp=self.rfile,
|
qs = self.rfile.read(length).decode()
|
||||||
headers=self.headers,
|
form = HttpHeaders([
|
||||||
environ={'REQUEST_METHOD':'POST',
|
HttpHeader(k, v) for (k, v) in urllib.parse.parse_qsl(qs)
|
||||||
'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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user