From b58b204a8e65caa46c5793e97d457038f1752569 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Fri, 9 Jul 2021 17:13:51 -0700 Subject: [PATCH] Add matrix-eavesdrop container image This builds a container image with a simple eavesdrop bot for Matrix. Change-Id: I5304b4ec974b84886ac969b59cfcec8dec2febf9 --- docker/matrix-eavesdrop/Dockerfile | 30 ++++ docker/matrix-eavesdrop/src/bindep.txt | 7 + .../src/eavesdrop/__init__.py | 0 docker/matrix-eavesdrop/src/eavesdrop/bot.py | 155 ++++++++++++++++++ docker/matrix-eavesdrop/src/setup.py | 13 ++ tools/run-bashate.sh | 2 +- zuul.d/docker-images/eavesdrop.yaml | 31 ++++ zuul.d/project.yaml | 11 ++ 8 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 docker/matrix-eavesdrop/Dockerfile create mode 100644 docker/matrix-eavesdrop/src/bindep.txt create mode 100644 docker/matrix-eavesdrop/src/eavesdrop/__init__.py create mode 100644 docker/matrix-eavesdrop/src/eavesdrop/bot.py create mode 100644 docker/matrix-eavesdrop/src/setup.py create mode 100644 zuul.d/docker-images/eavesdrop.yaml diff --git a/docker/matrix-eavesdrop/Dockerfile b/docker/matrix-eavesdrop/Dockerfile new file mode 100644 index 0000000000..e64c6846f6 --- /dev/null +++ b/docker/matrix-eavesdrop/Dockerfile @@ -0,0 +1,30 @@ +# Copyright (C) 2021 Acme Gating, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM docker.io/opendevorg/python-builder:3.9 as builder +RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list +# ENV DEBIAN_FRONTEND=noninteractive + +COPY src /tmp/src +RUN assemble + +FROM docker.io/opendevorg/python-base:3.9 as eavesdrop +RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list + +COPY --from=builder /output/ /output +RUN /output/install-from-bindep \ + && rm -rf /output + +CMD ["eavesdrop"] diff --git a/docker/matrix-eavesdrop/src/bindep.txt b/docker/matrix-eavesdrop/src/bindep.txt new file mode 100644 index 0000000000..16b91ae828 --- /dev/null +++ b/docker/matrix-eavesdrop/src/bindep.txt @@ -0,0 +1,7 @@ +gcc [compile test] +libc6-dev [compile test] +libffi-dev [compile test] +libolm-dev/buster-backports [compile test] +make [compile test] +python3-dev [compile test] +libolm3/buster-backports diff --git a/docker/matrix-eavesdrop/src/eavesdrop/__init__.py b/docker/matrix-eavesdrop/src/eavesdrop/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/matrix-eavesdrop/src/eavesdrop/bot.py b/docker/matrix-eavesdrop/src/eavesdrop/bot.py new file mode 100644 index 0000000000..2e7622d1a4 --- /dev/null +++ b/docker/matrix-eavesdrop/src/eavesdrop/bot.py @@ -0,0 +1,155 @@ +#!/usr/bin/python3 + +# Copyright (C) 2021 Acme Gating, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import time +import json +import os +import sys +import getpass +import socket +import yaml +import logging +import datetime + +logging.basicConfig(level=logging.INFO) + +from nio import AsyncClient, AsyncClientConfig, LoginResponse, RoomMessageText +from nio.store.database import DefaultStore + + +class Bot: + def __init__(self): + self.log = logging.getLogger('bot') + self.config_path = os.environ.get("MATRIX_CONFIG_FILE", + "/config/config.yaml") + self.load_config() + self.cred_path = os.path.join( + self.config['data_dir'], 'credentials.json') + self.device_name = socket.gethostname() + self.room_map = {} + + async def login(self): + config = AsyncClientConfig( + store=DefaultStore, + store_sync_tokens=True) + creds = self.load_creds() + if creds: + self.log.info("Restoring previous session") + self.client = AsyncClient(self.config['homeserver'], + store_path=self.config['data_dir'], + config=config) + self.client.restore_login( + user_id=self.config['user_id'], + device_id=creds["device_id"], + access_token=creds["access_token"], + ) + else: + self.log.info("Creating new session") + self.client = AsyncClient(self.config['homeserver'], + self.config['user_id'], + store_path=self.config['data_dir'], + config=config) + resp = await self.client.login( + self.config['password'], device_name=self.device_name) + if (isinstance(resp, LoginResponse)): + self.save_creds(resp.device_id, resp.access_token) + else: + self.log.error(resp) + raise Exception("Error logging in") + # Load the sync tokens + self.client.load_store() + + def load_config(self): + with open(self.config_path) as f: + data = yaml.safe_load(f) + self.rooms = data['rooms'] + self.config = data['config'] + + def save_creds(self, device_id, token): + data = { + 'device_id': device_id, + 'access_token': token, + } + with open(self.cred_path, 'w') as f: + json.dump(data, f) + + def load_creds(self): + if os.path.exists(self.cred_path): + with open(self.cred_path) as f: + data = json.load(f) + return data + + async def join_rooms(self): + new = set() + old = set() + resp = await self.client.joined_rooms() + for room in resp.rooms: + old.add(room) + for room in self.rooms: + self.log.info("Join room %s", room['id']) + resp = await self.client.join(room['id']) + new.add(resp.room_id) + # Store the canonical room id, since the one in the config + # file may be an alias + self.room_map[resp.room_id] = room + os.makedirs(room['path'], exist_ok=True) + for room in old-new: + self.log.info("Leave room %s", room['id']) + await self.client.room_leave(room) + + async def message_callback(self, room, event): + config_room = self.room_map.get(room.room_id) + if not config_room: + return + room_name = config_room['id'].split(':')[0] + ts = datetime.datetime.utcfromtimestamp(event.server_timestamp/1000.0) + event_date = str(ts.date()) + event_time = str(ts.time())[:8] + room_path = config_room['path'] + if not room_path.startswith('/'): + room_path = os.path.join(self.config['log_dir'], room_path) + filename = f'{room_name}.{event_date}.log' + logpath = os.path.join(room_path, filename) + body = event.body + line = f'{event_date}T{event_time} <{event.sender}> {body}\n' + self.log.info('Logging %s %s', room.room_id, line[:-1]) + with open(logpath, 'a') as f: + f.write(line) + + async def run(self): + await self.login() + await self.join_rooms() + self.client.add_event_callback(self.message_callback, RoomMessageText) + try: + await self.client.sync_forever(timeout=30000, full_state=True) + finally: + await self.client.close() + + +async def _main(): + while True: + try: + bot = Bot() + await bot.run() + except Exception: + bot.log.exception("Error:") + time.sleep(10) + + +def main(): + asyncio.get_event_loop().run_until_complete(_main()) diff --git a/docker/matrix-eavesdrop/src/setup.py b/docker/matrix-eavesdrop/src/setup.py new file mode 100644 index 0000000000..6089a7fad0 --- /dev/null +++ b/docker/matrix-eavesdrop/src/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup, find_packages + +setup( + name='eavesdrop', + version='0.0.1', + packages=find_packages(), + install_requires=['matrix-nio[e2e]', 'PyYaml'], + entry_points={ + 'console_scripts': [ + 'eavesdrop = eavesdrop.bot:main', + ] + } +) diff --git a/tools/run-bashate.sh b/tools/run-bashate.sh index ee166fd46d..fd6328812b 100755 --- a/tools/run-bashate.sh +++ b/tools/run-bashate.sh @@ -1,4 +1,4 @@ #!/bin/bash ROOT=$(readlink -fn $(dirname $0)/.. ) -find $ROOT -not -wholename \*.tox/\* -and \( -name \*.sh -or -name \*rc -or -name functions\* \) -print0 | xargs -0 bashate -i E006 -v +find $ROOT -type f -not -wholename \*.tox/\* -and \( -name \*.sh -or -name \*rc -or -name functions\* \) -print0 | xargs -0 bashate -i E006 -v diff --git a/zuul.d/docker-images/eavesdrop.yaml b/zuul.d/docker-images/eavesdrop.yaml new file mode 100644 index 0000000000..946687d188 --- /dev/null +++ b/zuul.d/docker-images/eavesdrop.yaml @@ -0,0 +1,31 @@ +# matrix-eavesdrop jobs +- job: + name: system-config-build-image-matrix-eavesdrop + description: Build a matrix-eavesdrop image. + parent: system-config-build-image + requires: &matrix-eavesdrop_requires + - python-base-3.9-container-image + - python-builder-3.9-container-image + provides: matrix-eavesdrop-container-image + vars: &matrix-eavesdrop_vars + docker_images: + - context: docker/matrix-eavesdrop + repository: opendevorg/matrix-eavesdrop + files: &matrix-eavesdrop_files + - docker/matrix-eavesdrop/.* + +- job: + name: system-config-upload-image-matrix-eavesdrop + description: Build and upload a matrix-eavesdrop image. + parent: system-config-upload-image + requires: *matrix-eavesdrop_requires + provides: matrix-eavesdrop-container-image + vars: *matrix-eavesdrop_vars + files: *matrix-eavesdrop_files + +- job: + name: system-config-promote-image-matrix-eavesdrop + description: Promote a previously published matrix-eavesdrop image to latest. + parent: system-config-promote-image + vars: *matrix-eavesdrop_vars + files: *matrix-eavesdrop_files diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index 24c10445ae..13988c548f 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -122,6 +122,11 @@ - name: opendev-buildset-registry - name: system-config-build-image-python-builder-3.9 soft: true + - system-config-build-image-matrix-eavesdrop: + dependencies: + - name: opendev-buildset-registry + - name: system-config-build-image-python-builder-3.9 + soft: true - system-config-build-image-python-base-3.7 - system-config-build-image-python-base-3.8 - system-config-build-image-python-base-3.9 @@ -247,6 +252,11 @@ - name: opendev-buildset-registry - name: system-config-build-image-python-builder-3.9 soft: true + - system-config-build-image-matrix-eavesdrop: + dependencies: + - name: opendev-buildset-registry + - name: system-config-build-image-python-builder-3.9 + soft: true - system-config-upload-image-python-base-3.7 - system-config-upload-image-python-base-3.8 - system-config-upload-image-python-base-3.9 @@ -272,6 +282,7 @@ - system-config-promote-image-accessbot - system-config-promote-image-refstack - system-config-promote-image-ircbot + - system-config-promote-image-matrix-eavesdrop - system-config-promote-image-python-base-3.7 - system-config-promote-image-python-base-3.8 - system-config-promote-image-python-base-3.9