Move to pipenv, update dependencies + more
Replaces occurrences of channel with room (deprecates channel arguments). Use python native logging module instead of prints. Remove .idea from git Simplify .gitignore
This commit is contained in:
parent
425c39a733
commit
a0279c12e6
224
.gitignore
vendored
224
.gitignore
vendored
|
@ -1,225 +1,3 @@
|
||||||
config.yml
|
config.yml
|
||||||
|
.idea/
|
||||||
# Created by https://www.gitignore.io/api/python,pycharm,virtualenv
|
|
||||||
# Edit at https://www.gitignore.io/?templates=python,pycharm,virtualenv
|
|
||||||
|
|
||||||
### PyCharm ###
|
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
|
||||||
|
|
||||||
# User-specific stuff
|
|
||||||
.idea/**/workspace.xml
|
|
||||||
.idea/**/tasks.xml
|
|
||||||
.idea/**/usage.statistics.xml
|
|
||||||
.idea/**/dictionaries
|
|
||||||
.idea/**/shelf
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
.idea/**/contentModel.xml
|
|
||||||
|
|
||||||
# Sensitive or high-churn files
|
|
||||||
.idea/**/dataSources/
|
|
||||||
.idea/**/dataSources.ids
|
|
||||||
.idea/**/dataSources.local.xml
|
|
||||||
.idea/**/sqlDataSources.xml
|
|
||||||
.idea/**/dynamic.xml
|
|
||||||
.idea/**/uiDesigner.xml
|
|
||||||
.idea/**/dbnavigator.xml
|
|
||||||
|
|
||||||
# Gradle
|
|
||||||
.idea/**/gradle.xml
|
|
||||||
.idea/**/libraries
|
|
||||||
|
|
||||||
# Gradle and Maven with auto-import
|
|
||||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
|
||||||
# since they will be recreated, and may cause churn. Uncomment if using
|
|
||||||
# auto-import.
|
|
||||||
# .idea/modules.xml
|
|
||||||
# .idea/*.iml
|
|
||||||
# .idea/modules
|
|
||||||
# *.iml
|
|
||||||
# *.ipr
|
|
||||||
|
|
||||||
# CMake
|
|
||||||
cmake-build-*/
|
|
||||||
|
|
||||||
# Mongo Explorer plugin
|
|
||||||
.idea/**/mongoSettings.xml
|
|
||||||
|
|
||||||
# File-based project format
|
|
||||||
*.iws
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
out/
|
|
||||||
|
|
||||||
# mpeltonen/sbt-idea plugin
|
|
||||||
.idea_modules/
|
|
||||||
|
|
||||||
# JIRA plugin
|
|
||||||
atlassian-ide-plugin.xml
|
|
||||||
|
|
||||||
# Cursive Clojure plugin
|
|
||||||
.idea/replstate.xml
|
|
||||||
|
|
||||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
||||||
com_crashlytics_export_strings.xml
|
|
||||||
crashlytics.properties
|
|
||||||
crashlytics-build.properties
|
|
||||||
fabric.properties
|
|
||||||
|
|
||||||
# Editor-based Rest Client
|
|
||||||
.idea/httpRequests
|
|
||||||
|
|
||||||
# Android studio 3.1+ serialized cache file
|
|
||||||
.idea/caches/build_file_checksums.ser
|
|
||||||
|
|
||||||
### PyCharm Patch ###
|
|
||||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
|
||||||
|
|
||||||
# *.iml
|
|
||||||
# modules.xml
|
|
||||||
# .idea/misc.xml
|
|
||||||
# *.ipr
|
|
||||||
|
|
||||||
# Sonarlint plugin
|
|
||||||
.idea/sonarlint
|
|
||||||
|
|
||||||
### Python ###
|
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
pip-wheel-metadata/
|
|
||||||
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
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
.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
|
|
||||||
|
|
||||||
# celery beat schedule file
|
|
||||||
celerybeat-schedule
|
|
||||||
|
|
||||||
# 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/
|
|
||||||
|
|
||||||
### VirtualEnv ###
|
|
||||||
# Virtualenv
|
|
||||||
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
|
|
||||||
[Bb]in
|
|
||||||
[Ii]nclude
|
|
||||||
[Ll]ib
|
|
||||||
[Ll]ib64
|
|
||||||
[Ll]ocal
|
|
||||||
[Ss]cripts
|
|
||||||
pyvenv.cfg
|
|
||||||
pip-selfcheck.json
|
|
||||||
|
|
||||||
# End of https://www.gitignore.io/api/python,pycharm,virtualenv
|
|
|
@ -1,32 +0,0 @@
|
||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="ourVersions">
|
|
||||||
<value>
|
|
||||||
<list size="4">
|
|
||||||
<item index="0" class="java.lang.String" itemvalue="3.6" />
|
|
||||||
<item index="1" class="java.lang.String" itemvalue="3.7" />
|
|
||||||
<item index="2" class="java.lang.String" itemvalue="3.8" />
|
|
||||||
<item index="3" class="java.lang.String" itemvalue="3.9" />
|
|
||||||
</list>
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
|
||||||
<option name="ignoredErrors">
|
|
||||||
<list>
|
|
||||||
<option value="E501" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
|
||||||
<option name="ignoredErrors">
|
|
||||||
<list>
|
|
||||||
<option value="N812" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PyRedundantParenthesesInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="JavaScriptSettings">
|
|
||||||
<option name="languageLevel" value="ES6" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (webhook-matrix-notifier)" project-jdk-type="Python SDK" />
|
|
||||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
|
||||||
<option name="version" value="3" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/webhook-matrix-notifier.iml" filepath="$PROJECT_DIR$/.idea/webhook-matrix-notifier.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="PYTHON_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="jdk" jdkName="Python 3.9 (webhook-matrix-notifier)" jdkType="Python SDK" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
15
Pipfile
Normal file
15
Pipfile
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
pyyaml = "*"
|
||||||
|
flask = {version = "*", extras = ["async"]}
|
||||||
|
matrix-nio = {version = "*", extras = ["e2e"]}
|
||||||
|
python-dateutil = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = ">=3.9"
|
1012
Pipfile.lock
generated
Normal file
1012
Pipfile.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
12
README.md
12
README.md
|
@ -1,6 +1,6 @@
|
||||||
# Webhook Matrix Notifier
|
# Webhook Matrix Notifier
|
||||||
|
|
||||||
Takes notifications via webhook, checks a secret and notifies a [Matrix](https://matrix.org) channel.
|
Takes notifications via webhook, checks a secret and notifies a [Matrix](https://matrix.org) room.
|
||||||
Listens to HTTP only. Should be used behind a reverse-proxy with HTTPS.
|
Listens to HTTP only. Should be used behind a reverse-proxy with HTTPS.
|
||||||
|
|
||||||
An example configuration is at `config.yml.example` and the program always reads the configuration file `config.yml`.
|
An example configuration is at `config.yml.example` and the program always reads the configuration file `config.yml`.
|
||||||
|
@ -14,8 +14,8 @@ Then, send a POST request using curl.
|
||||||
### GitLab
|
### GitLab
|
||||||
|
|
||||||
```
|
```
|
||||||
export CHANNEL_ENC=`python3 -c 'from urllib.parse import quote_plus; print(quote_plus("#channel:matrix.org"))'`
|
export URLQUOTED_ROOM=`python3 -c 'from urllib.parse import quote_plus; print(quote_plus("#room:matrix.org"))'`
|
||||||
curl -i -X POST "http://localhost:5000/matrix?channel=${CHANNEL_ENC}" -H "X-Gitlab-Event: Push Hook" -H "X-Gitlab-Token: 123" -H "Content-Type: application/json" --data-binary @./testrequest_gitlab.json
|
curl -i -X POST "http://localhost:5000/matrix?room=${URLQUOTED_ROOM}" -H "X-Gitlab-Event: Push Hook" -H "X-Gitlab-Token: 123" -H "Content-Type: application/json" --data-binary @./testrequest_gitlab.json
|
||||||
```
|
```
|
||||||
|
|
||||||
The `X-Gitlab-Token` must correspond to the secret provided in `config.yml`
|
The `X-Gitlab-Token` must correspond to the secret provided in `config.yml`
|
||||||
|
@ -23,9 +23,9 @@ The `X-Gitlab-Token` must correspond to the secret provided in `config.yml`
|
||||||
### Prometheus
|
### Prometheus
|
||||||
|
|
||||||
```
|
```
|
||||||
export CHANNEL_ENC=`python3 -c 'from urllib.parse import quote_plus; print(quote_plus("#channel:matrix.org"))'`
|
export URLQUOTED_ROOM=`python3 -c 'from urllib.parse import quote_plus; print(quote_plus("#room:matrix.org"))'`
|
||||||
export WMN_SECRET=`python3 -c 'from urllib.parse import quote_plus; print(quote_plus("123"))'`
|
export URLQUOTED_SECRET=`python3 -c 'from urllib.parse import quote_plus; print(quote_plus("123"))'`
|
||||||
curl -i -X POST "http://localhost:5000/matrix?type=prometheus&secret=${WMN_SECRET}&channel=${CHANNEL_ENC}" -H "Content-Type: application/json" --data-binary @./testrequest_prometheus.json
|
curl -i -X POST "http://localhost:5000/matrix?type=prometheus&secret=${URLQUOTED_SECRET}&room=${URLQUOTED_ROOM}" -H "Content-Type: application/json" --data-binary @./testrequest_prometheus.json
|
||||||
```
|
```
|
||||||
|
|
||||||
The secret must be passed as a URI parameter here.
|
The secret must be passed as a URI parameter here.
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
PyYAML>=5.1,<6
|
|
||||||
flask[async]>=2.0.0,<3
|
|
||||||
matrix-nio[e2e]>=0.18.0,<1
|
|
||||||
python-dateutil~=2.8.2
|
|
|
@ -16,9 +16,10 @@
|
||||||
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
|
||||||
from typing import Optional, Dict, Any, Tuple
|
from typing import Optional, Dict, Any, Tuple
|
||||||
|
|
||||||
import nio
|
import nio
|
||||||
|
@ -28,17 +29,17 @@ Cfg = Dict[str, Any]
|
||||||
ErrorResponseTuple = Tuple[str, int]
|
ErrorResponseTuple = Tuple[str, int]
|
||||||
|
|
||||||
|
|
||||||
def format_response(error: "nio.ErrorResponse") -> ErrorResponseTuple:
|
def format_response(error_response: "nio.ErrorResponse") -> ErrorResponseTuple:
|
||||||
"""
|
"""
|
||||||
:returns: tuple to be interpreted as (body, status), see Flask.make_response
|
:returns: tuple to be interpreted as (body, status), see Flask.make_response
|
||||||
:rtype: ErrorResponseTuple
|
:rtype: ErrorResponseTuple
|
||||||
"""
|
"""
|
||||||
print("matrix_error was called with", error, file=sys.stderr, flush=True)
|
logging.warning("format_response was called with error %s", error_response)
|
||||||
if error.status_code:
|
if error_response.status_code:
|
||||||
status = int(error.status_code)
|
status = int(error_response.status_code)
|
||||||
else:
|
else:
|
||||||
status = 500
|
status = 500
|
||||||
return f"Error from Matrix: {error.message}", status
|
return f"Error from Matrix: {error_response.message}", status
|
||||||
|
|
||||||
|
|
||||||
class MatrixException(Exception):
|
class MatrixException(Exception):
|
||||||
|
@ -71,10 +72,10 @@ def save_configuration(configuration: Cfg):
|
||||||
|
|
||||||
async def client_login(configuration: Cfg) -> nio.AsyncClient:
|
async def client_login(configuration: Cfg) -> nio.AsyncClient:
|
||||||
"""
|
"""
|
||||||
:exception MatrixException: if the matrix server returns an error.
|
:exception MatrixException: if the Matrix server returns an error.
|
||||||
:param configuration: the configuration object to load login data from.
|
:param configuration: the configuration object to load login data from.
|
||||||
:type configuration: Cfg
|
:type configuration: Cfg
|
||||||
:return: the matrix client.
|
:return: the Matrix client.
|
||||||
:rtype: nio.AsyncClient
|
:rtype: nio.AsyncClient
|
||||||
"""
|
"""
|
||||||
client = nio.AsyncClient(
|
client = nio.AsyncClient(
|
||||||
|
@ -105,7 +106,7 @@ async def send_message(
|
||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
) -> nio.RoomSendResponse:
|
) -> nio.RoomSendResponse:
|
||||||
"""
|
"""
|
||||||
:exception MatrixException: if the matrix server returns an error.
|
:exception MatrixException: if the Matrix server returns an error.
|
||||||
:param client: the client to operate on.
|
:param client: the client to operate on.
|
||||||
:param room_id: the room to send the message to.
|
:param room_id: the room to send the message to.
|
||||||
:param text: the text to send.
|
:param text: the text to send.
|
||||||
|
@ -135,11 +136,11 @@ async def send_message(
|
||||||
async def resolve_room(client: nio.AsyncClient, room: str) -> str:
|
async def resolve_room(client: nio.AsyncClient, room: str) -> str:
|
||||||
"""
|
"""
|
||||||
Takes a room alias or room id and always returns a resolved room id.
|
Takes a room alias or room id and always returns a resolved room id.
|
||||||
:exception MatrixException: if the matrix server returns an error.
|
:exception MatrixException: if the Matrix server returns an error.
|
||||||
:exception RuntimeError: if the passed room string cannot be handled.
|
:exception RuntimeError: if the passed room string cannot be handled.
|
||||||
:param client: the client to operate on.
|
:param client: the client to operate on.
|
||||||
:param room: the room to resolve.
|
:param room: the room to resolve.
|
||||||
:returns: the room's matrix id, starting with a "!".
|
:returns: the room's Matrix id, starting with a "!".
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -47,11 +48,15 @@ async def main():
|
||||||
username: ...
|
username: ...
|
||||||
password: "..."
|
password: "..."
|
||||||
"""
|
"""
|
||||||
|
logging.basicConfig()
|
||||||
cfg = load_configuration()
|
cfg = load_configuration()
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Notify a matrix channel.")
|
parser = argparse.ArgumentParser(description="Notify a Matrix room.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-c", "--channel", required=True, help="the channel to send the message to"
|
"-c", "--channel", required=False, help="the channel to send the message to (deprecated, use --room)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-r", "--room", required=False, help="the Matrix room to send the message to"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-t",
|
"-t",
|
||||||
|
@ -61,19 +66,27 @@ async def main():
|
||||||
choices=("m.text", "m.notice"),
|
choices=("m.text", "m.notice"),
|
||||||
default="m.text",
|
default="m.text",
|
||||||
)
|
)
|
||||||
parser.add_argument("text", help="the text message to send to the channel")
|
parser.add_argument("text", help="the text message to send to the room")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"html", nargs="?", help="the html message to send to the channel"
|
"html", nargs="?", help="the html message to send to the room"
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if room_pattern.fullmatch(args.channel) is None:
|
if not args.channel and not args.room:
|
||||||
print("ERROR: Couldn't parse channel as a matrix channel", file=sys.stderr)
|
logging.error("Specify the Matrix room to send the message to")
|
||||||
|
sys.exit(1)
|
||||||
|
if args.room:
|
||||||
|
room = args.room
|
||||||
|
else:
|
||||||
|
room = args.channel
|
||||||
|
|
||||||
|
if room_pattern.fullmatch(room) is None:
|
||||||
|
logging.error("Could not parse Matrix room '%s'", room)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
client = await client_login(cfg)
|
client = await client_login(cfg)
|
||||||
try:
|
try:
|
||||||
room_id = await resolve_room(client=client, room=args.channel)
|
room_id = await resolve_room(client=client, room=room)
|
||||||
response = await client.join(room_id=room_id)
|
response = await client.join(room_id=room_id)
|
||||||
if isinstance(response, nio.ErrorResponse):
|
if isinstance(response, nio.ErrorResponse):
|
||||||
raise MatrixException(response)
|
raise MatrixException(response)
|
||||||
|
@ -90,10 +103,9 @@ async def main():
|
||||||
response = await send_message(
|
response = await send_message(
|
||||||
client=client, room_id=room_id, text=args.text, msgtype=args.type
|
client=client, room_id=room_id, text=args.text, msgtype=args.type
|
||||||
)
|
)
|
||||||
print("Message sent.", file=sys.stderr, flush=True)
|
|
||||||
finally:
|
finally:
|
||||||
await client.close()
|
await client.close()
|
||||||
print(response.event_id)
|
logging.info("Message sent. %s", response.event_id)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
69
wmn/wmn.py
69
wmn/wmn.py
|
@ -18,9 +18,8 @@
|
||||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Tuple, Optional, Dict, Any
|
from typing import Tuple, Optional, Dict, Any
|
||||||
|
|
||||||
|
@ -41,8 +40,10 @@ from .common import (
|
||||||
|
|
||||||
RequestArgs = MultiDict[str, str]
|
RequestArgs = MultiDict[str, str]
|
||||||
|
|
||||||
app = Flask(__name__)
|
logging.basicConfig()
|
||||||
application = app
|
|
||||||
|
# application is the wsgi variable name
|
||||||
|
application = Flask(__name__)
|
||||||
|
|
||||||
# Not going to care for specifics like the underscore.
|
# Not going to care for specifics like the underscore.
|
||||||
# Generally match room alias or id [!#]anything:example.com with unicode support.
|
# Generally match room alias or id [!#]anything:example.com with unicode support.
|
||||||
|
@ -51,49 +52,31 @@ room_pattern = re.compile(r"^[!#]\w+:[\w\-.]+$")
|
||||||
|
|
||||||
def check_token(configuration: Cfg, token: str):
|
def check_token(configuration: Cfg, token: str):
|
||||||
if token != configuration["secret"]:
|
if token != configuration["secret"]:
|
||||||
print(
|
logging.warning("request denied (401): check_token failed, because token did not match")
|
||||||
"check_token failed, because token did not match",
|
|
||||||
file=sys.stderr,
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
abort(401)
|
abort(401)
|
||||||
|
|
||||||
|
|
||||||
async def get_a_room(client: nio.AsyncClient, request_args: RequestArgs) -> str:
|
async def get_a_room(client: nio.AsyncClient, request_args: RequestArgs) -> str:
|
||||||
"""Takes a nio.AsyncClient and the request args to return a room id."""
|
"""Takes a nio.AsyncClient and the request args to return a room id."""
|
||||||
|
|
||||||
if "channel" not in request_args:
|
if "channel" not in request_args and "room" not in request_args:
|
||||||
print(
|
logging.warning("request denied (400): get_a_room failed, because room was not in request args")
|
||||||
"get_a_room failed, because channel was not in request args",
|
|
||||||
file=sys.stderr,
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
abort(400)
|
abort(400)
|
||||||
room = request_args.get("channel")
|
room = request_args.get("channel", "")
|
||||||
|
room = request_args.get("room", room)
|
||||||
if not room:
|
if not room:
|
||||||
print(
|
logging.warning("request denied (400): get_a_room failed, because room was empty")
|
||||||
"get_a_room failed, because channel was empty",
|
|
||||||
file=sys.stderr,
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
# sanitize input
|
# sanitize input
|
||||||
if room_pattern.fullmatch(room) is None:
|
if room_pattern.fullmatch(room) is None:
|
||||||
print(
|
logging.warning("request denied (400): get_a_room failed, because room '%s' did not match room pattern '%s'", room, room_pattern)
|
||||||
"get_a_room failed, because channel",
|
|
||||||
room,
|
|
||||||
"did not match room pattern",
|
|
||||||
room_pattern,
|
|
||||||
file=sys.stderr,
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await resolve_room(client=client, room=room)
|
return await resolve_room(client=client, room=room)
|
||||||
except MatrixException as error:
|
except MatrixException as error:
|
||||||
abort(app.make_response(error.format_response()))
|
abort(application.make_response(error.format_response()))
|
||||||
|
|
||||||
|
|
||||||
def get_msg_type(request_args: RequestArgs):
|
def get_msg_type(request_args: RequestArgs):
|
||||||
|
@ -103,13 +86,7 @@ def get_msg_type(request_args: RequestArgs):
|
||||||
if msgtype in ["m.text", "m.notice"]:
|
if msgtype in ["m.text", "m.notice"]:
|
||||||
return msgtype
|
return msgtype
|
||||||
else:
|
else:
|
||||||
print(
|
logging.warning("request denied (400): get_msg_type failed, because msgtype '%s' is not known", msgtype)
|
||||||
"get_msg_type failed, because msgtype",
|
|
||||||
msgtype,
|
|
||||||
"is not known",
|
|
||||||
file=sys.stderr,
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
|
|
||||||
|
@ -199,7 +176,7 @@ async def process_gitlab_request():
|
||||||
return format_response(response)
|
return format_response(response)
|
||||||
|
|
||||||
except MatrixException as error:
|
except MatrixException as error:
|
||||||
abort(app.make_response(error.format_response()))
|
abort(application.make_response(error.format_response()))
|
||||||
finally:
|
finally:
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
|
@ -286,7 +263,7 @@ async def process_jenkins_request():
|
||||||
)
|
)
|
||||||
|
|
||||||
except MatrixException as error:
|
except MatrixException as error:
|
||||||
abort(app.make_response(error.format_response()))
|
abort(application.make_response(error.format_response()))
|
||||||
finally:
|
finally:
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
|
@ -387,11 +364,7 @@ async def process_prometheus_request():
|
||||||
cfg = load_configuration()
|
cfg = load_configuration()
|
||||||
secret = request.args.get("secret")
|
secret = request.args.get("secret")
|
||||||
if secret != cfg["secret"]:
|
if secret != cfg["secret"]:
|
||||||
print(
|
logging.warning("check_token failed, because token did not match")
|
||||||
"check_token failed, because token did not match",
|
|
||||||
file=sys.stderr,
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
abort(401)
|
abort(401)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -428,12 +401,10 @@ async def process_prometheus_request():
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
text="Error parsing data in prometheus request",
|
text="Error parsing data in prometheus request",
|
||||||
)
|
)
|
||||||
print("Error parsing JSON and forming message:", file=sys.stderr)
|
logging.exception("Error parsing JSON and forming message")
|
||||||
traceback.print_exc()
|
|
||||||
print(file=sys.stderr, flush=True)
|
|
||||||
return "Error parsing JSON and forming message", 500
|
return "Error parsing JSON and forming message", 500
|
||||||
except MatrixException as error:
|
except MatrixException as error:
|
||||||
abort(app.make_response(error.format_response()))
|
abort(application.make_response(error.format_response()))
|
||||||
finally:
|
finally:
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
|
@ -441,7 +412,7 @@ async def process_prometheus_request():
|
||||||
return "", 204
|
return "", 204
|
||||||
|
|
||||||
|
|
||||||
@app.post("/matrix")
|
@application.post("/matrix")
|
||||||
async def notify():
|
async def notify():
|
||||||
if "X-Gitlab-Token" in request.headers:
|
if "X-Gitlab-Token" in request.headers:
|
||||||
return await process_gitlab_request()
|
return await process_gitlab_request()
|
||||||
|
|
Loading…
Reference in a new issue