From b58b204a8e65caa46c5793e97d457038f1752569 Mon Sep 17 00:00:00 2001
From: "James E. Blair" <jim@acmegating.com>
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