Rewrite bot
This commit is contained in:
parent
c60809b94a
commit
cfe2cf16db
|
@ -6,3 +6,6 @@ indent_size = 4
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
insert_final_newline = false
|
insert_final_newline = false
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
insert_final_newline = true
|
|
@ -154,3 +154,5 @@ bot
|
||||||
|
|
||||||
# Local History for Visual Studio Code
|
# Local History for Visual Studio Code
|
||||||
.history/
|
.history/
|
||||||
|
|
||||||
|
bot_old.py
|
||||||
|
|
2
Pipfile
2
Pipfile
|
@ -8,9 +8,11 @@ flake8 = "*"
|
||||||
flake8-quotes = "*"
|
flake8-quotes = "*"
|
||||||
flake8-annotations = "*"
|
flake8-annotations = "*"
|
||||||
yapf = "*"
|
yapf = "*"
|
||||||
|
isort = "*"
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
matrix-bot = {editable = true, path = "."}
|
matrix-bot = {editable = true, path = "."}
|
||||||
|
appdirs = "*"
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.8"
|
python_version = "3.8"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "a17f7aa5b096658663f9e578d693b9f69f01711f3776f8ede29ac1ff34175380"
|
"sha256": "4e2921a6197811de728000532c94147d2fdcf4a20d76d1d7e6da6d39eebd4d91"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -41,6 +41,14 @@
|
||||||
"markers": "python_full_version >= '3.5.3'",
|
"markers": "python_full_version >= '3.5.3'",
|
||||||
"version": "==3.6.2"
|
"version": "==3.6.2"
|
||||||
},
|
},
|
||||||
|
"appdirs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
|
||||||
|
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.4.4"
|
||||||
|
},
|
||||||
"async-timeout": {
|
"async-timeout": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||||
|
@ -354,6 +362,14 @@
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.2.0"
|
"version": "==3.2.0"
|
||||||
},
|
},
|
||||||
|
"isort": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:723de131d9ae9d2561844f0ee525ce33a396a11bcda57174f036ed5ab3d6a122",
|
||||||
|
"sha256:cdca22530d093ed16983ba52c41560fa0219d1b958e44fd2ae2995dcc7b785be"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==5.3.0"
|
||||||
|
},
|
||||||
"mccabe": {
|
"mccabe": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||||
|
|
11
README.md
11
README.md
|
@ -30,15 +30,10 @@ After that you can begin sending messages
|
||||||
|
|
||||||
cd MATRIX_BOT_DIR
|
cd MATRIX_BOT_DIR
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
matrix-bot message "Message Content" # To send to the default room
|
matrix-bot send '!yourRoomId' "Message Content"
|
||||||
matrix-bot message -r '!yourRoomId' "Message Content" # To send to a specific room
|
|
||||||
|
|
||||||
Or start the bot in daemon mode. In this mode the configured room will be monitored for incoming messages and you can send messages from external scripts with `matrix-botctl`
|
Or start the bot in daemon mode. In this mode the bot will listen for defined events (not implemented yet)
|
||||||
|
|
||||||
cd MATRIX_BOT_DIR
|
cd MATRIX_BOT_DIR
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
matrix-bot
|
matrix-bot run
|
||||||
|
|
||||||
# Then you can send messages with
|
|
||||||
|
|
||||||
matrix-botctl message "Message Content"
|
|
||||||
|
|
|
@ -1,124 +1,46 @@
|
||||||
from nio import (AsyncClient, AsyncClientConfig, LoginResponse,
|
|
||||||
KeyVerificationEvent, KeyVerificationStart,
|
|
||||||
KeyVerificationCancel, KeyVerificationKey, KeyVerificationMac,
|
|
||||||
ToDeviceError, LocalProtocolError, SyncResponse,
|
|
||||||
RoomMessageText)
|
|
||||||
import click
|
|
||||||
import traceback
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import markdown
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
from .config import (CONFIG_FILE, STORE_PATH, NEXT_BATCH_PATH, HOMESERVER_URL,
|
import click
|
||||||
FIFO_PATH)
|
from nio import (AsyncClient, AsyncClientConfig, InviteEvent,
|
||||||
|
KeyVerificationCancel, KeyVerificationEvent,
|
||||||
|
KeyVerificationKey, KeyVerificationMac, KeyVerificationStart,
|
||||||
|
LocalProtocolError, LoginResponse, MatrixRoom, RoomMessage,
|
||||||
|
RoomMessageText, SyncResponse, ToDeviceError)
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .utils import setup_logger
|
||||||
|
|
||||||
|
# from .message import TextMessage
|
||||||
|
|
||||||
|
|
||||||
def write_details_to_disk(resp: LoginResponse, credentials: dict) -> None:
|
class Bot(object):
|
||||||
"""Write the required login details to disk.
|
|
||||||
|
|
||||||
It will allow following logins to be made without password.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
---------
|
|
||||||
resp : LoginResponse - successful client login response
|
|
||||||
credentials : dict - The credentials used to sign in
|
|
||||||
|
|
||||||
"""
|
|
||||||
# open the config file in write-mode
|
|
||||||
with open(CONFIG_FILE, 'w') as f:
|
|
||||||
# write the login details to disk
|
|
||||||
json.dump(
|
|
||||||
{
|
|
||||||
'homeserver': credentials[
|
|
||||||
'homeserver'], # e.g. "https://matrix.example.org"
|
|
||||||
'device_name':
|
|
||||||
credentials['device_name'], # e.g. 'matrix-bot'
|
|
||||||
'room_id':
|
|
||||||
credentials['room_id'], # e.g. '!yourRoomId:example.org'
|
|
||||||
'user_id': resp.user_id, # e.g. '@user:example.org'
|
|
||||||
'device_id':
|
|
||||||
resp.device_id, # device ID, 10 uppercase letters
|
|
||||||
'access_token': resp.access_token, # cryptogr. access token
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Bot():
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._loop = asyncio.get_event_loop()
|
self.__logger = setup_logger(__name__)
|
||||||
|
self.logger.debug('Initializing Bot')
|
||||||
|
self.__loop = asyncio.get_running_loop()
|
||||||
|
self.__first_sync = True
|
||||||
|
for s in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
self.__loop.add_signal_handler(s, self.__signal_handler)
|
||||||
|
|
||||||
@classmethod
|
async def login(self) -> AsyncClient:
|
||||||
async def login(cls: any) -> any:
|
|
||||||
bot = cls()
|
|
||||||
await bot._login()
|
|
||||||
return bot
|
|
||||||
|
|
||||||
@property
|
|
||||||
def client(self) -> AsyncClient:
|
|
||||||
return self._client
|
|
||||||
|
|
||||||
@property
|
|
||||||
def loop(self) -> asyncio.BaseEventLoop:
|
|
||||||
return self._loop
|
|
||||||
|
|
||||||
@property
|
|
||||||
def client_config(self) -> AsyncClientConfig:
|
|
||||||
return self._client_config
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config(self) -> dict:
|
|
||||||
return self._config
|
|
||||||
|
|
||||||
async def send_message_to_room(
|
|
||||||
self,
|
|
||||||
room_id: str,
|
|
||||||
content: dict,
|
|
||||||
message_type: str = 'm.room.message') -> None:
|
|
||||||
"""Login and wait for and perform emoji verify."""
|
|
||||||
# Set up event callbacks
|
|
||||||
await self._login()
|
|
||||||
if room_id is None:
|
|
||||||
room_id = self.config['room_id']
|
|
||||||
await self.client.sync(timeout=30000, full_state=True)
|
|
||||||
await self.client.room_send(
|
|
||||||
room_id=room_id,
|
|
||||||
message_type=message_type,
|
|
||||||
content=content,
|
|
||||||
ignore_unverified_devices=True,
|
|
||||||
)
|
|
||||||
await self.client.set_presence('offline')
|
|
||||||
await self.client.close()
|
|
||||||
|
|
||||||
async def send_text_to_room(self, room_id: str, message: str) -> None:
|
|
||||||
content = {
|
|
||||||
'msgtype': 'm.text',
|
|
||||||
'format': 'org.matrix.custom.html',
|
|
||||||
'formatted_body': markdown.markdown(message),
|
|
||||||
}
|
|
||||||
await self.send_message_to_room(room_id, content)
|
|
||||||
|
|
||||||
async def send_notice_to_room(self, room_id: str, message: str) -> None:
|
|
||||||
content = {
|
|
||||||
'msgtype': 'm.notice',
|
|
||||||
'format': 'org.matrix.custom.html',
|
|
||||||
'formatted_body': markdown.markdown(message),
|
|
||||||
}
|
|
||||||
await self.send_message_to_room(room_id, content)
|
|
||||||
|
|
||||||
async def _login(self) -> AsyncClient:
|
|
||||||
"""Login to the matrix homeserver defined in the config file.
|
"""Login to the matrix homeserver defined in the config file.
|
||||||
"""
|
"""
|
||||||
self._client_config = AsyncClientConfig(
|
self.logger.debug('Starting login process')
|
||||||
|
self.__client_config = AsyncClientConfig(
|
||||||
max_limit_exceeded=0,
|
max_limit_exceeded=0,
|
||||||
max_timeouts=0,
|
max_timeouts=0,
|
||||||
store_sync_tokens=True,
|
store_sync_tokens=True,
|
||||||
encryption_enabled=True,
|
encryption_enabled=True,
|
||||||
)
|
)
|
||||||
# If there are no previously-saved credentials, we'll use the password
|
# If there are no previously-saved credentials, we'll use the password
|
||||||
if not os.path.exists(CONFIG_FILE):
|
if not os.path.exists(Config.CONFIG_FILE):
|
||||||
|
self.logger.debug('Starting password verification process')
|
||||||
click.secho(
|
click.secho(
|
||||||
'First time use. Did not find credential file. Asking '
|
'First time use. Did not find credential file. Asking '
|
||||||
'for homeserver, user, and password to create '
|
'for homeserver, user, and password to create '
|
||||||
|
@ -126,8 +48,8 @@ class Bot():
|
||||||
bold=True,
|
bold=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not os.path.exists(STORE_PATH):
|
if not os.path.exists(Config.STORE_PATH):
|
||||||
os.makedirs(STORE_PATH)
|
os.makedirs(Config.STORE_PATH)
|
||||||
|
|
||||||
credentials = self._ask_credentials()
|
credentials = self._ask_credentials()
|
||||||
|
|
||||||
|
@ -135,7 +57,7 @@ class Bot():
|
||||||
client = AsyncClient(
|
client = AsyncClient(
|
||||||
credentials['homeserver'],
|
credentials['homeserver'],
|
||||||
credentials['user_id'],
|
credentials['user_id'],
|
||||||
store_path=STORE_PATH,
|
store_path=Config.STORE_PATH,
|
||||||
config=self.client_config,
|
config=self.client_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -149,20 +71,16 @@ class Bot():
|
||||||
|
|
||||||
# check that we logged in succesfully
|
# check that we logged in succesfully
|
||||||
if isinstance(resp, LoginResponse):
|
if isinstance(resp, LoginResponse):
|
||||||
write_details_to_disk(resp, credentials)
|
self.__write_details_to_disk(resp, credentials)
|
||||||
else:
|
else:
|
||||||
click.secho(
|
self.logger.debug(f'homeserver = {credentials["homeserver"]}; '
|
||||||
f'homeserver = {credentials["homeserver"]}; '
|
f' user = {credentials["user_id"]}')
|
||||||
f' user = {credentials["user_id"]}',
|
self.logger.warn(f'Failed to log in: {resp}')
|
||||||
fg='red',
|
|
||||||
)
|
|
||||||
click.secho(f'Failed to log in: {resp}', fg='red')
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
self._config = {
|
self.__config = {
|
||||||
'user_id': credentials['user_id'],
|
'user_id': credentials['user_id'],
|
||||||
'homeserver': credentials['homeserver'],
|
'homeserver': credentials['homeserver'],
|
||||||
'room_id': credentials['room_id'],
|
|
||||||
'device_name': credentials['device_name'],
|
'device_name': credentials['device_name'],
|
||||||
'device_id': resp.device_id,
|
'device_id': resp.device_id,
|
||||||
'access_token': resp.access_token,
|
'access_token': resp.access_token,
|
||||||
|
@ -177,15 +95,16 @@ class Bot():
|
||||||
|
|
||||||
# Otherwise the config file exists, so we'll use the stored credentials
|
# Otherwise the config file exists, so we'll use the stored credentials
|
||||||
else:
|
else:
|
||||||
|
self.logger.debug('Reading credentials.json')
|
||||||
# open the file in read-only mode
|
# open the file in read-only mode
|
||||||
with open(CONFIG_FILE, 'r') as f:
|
with open(Config.CONFIG_FILE, 'r') as f:
|
||||||
self._config = json.load(f)
|
self.__config = json.load(f)
|
||||||
# Initialize the matrix client based on credentials from file
|
# Initialize the matrix client based on credentials from file
|
||||||
client = AsyncClient(
|
client = AsyncClient(
|
||||||
self.config['homeserver'],
|
self.config['homeserver'],
|
||||||
self.config['user_id'],
|
self.config['user_id'],
|
||||||
device_id=self.config['device_id'],
|
device_id=self.config['device_id'],
|
||||||
store_path=STORE_PATH,
|
store_path=Config.STORE_PATH,
|
||||||
config=self.client_config,
|
config=self.client_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -194,36 +113,49 @@ class Bot():
|
||||||
device_id=self.config['device_id'],
|
device_id=self.config['device_id'],
|
||||||
access_token=self.config['access_token'],
|
access_token=self.config['access_token'],
|
||||||
)
|
)
|
||||||
# click.secho('Logged in using stored credentials.', fg='green')
|
self.logger.debug('Logged in using stored credentials.')
|
||||||
|
|
||||||
self._client = client
|
self.__client = client
|
||||||
await self.client.set_presence('online')
|
|
||||||
|
|
||||||
return client
|
return client
|
||||||
|
|
||||||
async def shutdown(self, sig: any) -> None:
|
async def sync(self) -> None:
|
||||||
await self.client.close()
|
self.logger.debug('Starting sync')
|
||||||
tasks = [
|
next_batch = self.__read_next_batch()
|
||||||
t for t in asyncio.all_tasks() if t is not asyncio.current_task()
|
|
||||||
]
|
|
||||||
for task in tasks:
|
|
||||||
task.cancel()
|
|
||||||
await task
|
|
||||||
|
|
||||||
await asyncio.gather(*tasks, return_exceptions=False)
|
if self.client.should_upload_keys:
|
||||||
self.loop.stop()
|
await self.client.keys_upload()
|
||||||
|
|
||||||
async def _verify(self) -> None:
|
if self.client.should_query_keys:
|
||||||
|
await self.client.keys_query()
|
||||||
|
|
||||||
|
if self.client.should_claim_keys:
|
||||||
|
await self.client.keys_claim(self.get_users_for_key_claiming())
|
||||||
|
|
||||||
|
await self.client.sync(timeout=30000,
|
||||||
|
full_state=True,
|
||||||
|
since=next_batch)
|
||||||
|
|
||||||
|
async def sync_forever(self) -> None:
|
||||||
|
# next_batch = self.__read_next_batch()
|
||||||
|
|
||||||
|
await self.client.sync_forever(timeout=30000, full_state=True)
|
||||||
|
|
||||||
|
async def verify(self) -> None:
|
||||||
"""Login and wait for and perform emoji verify."""
|
"""Login and wait for and perform emoji verify."""
|
||||||
# Set up event callbacks
|
# Set up event callbacks
|
||||||
client = await self._login()
|
client = await self.login()
|
||||||
client.add_to_device_callback(self._to_device_callback,
|
|
||||||
|
self.logger.debug('Adding callbacks')
|
||||||
|
client.add_to_device_callback(self.__to_device_callback,
|
||||||
(KeyVerificationEvent, ))
|
(KeyVerificationEvent, ))
|
||||||
# Sync encryption keys with the server
|
# Sync encryption keys with the server
|
||||||
# Required for participating in encrypted rooms
|
# Required for participating in encrypted rooms
|
||||||
if self.client.should_upload_keys:
|
if self.client.should_upload_keys:
|
||||||
await self.client.keys_upload()
|
await self.client.keys_upload()
|
||||||
click.secho('\nStarting verification process...', bold=True)
|
click.secho('\nStarting verification process...',
|
||||||
|
bold=True,
|
||||||
|
fg='green')
|
||||||
click.secho(
|
click.secho(
|
||||||
'\nThis program is ready and waiting for the other '
|
'\nThis program is ready and waiting for the other '
|
||||||
'party to initiate an emoji verification with us by '
|
'party to initiate an emoji verification with us by '
|
||||||
|
@ -231,56 +163,30 @@ class Bot():
|
||||||
'client.',
|
'client.',
|
||||||
fg='green',
|
fg='green',
|
||||||
)
|
)
|
||||||
await self.client.sync_forever(timeout=30000, full_state=True)
|
await self.sync_forever()
|
||||||
|
|
||||||
def verify(self) -> None:
|
async def run(self) -> None:
|
||||||
self._run_async(self._verify())
|
await self.login()
|
||||||
|
|
||||||
def run(self) -> None:
|
self.client.add_response_callback(self.__sync_callback,
|
||||||
self._run_async(self._run())
|
(SyncResponse, ))
|
||||||
|
self.client.add_event_callback(self.__message_callback,
|
||||||
|
(RoomMessage, ))
|
||||||
|
self.client.add_event_callback(self.__invite_callback, (InviteEvent, ))
|
||||||
|
|
||||||
async def _run(self) -> None:
|
await self.sync_forever()
|
||||||
fifo_name = FIFO_PATH
|
|
||||||
|
|
||||||
if os.path.exists(fifo_name):
|
async def find_room_by_id(self, room_id: str) -> (MatrixRoom, None):
|
||||||
os.remove(fifo_name)
|
rooms = self.client.rooms.keys()
|
||||||
|
if room_id in rooms:
|
||||||
os.mkfifo(fifo_name)
|
return self.client.rooms[room_id]
|
||||||
pipe_fd = os.open(fifo_name, (os.O_RDONLY | os.O_NONBLOCK))
|
return None
|
||||||
|
|
||||||
client = await self._login()
|
|
||||||
|
|
||||||
# we read the previously-written token...
|
|
||||||
next_batch_name = NEXT_BATCH_PATH
|
|
||||||
if os.path.exists(next_batch_name):
|
|
||||||
with open(next_batch_name, 'r') as next_batch_token:
|
|
||||||
# ... and well async_client to use it
|
|
||||||
self.client.next_batch = next_batch_token.read()
|
|
||||||
|
|
||||||
client.add_response_callback(self._sync_callback, SyncResponse)
|
|
||||||
client.add_event_callback(self._room_text_message_callback,
|
|
||||||
RoomMessageText)
|
|
||||||
await asyncio.gather(
|
|
||||||
self.client.sync_forever(timeout=30000, full_state=True),
|
|
||||||
self._fifo_reader(pipe_fd),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _run_async(self, future: asyncio.Future) -> None:
|
|
||||||
try:
|
|
||||||
self.loop.run_until_complete(future)
|
|
||||||
except Exception:
|
|
||||||
print(traceback.format_exc())
|
|
||||||
sys.exit(1)
|
|
||||||
except asyncio.exceptions.CancelledError:
|
|
||||||
sys.exit(0)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
def _ask_credentials(self) -> dict:
|
def _ask_credentials(self) -> dict:
|
||||||
"""Ask the user for credentials
|
"""Ask the user for credentials
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
homeserver = HOMESERVER_URL
|
homeserver = Config.HOMESERVER_URL
|
||||||
homeserver = click.prompt(
|
homeserver = click.prompt(
|
||||||
click.style('Enter your homeserver URL', bold=True),
|
click.style('Enter your homeserver URL', bold=True),
|
||||||
default=homeserver,
|
default=homeserver,
|
||||||
|
@ -300,11 +206,6 @@ class Bot():
|
||||||
default=device_name,
|
default=device_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
room_id = '!yourRoomId:gaja-group.com'
|
|
||||||
room_id = click.prompt(
|
|
||||||
click.style('Enter a default room ID to send to', bold=True),
|
|
||||||
default=room_id,
|
|
||||||
)
|
|
||||||
except click.exceptions.Abort:
|
except click.exceptions.Abort:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
@ -312,10 +213,11 @@ class Bot():
|
||||||
'homeserver': homeserver,
|
'homeserver': homeserver,
|
||||||
'user_id': user_id,
|
'user_id': user_id,
|
||||||
'device_name': device_name,
|
'device_name': device_name,
|
||||||
'room_id': room_id,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _to_device_callback(self, event): # noqa
|
# Callbacks
|
||||||
|
|
||||||
|
async def __to_device_callback(self, event): # noqa
|
||||||
"""Handle events sent to device."""
|
"""Handle events sent to device."""
|
||||||
try:
|
try:
|
||||||
client = self.client
|
client = self.client
|
||||||
|
@ -351,24 +253,24 @@ class Bot():
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if 'emoji' not in event.short_authentication_string:
|
if 'emoji' not in event.short_authentication_string:
|
||||||
click.secho(
|
click.echo(
|
||||||
'Other device does not support emoji verification '
|
'Other device does not support emoji verification '
|
||||||
f'{event.short_authentication_string}.',
|
f'{event.short_authentication_string}.')
|
||||||
fg='red',
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
resp = await client.accept_key_verification(
|
resp = await client.accept_key_verification(
|
||||||
event.transaction_id)
|
event.transaction_id)
|
||||||
if isinstance(resp, ToDeviceError):
|
if isinstance(resp, ToDeviceError):
|
||||||
click.secho(f'accept_key_verification failed with {resp}',
|
self.logger.warning(
|
||||||
fg='red')
|
f'accept_key_verification failed with {resp}',
|
||||||
|
fg='red')
|
||||||
|
|
||||||
sas = client.key_verifications[event.transaction_id]
|
sas = client.key_verifications[event.transaction_id]
|
||||||
|
|
||||||
todevice_msg = sas.share_key()
|
todevice_msg = sas.share_key()
|
||||||
resp = await client.to_device(todevice_msg)
|
resp = await client.to_device(todevice_msg)
|
||||||
if isinstance(resp, ToDeviceError):
|
if isinstance(resp, ToDeviceError):
|
||||||
click.secho(f'to_device failed with {resp}', fg='red')
|
self.logger.warning(f'to_device failed with {resp}',
|
||||||
|
fg='red')
|
||||||
|
|
||||||
elif isinstance(event, KeyVerificationCancel): # anytime
|
elif isinstance(event, KeyVerificationCancel): # anytime
|
||||||
""" at any time: receive KeyVerificationCancel
|
""" at any time: receive KeyVerificationCancel
|
||||||
|
@ -388,11 +290,8 @@ class Bot():
|
||||||
# client.cancel_key_verification(tx_id, reject=False)
|
# client.cancel_key_verification(tx_id, reject=False)
|
||||||
# here. The SAS flow is already cancelled.
|
# here. The SAS flow is already cancelled.
|
||||||
# We only need to inform the user.
|
# We only need to inform the user.
|
||||||
click.secho(
|
click.echo('\nVerification has been cancelled by '
|
||||||
'\nVerification has been cancelled by '
|
f'{event.sender} for reason "{event.reason}".')
|
||||||
f'{event.sender} for reason "{event.reason}".',
|
|
||||||
fg='yellow',
|
|
||||||
)
|
|
||||||
|
|
||||||
elif isinstance(event, KeyVerificationKey): # second step
|
elif isinstance(event, KeyVerificationKey): # second step
|
||||||
""" Second step is to receive KeyVerificationKey
|
""" Second step is to receive KeyVerificationKey
|
||||||
|
@ -458,7 +357,8 @@ class Bot():
|
||||||
resp = await client.cancel_key_verification(
|
resp = await client.cancel_key_verification(
|
||||||
event.transaction_id, reject=False)
|
event.transaction_id, reject=False)
|
||||||
if isinstance(resp, ToDeviceError):
|
if isinstance(resp, ToDeviceError):
|
||||||
print(f'cancel_key_verification failed with {resp}')
|
self.logger.warn(
|
||||||
|
f'cancel_key_verification failed with {resp}')
|
||||||
|
|
||||||
elif isinstance(event, KeyVerificationMac): # third step
|
elif isinstance(event, KeyVerificationMac): # third step
|
||||||
""" Third step is to receive KeyVerificationMac
|
""" Third step is to receive KeyVerificationMac
|
||||||
|
@ -490,7 +390,7 @@ class Bot():
|
||||||
else:
|
else:
|
||||||
resp = await client.to_device(todevice_msg)
|
resp = await client.to_device(todevice_msg)
|
||||||
if isinstance(resp, ToDeviceError):
|
if isinstance(resp, ToDeviceError):
|
||||||
print(f'to_device failed with {resp}')
|
self.logger.warn(f'to_device failed with {resp}')
|
||||||
# print(f'sas.we_started_it = {sas.we_started_it}\n'
|
# print(f'sas.we_started_it = {sas.we_started_it}\n'
|
||||||
# f'sas.sas_accepted = {sas.sas_accepted}\n'
|
# f'sas.sas_accepted = {sas.sas_accepted}\n'
|
||||||
# f'sas.canceled = {sas.canceled}\n'
|
# f'sas.canceled = {sas.canceled}\n'
|
||||||
|
@ -502,76 +402,102 @@ class Bot():
|
||||||
'to exit.',
|
'to exit.',
|
||||||
fg='green')
|
fg='green')
|
||||||
else:
|
else:
|
||||||
print(f'Received unexpected event type {type(event)}. '
|
self.logger.warn(
|
||||||
f'Event is {event}. Event will be ignored.')
|
f'Received unexpected event type {type(event)}. '
|
||||||
|
f'Event is {event}. Event will be ignored.')
|
||||||
except BaseException:
|
except BaseException:
|
||||||
print(traceback.format_exc())
|
self.logger.critical(traceback.format_exc())
|
||||||
|
|
||||||
def _sync_callback(self, event: any) -> None:
|
async def shutdown(self) -> None:
|
||||||
with open(NEXT_BATCH_PATH, 'w') as next_batch_token:
|
self.logger.info('Shutdown Bot')
|
||||||
|
for task in asyncio.Task.all_tasks():
|
||||||
|
task.cancel()
|
||||||
|
await self.client.close()
|
||||||
|
|
||||||
|
async def __sync_callback(self, event: any) -> None:
|
||||||
|
self.logger.debug('Client syncing and saving next batch token')
|
||||||
|
if self.__first_sync and len(self.client.invited_rooms) > 0:
|
||||||
|
for room in self.client.invited_rooms:
|
||||||
|
await self.client.join(room)
|
||||||
|
self.__first_sync = False
|
||||||
|
with open(Config.NEXT_BATCH_PATH, 'w') as next_batch_token:
|
||||||
next_batch_token.write(event.next_batch)
|
next_batch_token.write(event.next_batch)
|
||||||
|
|
||||||
async def _room_text_message_callback(self, room: any,
|
async def __invite_callback(self, source: MatrixRoom, sender: any) -> None:
|
||||||
message: any) -> None:
|
await self.client.join(source.room_id)
|
||||||
if room.room_id == self.config['room_id']:
|
|
||||||
if message.body.startswith('!help'):
|
|
||||||
await self.send_notice_to_room(
|
|
||||||
room.room_id, """##### Usage
|
|
||||||
|
|
||||||
**!help** - displays this help message
|
async def __text_message_callback(self, source: MatrixRoom,
|
||||||
""")
|
message: RoomMessageText) -> None:
|
||||||
|
self.logger.debug('Text Message Recieved: %s %s: %s', source.room_id,
|
||||||
|
message.sender, message.body)
|
||||||
|
|
||||||
async def _fifo_reader(self, pipe_fd: str) -> None:
|
async def __message_callback(self, source: MatrixRoom,
|
||||||
with os.fdopen(pipe_fd) as file:
|
message: RoomMessage) -> None:
|
||||||
while True:
|
print(message)
|
||||||
data = file.read()
|
self.logger.debug('Message Recieved')
|
||||||
if len(data) > 0:
|
if (isinstance(message, RoomMessageText)):
|
||||||
try:
|
await self.__text_message_callback(source, message)
|
||||||
data = data.split('\0')
|
|
||||||
for d in data:
|
|
||||||
if (len(d) > 0):
|
|
||||||
cmd = json.loads(d)
|
|
||||||
room_id = self.config['room_id']
|
|
||||||
if cmd['type'] == 'message':
|
|
||||||
if 'room_id' in cmd.keys():
|
|
||||||
room_id = cmd['room_id']
|
|
||||||
await self.send_notice_to_room(
|
|
||||||
room_id, cmd['content'])
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
print('JSON decode error:', e)
|
|
||||||
pass
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
|
def __signal_handler(self) -> None:
|
||||||
|
self.loop.create_task(self.shutdown())
|
||||||
|
|
||||||
@click.group(invoke_without_command=True)
|
# Files
|
||||||
@click.option('-v', '--verbose', count=True)
|
|
||||||
@click.pass_context
|
|
||||||
def cli(ctx: click.Context, verbose: int) -> None:
|
|
||||||
# click.secho('Matrix Bot\n', bold='true')
|
|
||||||
if ctx.invoked_subcommand is None:
|
|
||||||
bot = Bot()
|
|
||||||
bot.run()
|
|
||||||
|
|
||||||
|
def __write_details_to_disk(self, resp: LoginResponse,
|
||||||
|
credentials: dict) -> None:
|
||||||
|
"""Write the required login details to disk.
|
||||||
|
|
||||||
@cli.command('verify')
|
It will allow following logins to be made without password.
|
||||||
def verify_command() -> None:
|
|
||||||
bot = Bot()
|
|
||||||
bot.verify()
|
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
---------
|
||||||
|
resp : LoginResponse - successful client login response
|
||||||
|
credentials : dict - The credentials used to sign in
|
||||||
|
|
||||||
@cli.command('message')
|
"""
|
||||||
@click.argument('message')
|
# open the config file in write-mode
|
||||||
@click.option('-r', '--room', help='the room to send to')
|
with open(Config.CONFIG_FILE, 'w') as f:
|
||||||
def send_command(message: str, room: str) -> None:
|
# write the login details to disk
|
||||||
bot = Bot()
|
json.dump(
|
||||||
bot._run_async(bot.send_text_to_room(room, message))
|
{
|
||||||
|
'credentials': {
|
||||||
|
'homeserver': credentials['homeserver'],
|
||||||
|
'device_name': credentials['device_name'],
|
||||||
|
'user_id': resp.user_id,
|
||||||
|
'device_id': resp.device_id,
|
||||||
|
'access_token': resp.access_token,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
f,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __read_next_batch(self) -> (str, None):
|
||||||
|
# we read the previously-written token...
|
||||||
|
next_batch_name = Config.NEXT_BATCH_PATH
|
||||||
|
if os.path.exists(next_batch_name):
|
||||||
|
with open(next_batch_name, 'r') as next_batch_token:
|
||||||
|
# ... and well async_client to use it
|
||||||
|
self.client.next_batch = next_batch_token.read()
|
||||||
|
return self.client.next_batch
|
||||||
|
|
||||||
@cli.command('run')
|
# Properties
|
||||||
def run() -> None:
|
|
||||||
bot = Bot()
|
|
||||||
bot.run()
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loop(self) -> asyncio.AbstractEventLoop:
|
||||||
|
return self.__loop
|
||||||
|
|
||||||
if __name__ == '__main__':
|
@property
|
||||||
cli()
|
def logger(self) -> logging.Logger:
|
||||||
|
return self.__logger
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> dict:
|
||||||
|
return self.__config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_config(self) -> AsyncClientConfig:
|
||||||
|
return self.__client_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self) -> AsyncClient:
|
||||||
|
return self.__client
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import click
|
|
||||||
import json
|
|
||||||
|
|
||||||
from .config import FIFO_PATH
|
|
||||||
|
|
||||||
|
|
||||||
def send_message(message: str) -> None:
|
|
||||||
fifo_name = FIFO_PATH
|
|
||||||
|
|
||||||
if not os.path.exists(fifo_name):
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Starting two readers and one writer, but only one reader
|
|
||||||
# will be reading at the same time.
|
|
||||||
content = {'type': 'message', 'content': message}
|
|
||||||
with open(fifo_name, 'w') as file:
|
|
||||||
file.write(json.dumps(content))
|
|
||||||
file.write('\0')
|
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
|
||||||
def cli() -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.argument('message', nargs=-1)
|
|
||||||
def message(message: list) -> None:
|
|
||||||
send_message(' '.join(message))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
cli()
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from .bot import Bot
|
||||||
|
from .utils import run_async, setup_logger
|
||||||
|
from .config import Config
|
||||||
|
from .message import TextMessage, MarkdownMessage
|
||||||
|
from .exceptions import NoRoomException
|
||||||
|
|
||||||
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_loglevel(verbosity: int) -> int:
|
||||||
|
|
||||||
|
level = logging.ERROR
|
||||||
|
if verbosity == 1:
|
||||||
|
level = logging.WARNING
|
||||||
|
elif verbosity == 2:
|
||||||
|
level = logging.INFO
|
||||||
|
elif verbosity >= 3:
|
||||||
|
level = logging.DEBUG
|
||||||
|
|
||||||
|
return level
|
||||||
|
|
||||||
|
|
||||||
|
async def send_message(room_id: str, message: str, markdown: bool) -> None:
|
||||||
|
logger.debug('Sending a message to %s', room_id)
|
||||||
|
bot = Bot()
|
||||||
|
client = await bot.login()
|
||||||
|
await bot.sync()
|
||||||
|
room = await bot.find_room_by_id(room_id)
|
||||||
|
if markdown:
|
||||||
|
message = MarkdownMessage(message)
|
||||||
|
else:
|
||||||
|
message = TextMessage(message)
|
||||||
|
try:
|
||||||
|
await message.send(client, room)
|
||||||
|
except NoRoomException:
|
||||||
|
click.echo(f'No Room with id {room_id} found')
|
||||||
|
finally:
|
||||||
|
await bot.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
async def main_run() -> None:
|
||||||
|
logger.debug('Starting')
|
||||||
|
bot = Bot()
|
||||||
|
await bot.run()
|
||||||
|
|
||||||
|
|
||||||
|
async def main_verify() -> None:
|
||||||
|
logger.debug('Starting')
|
||||||
|
bot = Bot()
|
||||||
|
await bot.verify()
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(invoke_without_command=True)
|
||||||
|
@click.option('-v',
|
||||||
|
'--verbose',
|
||||||
|
count=True,
|
||||||
|
help='Set the loglevel (Can be used multiple times e.g. -vvv)')
|
||||||
|
@click.option('-b',
|
||||||
|
'--botname',
|
||||||
|
default='default',
|
||||||
|
help='The name of the bot to start')
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx: click.Context,
|
||||||
|
verbose: bool,
|
||||||
|
botname: str,
|
||||||
|
help: str = 'matrix-bot - A Bot That listens in matrix chatrooms'
|
||||||
|
) -> None:
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
|
||||||
|
Config.init(botname, get_loglevel(verbose))
|
||||||
|
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
click.echo(ctx.get_help())
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(help='Start the Bot')
|
||||||
|
@click.pass_context
|
||||||
|
@click.option('-v',
|
||||||
|
'--verbose',
|
||||||
|
count=True,
|
||||||
|
help='Set the loglevel (Can be used multiple times e.g. -vvv)')
|
||||||
|
def run(ctx: click.Context, verbose: int) -> None:
|
||||||
|
run_async(main_run())
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(help='Verify the bot with a matrix homeserver')
|
||||||
|
@click.pass_context
|
||||||
|
@click.option('-v',
|
||||||
|
'--verbose',
|
||||||
|
count=True,
|
||||||
|
help='Set the loglevel (Can be used multiple times e.g. -vvv)')
|
||||||
|
def verify(ctx: click.Context, verbose: int) -> None:
|
||||||
|
run_async(main_verify())
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(help='Send a message with the bot')
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument('room_id')
|
||||||
|
@click.argument('message', nargs=-1, required=True)
|
||||||
|
@click.option('-m', '--markdown/--no-markdown', default=False)
|
||||||
|
@click.option('-v',
|
||||||
|
'--verbose',
|
||||||
|
count=True,
|
||||||
|
help='Set the loglevel (Can be used multiple times e.g. -vvv)')
|
||||||
|
def send(ctx: click.Context, room_id: str, message: list, markdown: bool,
|
||||||
|
verbose: int) -> None:
|
||||||
|
run_async(send_message(room_id, ' '.join(message), markdown))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cli(obj={})
|
|
@ -1,15 +1,32 @@
|
||||||
import os
|
import os
|
||||||
|
import appdirs
|
||||||
|
|
||||||
# The directory containing the store and credentials file
|
|
||||||
CONFIG_DIRECTORY = '.'
|
|
||||||
# file to store credentials in case you want to run program multiple times
|
|
||||||
# login credentials JSON file
|
|
||||||
CONFIG_FILE = os.path.join(CONFIG_DIRECTORY, 'credentials.json')
|
|
||||||
# directory to store persistent data for end-to-end encryption
|
|
||||||
STORE_PATH = os.path.join(CONFIG_DIRECTORY, 'store') # local directory
|
|
||||||
FIFO_PATH = os.path.join(CONFIG_DIRECTORY, 'fifo') # local directory
|
|
||||||
NEXT_BATCH_PATH = os.path.join(CONFIG_DIRECTORY,
|
|
||||||
'next_batch') # local directory
|
|
||||||
|
|
||||||
# Default Homeserver URL
|
class Config:
|
||||||
HOMESERVER_URL = 'https://matrix.gaja-group.com'
|
APP_NAME = 'matrix-bot'
|
||||||
|
# Default Homeserver URL
|
||||||
|
HOMESERVER_URL = 'https://matrix.gaja-group.com'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init(cls, botname: str = 'default', loglevel: int = 40) -> None:
|
||||||
|
cls.LOGLEVEL = loglevel
|
||||||
|
cls.CONFIG_DIRECTORY = os.path.join(appdirs.user_config_dir(),
|
||||||
|
cls.APP_NAME, botname)
|
||||||
|
cls.DATA_DIRECTORY = os.path.join(appdirs.user_data_dir(),
|
||||||
|
cls.APP_NAME, botname)
|
||||||
|
|
||||||
|
if not os.path.exists(cls.CONFIG_DIRECTORY):
|
||||||
|
os.makedirs(cls.CONFIG_DIRECTORY)
|
||||||
|
|
||||||
|
if not os.path.exists(cls.DATA_DIRECTORY):
|
||||||
|
os.makedirs(cls.DATA_DIRECTORY)
|
||||||
|
cls.CONFIG_FILE = os.path.join(cls.CONFIG_DIRECTORY, 'config.json')
|
||||||
|
# directory to store persistent data for end-to-end encryption
|
||||||
|
cls.STORE_PATH = os.path.join(cls.DATA_DIRECTORY,
|
||||||
|
'store') # local directory
|
||||||
|
cls.NEXT_BATCH_PATH = os.path.join(cls.DATA_DIRECTORY,
|
||||||
|
'next_batch') # local directory
|
||||||
|
print(cls.CONFIG_DIRECTORY)
|
||||||
|
|
||||||
|
|
||||||
|
Config.init()
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
class NoRoomException(BaseException):
|
||||||
|
pass
|
|
@ -0,0 +1,89 @@
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from nio import AsyncClient, MatrixRoom
|
||||||
|
from markdown import markdown
|
||||||
|
|
||||||
|
from .utils import setup_logger
|
||||||
|
from .exceptions import NoRoomException
|
||||||
|
|
||||||
|
|
||||||
|
class Message(object):
|
||||||
|
def __init__(self,
|
||||||
|
_type: str = 'm.room.message',
|
||||||
|
loglevel: int = 10) -> None:
|
||||||
|
self.__type = _type
|
||||||
|
self.__ignore_unverified_devices = True
|
||||||
|
self.__logger = setup_logger(__name__)
|
||||||
|
|
||||||
|
async def send(self, client: AsyncClient,
|
||||||
|
room: (MatrixRoom, None)) -> None:
|
||||||
|
if room:
|
||||||
|
self.logger.debug('Sending message: %s', self.content)
|
||||||
|
await client.room_send(
|
||||||
|
room.room_id,
|
||||||
|
self.type,
|
||||||
|
self.content,
|
||||||
|
ignore_unverified_devices=self.ignore_unverified_devices)
|
||||||
|
else:
|
||||||
|
raise NoRoomException()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> str:
|
||||||
|
return self.__type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ignore_unverified_devices(self) -> bool:
|
||||||
|
return self.__ignore_unverified_devices
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logger(self) -> logging.Logger:
|
||||||
|
return self.__logger
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content(self) -> str:
|
||||||
|
raise NotImplementedError('content must be implemented')
|
||||||
|
|
||||||
|
|
||||||
|
class TextMessage(Message):
|
||||||
|
def __init__(self,
|
||||||
|
body: str,
|
||||||
|
msgtype: str = 'm.notice',
|
||||||
|
_type: str = 'm.room.message') -> None:
|
||||||
|
super().__init__(_type)
|
||||||
|
self.body = body
|
||||||
|
self.msgtype = msgtype
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content(self) -> dict:
|
||||||
|
content = {'msgtype': self.msgtype, 'body': self.body}
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownMessage(Message):
|
||||||
|
|
||||||
|
clean_regexp = re.compile('<.*?>')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
body: str,
|
||||||
|
msgtype: str = 'm.notice',
|
||||||
|
_type: str = 'm.room.message') -> None:
|
||||||
|
super().__init__(_type)
|
||||||
|
self.formatted_body = markdown(body)
|
||||||
|
self.format = 'org.matrix.custom.html'
|
||||||
|
self.body = self.__clean_html(self.formatted_body)
|
||||||
|
self.msgtype = msgtype
|
||||||
|
|
||||||
|
def __clean_html(self, raw_html: str) -> str:
|
||||||
|
cleantext = re.sub(self.clean_regexp, '', raw_html)
|
||||||
|
return cleantext
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content(self) -> dict:
|
||||||
|
content = {
|
||||||
|
'msgtype': self.msgtype,
|
||||||
|
'body': self.body,
|
||||||
|
'formatted_body': self.formatted_body,
|
||||||
|
'format': self.format
|
||||||
|
}
|
||||||
|
return content
|
|
@ -0,0 +1,30 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
|
||||||
|
def run_async(future: asyncio.Future) -> None:
|
||||||
|
global logger
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
try:
|
||||||
|
loop.run_until_complete(future)
|
||||||
|
except asyncio.exceptions.CancelledError:
|
||||||
|
loop.run_until_complete(asyncio.sleep(0.25))
|
||||||
|
finally:
|
||||||
|
loop.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logger(name: str = __name__) -> logging.Logger:
|
||||||
|
logger = logging.getLogger(name)
|
||||||
|
|
||||||
|
logger.setLevel(Config.LOGLEVEL)
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
ch.setLevel(Config.LOGLEVEL)
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
ch.setFormatter(formatter)
|
||||||
|
|
||||||
|
logger.addHandler(ch)
|
||||||
|
|
||||||
|
return logger
|
5
setup.py
5
setup.py
|
@ -7,7 +7,8 @@ setup(
|
||||||
install_requires=['Click', 'matrix-nio[e2e]', 'markdown'],
|
install_requires=['Click', 'matrix-nio[e2e]', 'markdown'],
|
||||||
entry_points="""
|
entry_points="""
|
||||||
[console_scripts]
|
[console_scripts]
|
||||||
matrix-bot=matrix_bot.bot:cli
|
matrix-bot=matrix_bot.cli:cli
|
||||||
matrix-botctl=matrix_bot.botctl:cli
|
matrix-send=matrix_bot.cli:send
|
||||||
|
matrix-verify=matrix_bot.cli:verify
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue