Skip to content

Commit

Permalink
feat: use unique users.json list and manage welcome message when ther…
Browse files Browse the repository at this point in the history
…e haven't been any previous activity
  • Loading branch information
bolinocroustibat committed Jun 17, 2024
1 parent 628d44d commit fe4c815
Show file tree
Hide file tree
Showing 10 changed files with 94 additions and 48 deletions.
1 change: 0 additions & 1 deletion .github/workflows/build_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ jobs:
run: |
cd ./${{ env.CI_JOB_ID }}
touch .env
echo JOIN_ON_INVITE=${{ vars.JOIN_ON_INVITE }} >> .env
echo SALT=${{ secrets.SALT }} >> .env
echo MATRIX_HOME_SERVER=${{ secrets.MATRIX_HOME_SERVER }} >> .env
echo MATRIX_BOT_USERNAME=${{ secrets.MATRIX_BOT_USERNAME }} >> .env
Expand Down
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
# Project-specific
store
session.txt
users_pending.txt
users_whitelist.txt
users.json

*.pyc
__pycache__
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ cp app/.env.example app/.env

Les variables d'environnement à renseigner sont les suivantes :

- `JOIN_ON_INVITE` : booléen facultatif pour activer ou non l'acceptation automatique des invitations dans les salons (exemple : `JOIN_ON_INVITE=True`. Par défaut, `False`)
- `SALT` : il est conseillé de changer la valeur du salt pour ne pas avoir celle par défaut. Il faudra en revanche qu'elle de change pas entre deux sessions.
- `MATRIX_HOME_SERVER` : l'URL du serveur Matrix à utiliser (exemple : `MATRIX_HOME_SERVER="https://matrix.agent.ministere_example.tchap.gouv.fr"`)
- `MATRIX_BOT_USERNAME` : le nom d'utilisateur du bot Matrix (exemple : `MATRIX_BOT_USERNAME="tchapbot@ministere_example.gouv.fr"`)
Expand Down Expand Up @@ -167,7 +166,6 @@ cp app/.env.example app/.env

The following environment variables must be entered:

- `JOIN_ON_INVITE`: optional boolean to enable or disable automatic acceptance of invitations to Tchap rooms (example: `JOIN_ON_INVITE=True`. Default: `False`).
- `SALT`: it is advisable to change the salt value to avoid having the default one. However, it must not change between sessions.
- `MATRIX_HOME_SERVER`: the URL of the Matrix server to be used (example: `MATRIX_HOME_SERVER=“https://matrix.agent.ministere_example.tchap.gouv.fr”`).
- `MATRIX_BOT_USERNAME`: the Matrix bot username (example: `MATRIX_BOT_USERNAME=“tchapbot@ministere_example.gouv.fr”`)
Expand Down
1 change: 0 additions & 1 deletion app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

VERBOSE=False
SYSTEMD_LOGGING=True
JOIN_ON_INVITE=True
SALT=b"\xce,\xa1\xc6lY\x80\xe3X}\x91\xa60m\xa8N"
MATRIX_HOME_SERVER="https://matrix.agent.ministere_example.tchap.gouv.fr"
MATRIX_BOT_USERNAME="jean.quidam@ministere_example.gouv.fr"
Expand Down
40 changes: 17 additions & 23 deletions app/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#
# SPDX-License-Identifier: MIT

import time
from collections import defaultdict
from dataclasses import dataclass

Expand Down Expand Up @@ -144,25 +143,7 @@ def decorator(func):
)
async def help(ep: EventParser, matrix_client: MatrixClient):
config = user_configs[ep.sender]
await matrix_client.room_typing(ep.room.room_id)
await matrix_client.send_markdown_message(ep.room.room_id, command_registry.get_help(config))


