Initial commit with functional framework(!) and example plugin
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
286
.gitignore
vendored
Normal file
286
.gitignore
vendored
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# Don't add production config.toml to the repo
|
||||||
|
config.toml
|
||||||
|
|
||||||
|
|
||||||
|
## Standard gitignore for tools follows.
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
|
||||||
|
### Vim ###
|
||||||
|
# Swap
|
||||||
|
[._]*.s[a-v][a-z]
|
||||||
|
!*.svg # comment out if you don't need vector files
|
||||||
|
[._]*.sw[a-p]
|
||||||
|
[._]s[a-rt-v][a-z]
|
||||||
|
[._]ss[a-gi-z]
|
||||||
|
[._]sw[a-p]
|
||||||
|
|
||||||
|
# Session
|
||||||
|
Session.vim
|
||||||
|
Sessionx.vim
|
||||||
|
|
||||||
|
# Temporary
|
||||||
|
.netrwhist
|
||||||
|
*~
|
||||||
|
# Auto-generated tag files
|
||||||
|
tags
|
||||||
|
# Persistent undo
|
||||||
|
[._]*.un~
|
||||||
|
|
||||||
|
### VisualStudioCode ###
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Built Visual Studio Code Extensions
|
||||||
|
*.vsix
|
||||||
|
|
||||||
|
### VisualStudioCode Patch ###
|
||||||
|
# Ignore all local history of files
|
||||||
|
.history
|
||||||
|
.ionide
|
||||||
|
|
||||||
|
# Support for Project snippet scope
|
||||||
|
.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Ignore code-workspaces
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/python,django,vim,visualstudiocode
|
||||||
19
Pipfile
Normal file
19
Pipfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
async-irc = "*"
|
||||||
|
toml = "*"
|
||||||
|
types-toml = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
mypy = "*"
|
||||||
|
black = "*"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.10"
|
||||||
|
|
||||||
|
[pipenv]
|
||||||
|
allow_prereleases = true
|
||||||
158
Pipfile.lock
generated
Normal file
158
Pipfile.lock
generated
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "383b4f4eb921d1fb5480b141cb699db971c38445f9f5debb5e13de4a878b9ace"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.10"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"async-irc": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4da55d270405446cd6a48c2766416a026b832502139cb9469f1d998d74ac39e9",
|
||||||
|
"sha256:9f96e2cdd9c42a20c8bb3a9f668071d49357546c3e9d91477d0a0d1cb965f0a6"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.1.7"
|
||||||
|
},
|
||||||
|
"py-irclib": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2236c42883d218501bd59d302f486c740e89ad47703212ce35f0ae78b9cd7321",
|
||||||
|
"sha256:9ac8a092f47a7517352b2cf83d3004bbe174b1ba8261c5da95495e4857f91b55"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==0.3.0"
|
||||||
|
},
|
||||||
|
"toml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||||
|
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.10.2"
|
||||||
|
},
|
||||||
|
"types-toml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:05a8da4bfde2f1ee60e90c7071c063b461f74c63a9c3c1099470c08d6fa58615",
|
||||||
|
"sha256:a567fe2614b177d537ad99a661adc9bfc8c55a46f95e66370a4ed2dd171335f9"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.10.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"black": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b",
|
||||||
|
"sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176",
|
||||||
|
"sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09",
|
||||||
|
"sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a",
|
||||||
|
"sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015",
|
||||||
|
"sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79",
|
||||||
|
"sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb",
|
||||||
|
"sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20",
|
||||||
|
"sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464",
|
||||||
|
"sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968",
|
||||||
|
"sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82",
|
||||||
|
"sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21",
|
||||||
|
"sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0",
|
||||||
|
"sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265",
|
||||||
|
"sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b",
|
||||||
|
"sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a",
|
||||||
|
"sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72",
|
||||||
|
"sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce",
|
||||||
|
"sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0",
|
||||||
|
"sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a",
|
||||||
|
"sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163",
|
||||||
|
"sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad",
|
||||||
|
"sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==22.3.0"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
|
||||||
|
"sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==8.1.3"
|
||||||
|
},
|
||||||
|
"mypy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0112752a6ff07230f9ec2f71b0d3d4e088a910fdce454fdb6553e83ed0eced7d",
|
||||||
|
"sha256:0384d9f3af49837baa92f559d3fa673e6d2652a16550a9ee07fc08c736f5e6f8",
|
||||||
|
"sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de",
|
||||||
|
"sha256:1fdeb0a0f64f2a874a4c1f5271f06e40e1e9779bf55f9567f149466fc7a55038",
|
||||||
|
"sha256:4c653e4846f287051599ed8f4b3c044b80e540e88feec76b11044ddc5612ffed",
|
||||||
|
"sha256:563514c7dc504698fb66bb1cf897657a173a496406f1866afae73ab5b3cdb334",
|
||||||
|
"sha256:5b231afd6a6e951381b9ef09a1223b1feabe13625388db48a8690f8daa9b71ff",
|
||||||
|
"sha256:5ce6a09042b6da16d773d2110e44f169683d8cc8687e79ec6d1181a72cb028d2",
|
||||||
|
"sha256:5e7647df0f8fc947388e6251d728189cfadb3b1e558407f93254e35abc026e22",
|
||||||
|
"sha256:6003de687c13196e8a1243a5e4bcce617d79b88f83ee6625437e335d89dfebe2",
|
||||||
|
"sha256:61504b9a5ae166ba5ecfed9e93357fd51aa693d3d434b582a925338a2ff57fd2",
|
||||||
|
"sha256:77423570c04aca807508a492037abbd72b12a1fb25a385847d191cd50b2c9605",
|
||||||
|
"sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb",
|
||||||
|
"sha256:a952b8bc0ae278fc6316e6384f67bb9a396eb30aced6ad034d3a76120ebcc519",
|
||||||
|
"sha256:b5b5bd0ffb11b4aba2bb6d31b8643902c48f990cc92fda4e21afac658044f0c0",
|
||||||
|
"sha256:ca75ecf2783395ca3016a5e455cb322ba26b6d33b4b413fcdedfc632e67941dc",
|
||||||
|
"sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b",
|
||||||
|
"sha256:dd4d670eee9610bf61c25c940e9ade2d0ed05eb44227275cce88701fee014b1f",
|
||||||
|
"sha256:e19736af56947addedce4674c0971e5dceef1b5ec7d667fe86bcd2b07f8f9075",
|
||||||
|
"sha256:eaea21d150fb26d7b4856766e7addcf929119dd19fc832b22e71d942835201ef",
|
||||||
|
"sha256:eaff8156016487c1af5ffa5304c3e3fd183edcb412f3e9c72db349faf3f6e0eb",
|
||||||
|
"sha256:ee0a36edd332ed2c5208565ae6e3a7afc0eabb53f5327e281f2ef03a6bc7687a",
|
||||||
|
"sha256:ef7beb2a3582eb7a9f37beaf38a28acfd801988cde688760aea9e6cc4832b10b"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.950"
|
||||||
|
},
|
||||||
|
"mypy-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
|
||||||
|
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
|
||||||
|
],
|
||||||
|
"version": "==0.4.3"
|
||||||
|
},
|
||||||
|
"pathspec": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
|
||||||
|
"sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
|
||||||
|
],
|
||||||
|
"version": "==0.9.0"
|
||||||
|
},
|
||||||
|
"platformdirs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788",
|
||||||
|
"sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2.5.2"
|
||||||
|
},
|
||||||
|
"tomli": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||||
|
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
|
||||||
|
],
|
||||||
|
"markers": "python_version < '3.11'",
|
||||||
|
"version": "==2.0.1"
|
||||||
|
},
|
||||||
|
"typing-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708",
|
||||||
|
"sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==4.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
omnibot/__init__.py
Normal file
1
omnibot/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import bot
|
||||||
24
omnibot/__main__.py
Normal file
24
omnibot/__main__.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .config import ServerConfig
|
||||||
|
from .bot import Bot
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG,
|
||||||
|
format="%(asctime)s - %(name)-12s - %(levelname)-8s - %(message)s",
|
||||||
|
)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
log.debug("Loading config")
|
||||||
|
config = ServerConfig()
|
||||||
|
config.load("config.toml")
|
||||||
|
log.debug("Using configuration: %s", config)
|
||||||
|
|
||||||
|
server = Bot(config)
|
||||||
|
await server.run()
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
BIN
omnibot/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
omnibot/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
omnibot/__pycache__/__main__.cpython-310.pyc
Normal file
BIN
omnibot/__pycache__/__main__.cpython-310.pyc
Normal file
Binary file not shown.
129
omnibot/bot.py
Normal file
129
omnibot/bot.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Sequence, Set
|
||||||
|
|
||||||
|
from asyncirc.protocol import IrcProtocol
|
||||||
|
from asyncirc.server import Server
|
||||||
|
from irclib.parser import Message
|
||||||
|
|
||||||
|
from .config import ServerConfig
|
||||||
|
from . import plugin
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Bot:
|
||||||
|
def __init__(self, server_config: ServerConfig):
|
||||||
|
self.__server_config = server_config
|
||||||
|
self.__plugins = [
|
||||||
|
plugin.load_plugin(server_config, config)
|
||||||
|
for config in server_config.plugins
|
||||||
|
]
|
||||||
|
# TODO - this may not be needed
|
||||||
|
self.__channels: Set[str] = set()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_config(self) -> ServerConfig:
|
||||||
|
return self.__server_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugins(self) -> Sequence[plugin.Plugin]:
|
||||||
|
return self.__plugins
|
||||||
|
|
||||||
|
def channel_plugins(self, channel: str) -> Sequence[plugin.Plugin]:
|
||||||
|
return [plugin for plugin in self.plugins if channel in plugin.channels]
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
server = Server(
|
||||||
|
self.server_config.server,
|
||||||
|
self.server_config.port,
|
||||||
|
self.server_config.use_ssl,
|
||||||
|
)
|
||||||
|
self.connection = IrcProtocol([server], self.server_config.nick, loop=loop)
|
||||||
|
# Register events
|
||||||
|
# self.connection.register("*", self.on_message)
|
||||||
|
self.connection.register("001", self.on_connect)
|
||||||
|
self.connection.register("JOIN", self.on_join)
|
||||||
|
self.connection.register("PART", self.on_part)
|
||||||
|
self.connection.register("KICK", self.on_kick)
|
||||||
|
self.connection.register("PRIVMSG", self.on_message)
|
||||||
|
# Connect
|
||||||
|
log.info("Connecting to %s", self.server_config.server)
|
||||||
|
await self.connection.connect()
|
||||||
|
# Keepalive loop
|
||||||
|
await self.keepalive()
|
||||||
|
|
||||||
|
async def on_connect(self, conn: IrcProtocol, message: Message):
|
||||||
|
# Join rooms
|
||||||
|
for ch in self.server_config.all_channels:
|
||||||
|
msg = Message(None, None, "JOIN", ch)
|
||||||
|
conn.send(str(msg))
|
||||||
|
|
||||||
|
async def on_join(self, conn: IrcProtocol, message: Message):
|
||||||
|
channel = message.parameters[0]
|
||||||
|
who = message.prefix
|
||||||
|
if who.nick == self.server_config.nick:
|
||||||
|
self.__channels |= {channel}
|
||||||
|
if channel not in self.server_config.all_channels:
|
||||||
|
# Try to leave this channel that we were forced to join like some kind of dog
|
||||||
|
msg = Message(None, None, "PART", channel)
|
||||||
|
conn.send(str(msg))
|
||||||
|
|
||||||
|
# Pass the message along to available plugins
|
||||||
|
plugins = self.channel_plugins(channel)
|
||||||
|
await asyncio.gather(
|
||||||
|
*[plugin.on_join(conn, channel, who) for plugin in plugins]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def __on_part(self, conn: IrcProtocol, message: Message):
|
||||||
|
"This is the common logic between on_part and on_kick. Don't call this."
|
||||||
|
channel = message.parameters[0]
|
||||||
|
who = message.prefix
|
||||||
|
if who.nick == self.server_config.nick:
|
||||||
|
self.__channels -= {channel}
|
||||||
|
if channel not in self.server_config.all_channels:
|
||||||
|
# Try to rejoin this channel that we were force-parted from
|
||||||
|
msg = Message(None, None, "JOIN", channel)
|
||||||
|
conn.send(str(msg))
|
||||||
|
|
||||||
|
async def on_part(self, conn: IrcProtocol, message: Message):
|
||||||
|
await self.__on_part(conn, message)
|
||||||
|
# Pass the message along to available plugins
|
||||||
|
channel = message.parameters[0]
|
||||||
|
who = message.prefix
|
||||||
|
plugins = self.channel_plugins(channel)
|
||||||
|
await asyncio.gather(
|
||||||
|
*[plugin.on_part(conn, channel, who) for plugin in plugins]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_kick(self, conn: IrcProtocol, message: Message):
|
||||||
|
await self.__on_part(conn, message)
|
||||||
|
# Pass the message along to available plugins
|
||||||
|
channel = message.parameters[0]
|
||||||
|
who = message.prefix
|
||||||
|
plugins = self.channel_plugins(channel)
|
||||||
|
await asyncio.gather(
|
||||||
|
*[plugin.on_kick(conn, channel, who) for plugin in plugins]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_message(self, conn: IrcProtocol, message: Message):
|
||||||
|
# Pass the message to the plugins
|
||||||
|
log.debug("%s", message)
|
||||||
|
channel = message.parameters[0]
|
||||||
|
who = message.prefix
|
||||||
|
if who.nick == self.server_config.nick:
|
||||||
|
# Don't raise on_message events for ourselves.
|
||||||
|
return
|
||||||
|
line = message.parameters[1]
|
||||||
|
plugins = self.channel_plugins(channel)
|
||||||
|
await asyncio.gather(
|
||||||
|
*[plugin.on_message(conn, channel, who, line) for plugin in plugins]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def keepalive(self):
|
||||||
|
# loop while we're connected, check every second
|
||||||
|
log.info("Starting keepalive loop")
|
||||||
|
while self.connection.connected:
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
85
omnibot/config.py
Normal file
85
omnibot/config.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import dataclasses
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Mapping, Sequence, Set
|
||||||
|
|
||||||
|
import toml
|
||||||
|
|
||||||
|
|
||||||
|
PluginConfig = Mapping[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigError(Exception):
|
||||||
|
def __init__(self, which: str, hint: str | None, plugin: str | None = None):
|
||||||
|
self.which = which
|
||||||
|
self.hint = hint
|
||||||
|
self.plugin = plugin
|
||||||
|
msg = f"{self.which}"
|
||||||
|
if self.hint:
|
||||||
|
msg = f"{msg}: {self.hint}"
|
||||||
|
if self.plugin:
|
||||||
|
msg = f"in config for plugin {plugin}: {msg}"
|
||||||
|
else:
|
||||||
|
msg = f"in server config: {msg}"
|
||||||
|
super(ConfigError, self).__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class ServerConfig:
|
||||||
|
server: str = ""
|
||||||
|
use_ssl: bool = False
|
||||||
|
port: int = 6667
|
||||||
|
plugins: Sequence[PluginConfig] = dataclasses.field(default_factory=list)
|
||||||
|
channels: Sequence[str] = dataclasses.field(default_factory=list)
|
||||||
|
nick: str = "omnibot"
|
||||||
|
|
||||||
|
def load(self, path: Path | str):
|
||||||
|
if isinstance(path, str):
|
||||||
|
path = Path(path)
|
||||||
|
with open(path) as fp:
|
||||||
|
obj = toml.load(fp)
|
||||||
|
|
||||||
|
if "server" not in obj:
|
||||||
|
raise ConfigError("server", "must be present")
|
||||||
|
if not isinstance(obj["server"], str):
|
||||||
|
raise ConfigError("server", "must be a string")
|
||||||
|
self.server = obj["server"]
|
||||||
|
|
||||||
|
if "use_ssl" in obj:
|
||||||
|
if not isinstance(obj["use_ssl"], bool):
|
||||||
|
raise ConfigError("use_ssl", "must be a boolean")
|
||||||
|
self.use_ssl = obj["use_ssl"]
|
||||||
|
else:
|
||||||
|
# Don't use SSL by default
|
||||||
|
self.use_ssl = False
|
||||||
|
|
||||||
|
if "port" in obj:
|
||||||
|
if not isinstance(obj["port"], int):
|
||||||
|
raise ConfigError("port", "must be an integer")
|
||||||
|
if not (0 < obj["port"] <= 65535):
|
||||||
|
raise ConfigError("port", "must be between 0 and 65535")
|
||||||
|
self.port = obj["port"]
|
||||||
|
else:
|
||||||
|
if self.use_ssl:
|
||||||
|
self.port = 6697
|
||||||
|
else:
|
||||||
|
self.port = 6667
|
||||||
|
|
||||||
|
if "plugins" in obj:
|
||||||
|
if not isinstance(obj["plugins"], Sequence):
|
||||||
|
raise ConfigError(
|
||||||
|
"plugins", "must be a mapping of configuration values"
|
||||||
|
)
|
||||||
|
self.plugins = obj["plugins"]
|
||||||
|
|
||||||
|
if "nick" in obj:
|
||||||
|
if not isinstance(obj["nick"], str):
|
||||||
|
raise ConfigError("nick", "must be a string")
|
||||||
|
self.nick = obj["nick"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_channels(self) -> Set[str]:
|
||||||
|
channels = set(self.channels)
|
||||||
|
for plugin in self.plugins:
|
||||||
|
if "channels" in plugin:
|
||||||
|
channels |= set(plugin["channels"])
|
||||||
|
return channels
|
||||||
60
omnibot/plugin.py
Normal file
60
omnibot/plugin.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from asyncirc.protocol import IrcProtocol
|
||||||
|
from irclib.parser import Message, Prefix
|
||||||
|
|
||||||
|
from .config import PluginConfig, ServerConfig
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin:
|
||||||
|
def __init__(self, server_config: ServerConfig, plugin_config: PluginConfig):
|
||||||
|
self.__server_config = server_config
|
||||||
|
self.__plugin_config = plugin_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channels(self) -> Sequence[str]:
|
||||||
|
if "channels" in self.plugin_config:
|
||||||
|
return self.plugin_config["channels"]
|
||||||
|
else:
|
||||||
|
return self.server_config.channels
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin_config(self) -> PluginConfig:
|
||||||
|
return self.__plugin_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_config(self) -> ServerConfig:
|
||||||
|
return self.__server_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nick(self) -> str:
|
||||||
|
return self.server_config.nick
|
||||||
|
|
||||||
|
def send_to(self, conn: IrcProtocol, who: str, message: str):
|
||||||
|
message = Message(None, None, "PRIVMSG", who, message)
|
||||||
|
conn.send(str(message))
|
||||||
|
|
||||||
|
async def on_join(self, conn: IrcProtocol, channel: str, who: Prefix):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_part(self, conn: IrcProtocol, channel: str, who: Prefix):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_kick(self, conn: IrcProtocol, channel: str, who: Prefix):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_message(self, conn: IrcProtocol, channel: str, who: Prefix, line: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def load_plugin(server_config: ServerConfig, plugin_config: PluginConfig) -> Plugin:
|
||||||
|
name = plugin_config["module"]
|
||||||
|
log.info("Loading plugin %s", name)
|
||||||
|
plugin_module = importlib.import_module(name)
|
||||||
|
PluginType = plugin_module.PLUGIN_TYPE
|
||||||
|
return PluginType(server_config, plugin_config)
|
||||||
0
omnibot/server.py
Normal file
0
omnibot/server.py
Normal file
41
plugins/fortune.py
Normal file
41
plugins/fortune.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import logging
|
||||||
|
import random
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from asyncirc.protocol import IrcProtocol
|
||||||
|
from irclib.parser import Message, Prefix
|
||||||
|
from omnibot.plugin import Plugin
|
||||||
|
from omnibot.config import ConfigError
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Fortune(Plugin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if "fortunes" not in self.plugin_config:
|
||||||
|
raise ConfigError("fortunes", "must be supplied to Fortune plugin")
|
||||||
|
fortunes = self.plugin_config["fortunes"]
|
||||||
|
if isinstance(fortunes, str):
|
||||||
|
with open(fortunes) as fp:
|
||||||
|
self.fortunes = [line.strip() for line in fp if line]
|
||||||
|
elif isinstance(fortunes, Sequence):
|
||||||
|
self.fortunes = fortunes
|
||||||
|
else:
|
||||||
|
raise ConfigError(
|
||||||
|
"fortunes",
|
||||||
|
"must be either a list or a path to a file containing the fortunes",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_message(self, conn: IrcProtocol, channel: str, who: Prefix, line: str):
|
||||||
|
parts = line.split()
|
||||||
|
if not parts:
|
||||||
|
return
|
||||||
|
word = parts[0]
|
||||||
|
if word == "!fortune":
|
||||||
|
fortune = random.choice(self.fortunes)
|
||||||
|
self.send_to(conn, channel, fortune)
|
||||||
|
|
||||||
|
|
||||||
|
PLUGIN_TYPE = Fortune
|
||||||
Reference in New Issue
Block a user