Compare commits

..

No commits in common. "c60809b94ab12c10a48600001d6a9a7458c32515" and "de772841adff5f686cca6ff76bfa9d2bbfa60025" have entirely different histories.

8 changed files with 203 additions and 252 deletions

3
.gitignore vendored
View File

@ -140,9 +140,6 @@ cython_debug/
# Project specific # Project specific
credentials.json credentials.json
store/* store/*
next_batch
fifo
bot
# VSCode # VSCode
.vscode/* .vscode/*

View File

@ -7,6 +7,7 @@ verify_ssl = true
flake8 = "*" flake8 = "*"
flake8-quotes = "*" flake8-quotes = "*"
flake8-annotations = "*" flake8-annotations = "*"
black = "*"
yapf = "*" yapf = "*"
[packages] [packages]

99
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "a17f7aa5b096658663f9e578d693b9f69f01711f3776f8ede29ac1ff34175380" "sha256": "b572da4f2fb6963d3f61a7a2234f625fd8c09274e429df366367fb4b359510a0"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -331,6 +331,37 @@
} }
}, },
"develop": { "develop": {
"appdirs": {
"hashes": [
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
],
"version": "==1.4.4"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==19.3.0"
},
"black": {
"hashes": [
"sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
"sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
],
"index": "pypi",
"version": "==19.10b0"
},
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"flake8": { "flake8": {
"hashes": [ "hashes": [
"sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c",
@ -361,6 +392,13 @@
], ],
"version": "==0.6.1" "version": "==0.6.1"
}, },
"pathspec": {
"hashes": [
"sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0",
"sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"
],
"version": "==0.8.0"
},
"pycodestyle": { "pycodestyle": {
"hashes": [ "hashes": [
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
@ -377,6 +415,65 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.2.0" "version": "==2.2.0"
}, },
"regex": {
"hashes": [
"sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204",
"sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162",
"sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f",
"sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb",
"sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6",
"sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7",
"sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88",
"sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99",
"sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644",
"sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a",
"sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840",
"sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067",
"sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd",
"sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4",
"sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e",
"sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89",
"sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e",
"sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc",
"sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf",
"sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341",
"sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"
],
"version": "==2020.7.14"
},
"toml": {
"hashes": [
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
],
"version": "==0.10.1"
},
"typed-ast": {
"hashes": [
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
"sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
"sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
"sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
"sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
"sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
"sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
"sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
"sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
"sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
"sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
"sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
"sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
"sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
"sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
"sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
"sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
"sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
"sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
"sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
],
"version": "==1.4.1"
},
"yapf": { "yapf": {
"hashes": [ "hashes": [
"sha256:3000abee4c28daebad55da6c85f3cd07b8062ce48e2e9943c8da1b9667d48427", "sha256:3000abee4c28daebad55da6c85f3cd07b8062ce48e2e9943c8da1b9667d48427",

View File

@ -32,13 +32,3 @@ After that you can begin sending messages
source venv/bin/activate source venv/bin/activate
matrix-bot message "Message Content" # To send to the default room matrix-bot message "Message Content" # To send to the default room
matrix-bot message -r '!yourRoomId' "Message Content" # To send to a specific room 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`
cd MATRIX_BOT_DIR
source venv/bin/activate
matrix-bot
# Then you can send messages with
matrix-botctl message "Message Content"

View File

@ -1,18 +1,33 @@
from nio import (AsyncClient, AsyncClientConfig, LoginResponse, from nio import (
KeyVerificationEvent, KeyVerificationStart, AsyncClient,
KeyVerificationCancel, KeyVerificationKey, KeyVerificationMac, AsyncClientConfig,
ToDeviceError, LocalProtocolError, SyncResponse, LoginResponse,
RoomMessageText) KeyVerificationEvent,
KeyVerificationStart,
KeyVerificationCancel,
KeyVerificationKey,
KeyVerificationMac,
ToDeviceError,
LocalProtocolError,
)
import click import click
import traceback import traceback
import sys import sys
import os import os
import json import json
import asyncio import asyncio
import markdown import signal
from .config import (CONFIG_FILE, STORE_PATH, NEXT_BATCH_PATH, HOMESERVER_URL, # The directory containing the store and credentials file
FIFO_PATH) 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
# Default Homeserver URL
HOMESERVER_URL = 'https://matrix.gaja-group.com'
def write_details_to_disk(resp: LoginResponse, credentials: dict) -> None: def write_details_to_disk(resp: LoginResponse, credentials: dict) -> None:
@ -46,12 +61,16 @@ def write_details_to_disk(resp: LoginResponse, credentials: dict) -> None:
) )
class Bot(): class Bot:
def __init__(self) -> None: def __init__(self) -> None:
self._loop = asyncio.get_event_loop() self._loop = asyncio.get_event_loop()
signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)
for sig in signals:
self._loop.add_signal_handler(
sig, lambda sig=sig: asyncio.create_task(self.shutdown(sig)))
@classmethod @classmethod
async def login(cls: any) -> any: async def login(cls: any) -> object:
bot = cls() bot = cls()
await bot._login() await bot._login()
return bot return bot
@ -72,41 +91,24 @@ class Bot():
def config(self) -> dict: def config(self) -> dict:
return self._config return self._config
async def send_message_to_room( def send_text_message_to_room(self, room_id: str, message: str) -> None:
self, async def send_message(room_id: str, message: str) -> None:
room_id: str,
content: dict,
message_type: str = 'm.room.message') -> 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
content = {'msgtype': 'm.text', 'body': message}
await self._login() await self._login()
if room_id is None: if room_id is None:
room_id = self.config['room_id'] room_id = self.config['room_id']
await self.client.sync(timeout=30000, full_state=True) await self.client.sync(timeout=30000, full_state=True)
await self.client.room_send( await self.client.room_send(
room_id=room_id, room_id=room_id,
message_type=message_type, message_type='m.room.message',
content=content, content=content,
ignore_unverified_devices=True, ignore_unverified_devices=True,
) )
await self.client.set_presence('offline')
await self.client.close() await self.client.close()
async def send_text_to_room(self, room_id: str, message: str) -> None: self.loop.run_until_complete(send_message(room_id, message))
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: 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.
@ -129,7 +131,7 @@ class Bot():
if not os.path.exists(STORE_PATH): if not os.path.exists(STORE_PATH):
os.makedirs(STORE_PATH) os.makedirs(STORE_PATH)
credentials = self._ask_credentials() credentials = self.ask_credentials()
# Initialize the matrix client # Initialize the matrix client
client = AsyncClient( client = AsyncClient(
@ -163,7 +165,7 @@ class Bot():
'user_id': credentials['user_id'], 'user_id': credentials['user_id'],
'homeserver': credentials['homeserver'], 'homeserver': credentials['homeserver'],
'room_id': credentials['room_id'], 'room_id': credentials['room_id'],
'device_name': credentials['device_name'], 'device_name': resp['device_name'],
'device_id': resp.device_id, 'device_id': resp.device_id,
'access_token': resp.access_token, 'access_token': resp.access_token,
} }
@ -197,7 +199,6 @@ class Bot():
# click.secho('Logged in using stored credentials.', fg='green') # click.secho('Logged in using stored credentials.', fg='green')
self._client = client self._client = client
await self.client.set_presence('online')
return client return client
@ -213,11 +214,12 @@ class Bot():
await asyncio.gather(*tasks, return_exceptions=False) await asyncio.gather(*tasks, return_exceptions=False)
self.loop.stop() self.loop.stop()
async def _verify(self) -> None: def verify(self) -> None:
async def verify() -> 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, 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
@ -233,53 +235,11 @@ class Bot():
) )
await self.client.sync_forever(timeout=30000, full_state=True) await self.client.sync_forever(timeout=30000, full_state=True)
def verify(self) -> None: self.loop.run_until_complete(verify())
self._run_async(self._verify())
def run(self) -> None:
self._run_async(self._run())
async def _run(self) -> None:
fifo_name = FIFO_PATH
if os.path.exists(fifo_name):
os.remove(fifo_name)
os.mkfifo(fifo_name)
pipe_fd = os.open(fifo_name, (os.O_RDONLY | os.O_NONBLOCK))
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:
homeserver = HOMESERVER_URL homeserver = HOMESERVER_URL
homeserver = click.prompt( homeserver = click.prompt(
click.style('Enter your homeserver URL', bold=True), click.style('Enter your homeserver URL', bold=True),
@ -305,8 +265,6 @@ class Bot():
click.style('Enter a default room ID to send to', bold=True), click.style('Enter a default room ID to send to', bold=True),
default=room_id, default=room_id,
) )
except click.exceptions.Abort:
sys.exit(0)
return { return {
'homeserver': homeserver, 'homeserver': homeserver,
@ -315,7 +273,7 @@ class Bot():
'room_id': room_id, 'room_id': room_id,
} }
async def _to_device_callback(self, event): # noqa 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
@ -497,60 +455,21 @@ class Bot():
# f'sas.timed_out = {sas.timed_out}\n' # f'sas.timed_out = {sas.timed_out}\n'
# f'sas.verified = {sas.verified}\n' # f'sas.verified = {sas.verified}\n'
# f'sas.verified_devices = {sas.verified_devices}\n') # f'sas.verified_devices = {sas.verified_devices}\n')
click.secho( click.secho('Emoji verification was successful!',
'Emoji verification was successful! Please use Ctrl+C '
'to exit.',
fg='green') fg='green')
await self.shutdown([], self._loop)
else: else:
print(f'Received unexpected event type {type(event)}. ' print(f'Received unexpected event type {type(event)}. '
f'Event is {event}. Event will be ignored.') f'Event is {event}. Event will be ignored.')
except BaseException: except BaseException:
print(traceback.format_exc()) print(traceback.format_exc())
def _sync_callback(self, event: any) -> None:
with open(NEXT_BATCH_PATH, 'w') as next_batch_token:
next_batch_token.write(event.next_batch)
async def _room_text_message_callback(self, room: any, @click.group()
message: any) -> None:
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 _fifo_reader(self, pipe_fd: str) -> None:
with os.fdopen(pipe_fd) as file:
while True:
data = file.read()
if len(data) > 0:
try:
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)
@click.group(invoke_without_command=True)
@click.option('-v', '--verbose', count=True) @click.option('-v', '--verbose', count=True)
@click.pass_context def cli(verbose: int) -> None:
def cli(ctx: click.Context, verbose: int) -> None:
# click.secho('Matrix Bot\n', bold='true') # click.secho('Matrix Bot\n', bold='true')
if ctx.invoked_subcommand is None: pass
bot = Bot()
bot.run()
@cli.command('verify') @cli.command('verify')
@ -564,13 +483,7 @@ def verify_command() -> None:
@click.option('-r', '--room', help='the room to send to') @click.option('-r', '--room', help='the room to send to')
def send_command(message: str, room: str) -> None: def send_command(message: str, room: str) -> None:
bot = Bot() bot = Bot()
bot._run_async(bot.send_text_to_room(room, message)) bot.send_text_message_to_room(room, message)
@cli.command('run')
def run() -> None:
bot = Bot()
bot.run()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -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()

View File

@ -1,15 +0,0 @@
import os
# 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
HOMESERVER_URL = 'https://matrix.gaja-group.com'

View File

@ -4,10 +4,13 @@ setup(
name='matrix-bot', name='matrix-bot',
version='0.1', version='0.1',
py_modules=['matrix_bot'], py_modules=['matrix_bot'],
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.bot:cli
matrix-botctl=matrix_bot.botctl:cli
""", """,
) )