@register_feature(
group="albert",
onEvent=RoomMemberEvent,
# @DEBUG: RoomCreateEvent is not captured ?
help=None,
)
async def albert_welcome(ep: EventParser, matrix_client: MatrixClient):
"""
Receive the join/invite event and send the welcome/help message
"""
config = user_configs[ep.sender]
ep.only_on_direct_message()
ep.only_on_join()
config.update_last_activity()
time.sleep(3) # wait for the room to be ready - otherwise the encryption seems to be not ready
await ep.only_allowed_sender(config)
await matrix_client.room_typing(ep.room.room_id)
await matrix_client.send_markdown_message(ep.room.room_id, command_registry.get_help(config))

Expand All @@ -175,6 +156,7 @@ async def albert_welcome(ep: EventParser, matrix_client: MatrixClient):
)
async def albert_reset(ep: EventParser, matrix_client: MatrixClient):
config = user_configs[ep.sender]
await ep.only_allowed_sender(config)
if config.albert_with_history:
config.update_last_activity()
await matrix_client.room_typing(ep.room.room_id)
Expand All @@ -195,6 +177,7 @@ async def albert_reset(ep: EventParser, matrix_client: MatrixClient):
)
async def albert_conversation(ep: EventParser, matrix_client: MatrixClient):
config = user_configs[ep.sender]
await ep.only_allowed_sender(config)
await matrix_client.room_typing(ep.room.room_id)
if config.albert_with_history:
config.albert_with_history = False
Expand All @@ -216,6 +199,7 @@ async def albert_conversation(ep: EventParser, matrix_client: MatrixClient):
)
async def albert_debug(ep: EventParser, matrix_client: MatrixClient):
config = user_configs[ep.sender]
await ep.only_allowed_sender(config)
await matrix_client.room_typing(ep.room.room_id)
debug_message = f"Configuration actuelle :\n\n"
debug_message += f"- Version: {APP_VERSION}\n"
Expand All @@ -237,6 +221,7 @@ async def albert_debug(ep: EventParser, matrix_client: MatrixClient):
)
async def albert_mode(ep: EventParser, matrix_client: MatrixClient):
config = user_configs[ep.sender]
await ep.only_allowed_sender(config)
await matrix_client.room_typing(ep.room.room_id)
commands = ep.event.body.split()
# Get all available mode for the current model
Expand All @@ -263,8 +248,8 @@ async def albert_mode(ep: EventParser, matrix_client: MatrixClient):
)
async def albert_sources(ep: EventParser, matrix_client: MatrixClient):
config = user_configs[ep.sender]
await ep.only_allowed_sender(config)
await matrix_client.room_typing(ep.room.room_id)

try:
if config.albert_stream_id:
sources = generate_sources(config=config, stream_id=config.albert_stream_id)
Expand Down Expand Up @@ -294,8 +279,17 @@ async def albert_answer(ep: EventParser, matrix_client: MatrixClient):
"""
Receive a message event which is not a command, send the prompt to Albert API and return the response to the user
"""
# user_prompt: str = await ep.hl()
config = user_configs[ep.sender]
await ep.only_allowed_sender(config)

# If the user has never used Albert, we send the help message
if not config.has_activity(ep.sender):
config.update_last_activity(ep.sender)
await matrix_client.room_typing(ep.room.room_id)
await matrix_client.send_markdown_message(
ep.room.room_id, command_registry.get_help(config)
)

user_prompt = ep.event.body
if user_prompt.startswith(COMMAND_PREFIX):
raise EventNotConcerned
Expand All @@ -313,7 +307,7 @@ async def albert_answer(ep: EventParser, matrix_client: MatrixClient):
ep.room.room_id, reset_message, msgtype="m.notice"
)

config.update_last_activity()
config.update_last_activity(ep.sender)
await matrix_client.room_typing(ep.room.room_id, typing_state=True, timeout=180_000)
try:
answer = generate(config=config, query=query)
Expand Down
48 changes: 42 additions & 6 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import tomllib
from pathlib import Path

from matrix_bot.config import bot_lib_config
from matrix_bot.config import bot_lib_config, logger
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict

Expand Down Expand Up @@ -45,7 +45,8 @@ class Config(BaseConfig):
description="List of allowed Tchap users email domains allowed to use Albert Tchap bot",
)
groups_used: list[str] = Field(["basic"], description="List of commands groups to use")
last_activity: int = Field(int(time.time()), description="Last activity timestamp")
last_activity: int | None = Field(None, description="Last activity timestamp")
authorized: bool | None = Field(None, description="is the user authorized to use the bot")
# Albert API settings
albert_api_url: str = Field("http://localhost:8090/api/v2", description="Albert API base URL")
albert_api_token: str = Field("", description="Albert API Token")
Expand All @@ -59,12 +60,47 @@ class Config(BaseConfig):
albert_chat_id: int | None = Field(None, description="Current chat id")
albert_stream_id: int | None = Field(None, description="Current stream id")

def is_authorized(self, sender_id: str) -> bool:
if not hasattr(self, "authorized") or self.authorized is None:
# The authorization status is not known yet, it might be because:
# - it's the first time the user talks to the bot
# - the bot has been restarted and lost the user config
# For both cases, we need to check it in the persistent storage
if not bot_lib_config.users.get(sender_id):
self.authorized = False
# Add user to pending users list
bot_lib_config.users[sender_id] = {"authorized": False, "has_activity": False}
bot_lib_config.save_users()
else:
self.authorized = bot_lib_config.users[sender_id]["authorized"]
return self.authorized

def has_activity(self, sender_id: str) -> bool:
if not hasattr(self, "last_activity") or self.last_activity is None:
# The last_activity is not known, it might be because:
# - it's the first time the user talks to the bot
# - the bot has been restarted and lost the user config
# For this last case, we need to check it in the persistent storage
if not bot_lib_config.users.get(sender_id):
logger.error(f"User {sender_id} doesn't exist in the users list")
else:
return bot_lib_config.users[sender_id]["has_activity"]
else:
return True

def update_last_activity(self, sender_id: str) -> None:
if not hasattr(self, "last_activity") or self.last_activity is None:
self.last_activity = int(time.time())
bot_lib_config.users[sender_id]["has_activity"] = True
bot_lib_config.save_users()
else:
self.last_activity = int(time.time())

@property
def is_conversation_obsolete(self) -> bool:
return int(time.time()) - self.last_activity > bot_lib_config.conversation_obsolescence

def update_last_activity(self) -> None:
self.last_activity = int(time.time())
if self.last_activity:
return int(time.time()) - self.last_activity > bot_lib_config.conversation_obsolescence
return False


env_config = Config()
Expand Down
6 changes: 2 additions & 4 deletions app/matrix_bot/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)

from .client import MatrixClient
from .config import bot_lib_config, logger
from .config import logger
from .eventparser import (
EventNotConcerned,
EventParser,
Expand Down Expand Up @@ -82,7 +82,6 @@ async def wrapped_func(room, event):
)

ep.do_not_accept_own_message() # avoid infinite loop
await ep.only_allowed_sender() # only allowed senders
await func(ep=ep, matrix_client=self.matrix_client)

self.client_callback.append((wrapped_func, onEvent))
Expand All @@ -101,8 +100,7 @@ def register_on_startup(self, func):

async def setup_callbacks(self):
"""Add callbacks to async_client"""
if bot_lib_config.join_on_invite:
self.matrix_client.add_event_callback(self.invite_callback, InviteMemberEvent)
self.matrix_client.add_event_callback(self.invite_callback, InviteMemberEvent)

self.matrix_client.add_event_callback(self.decryption_failure, MegolmEvent)

Expand Down
22 changes: 19 additions & 3 deletions app/matrix_bot/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: 2023 Pôle d'Expertise de la Régulation Numérique <[email protected]>
#
# SPDX-License-Identifier: MIT
import json
import logging
from pathlib import Path

Expand All @@ -21,9 +22,6 @@ class BotLibConfig(BaseSettings):
description="The maximum time that the server should wait for new events before "
"it should return the request anyways, in milliseconds",
)
join_on_invite: bool = Field(
default=False, description="Do the bot automatically join when invited"
)
encryption_enabled: bool = Field(default=ENCRYPTION_ENABLED)
ignore_unverified_devices: bool = Field(default=True, description="True by default in Element")
store_path: Path = Field(
Expand All @@ -32,6 +30,10 @@ class BotLibConfig(BaseSettings):
session_path: Path = Field(
default="/data/session.txt", description="path of the file to store session identifier"
)
users_path: Path = Field(
default="/data/users.json", description="path of the file to store pending users"
)
users: dict = Field(default={}, description="pending users")
log_level: int = Field(default=logging.INFO, description="log level for the library")
salt: bytes = Field(
default=b"\xce,\xa1\xc6lY\x80\xe3X}\x91\xa60m\xa8N",
Expand All @@ -43,6 +45,20 @@ class BotLibConfig(BaseSettings):

model_config = SettingsConfigDict(env_file=Path(".matrix_bot_env"))

def __init__(self):
super().__init__()
if not self.users_path.exists():
with open(self.users_path, "w") as f:
json.dump({}, f)
self.users = {}
else:
with open(self.users_path, "r") as f:
self.users: dict = json.load(f)

def save_users(self):
with open(self.users_path, "w") as f:
json.dump(self.users, f, indent=2)


bot_lib_config = BotLibConfig()

Expand Down
18 changes: 13 additions & 5 deletions app/matrix_bot/eventparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from nio import Event, MatrixRoom, RoomMessageText

from .client import MatrixClient
from .config import logger
from .config import bot_lib_config, logger
from .room_utils import room_is_direct_message


Expand Down Expand Up @@ -40,7 +40,7 @@ def is_from_userid(self, userid: str) -> bool:
def is_from_this_bot(self) -> bool:
return self.is_from_userid(self.matrix_client.user_id)

def is_sender_allowed(self) -> bool:
def is_sender_domain_allowed(self) -> bool:
if "*" in env_config.user_allowed_domains:
return True
return self.sender_domain() in env_config.user_allowed_domains
Expand Down Expand Up @@ -94,14 +94,22 @@ def only_on_join(self) -> None:
if not self.event.source.get("content", {}).get("membership") == "invite":
raise EventNotConcerned

async def only_allowed_sender(self) -> None:
async def only_allowed_sender(self, user_config) -> None:
"""
:raise EventNotConcerned: if the sender is not allowed to send messages
"""
if not self.is_sender_allowed():
if not self.is_sender_domain_allowed():
await self.matrix_client.send_text_message(
self.room.room_id,
"Albert n'est pas encore disponible pour votre domaine. Merci de rester en contact, il sera disponible après un beta test.",
msgtype="m.notice",
)
raise EventNotConcerned
if not user_config.is_authorized(self.sender):
await self.matrix_client.send_markdown_message(
self.room.room_id,
"Albert n'est pas encore disponible pour votre domaine. Merci de rester en contact, il sera disponible après un beta test !",
"Albert est en phase de test et n'est pas encore disponible pour votre utilisateur. Contactez [email protected] pour demander un accès.",
msgtype="m.notice",
)
raise EventNotConcerned

Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ services:
- VERBOSE=true
- SYSTEMD_LOGGING=True
- SESSION_PATH=/data/session.txt
- JOIN_ON_INVITE=${JOIN_ON_INVITE}
- SALT=${SALT}
- MATRIX_HOME_SERVER=${MATRIX_HOME_SERVER}
- MATRIX_BOT_USERNAME=${MATRIX_BOT_USERNAME}
Expand Down

0 comments on commit fe4c815

Please sign in to comment.