initial commit

This commit is contained in:
D3M0N 2025-01-10 20:20:31 +05:00
parent 1518e48553
commit a183b6dc3d
8 changed files with 228 additions and 0 deletions

3
.dockerignore Normal file
View file

@ -0,0 +1,3 @@
__pycache__
venv
*.pyc

2
.gitignore vendored
View file

@ -162,3 +162,5 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# configs
config.toml

13
Dockerfile Normal file
View file

@ -0,0 +1,13 @@
FROM python:3.12-slim
WORKDIR /app
# Install dependencies with additional options for reliability
COPY requirements.txt .
RUN pip3 install -r requirements.txt
# Copy source code
COPY *.py .
COPY config.toml .
CMD ["python", "main.py"]

16
config.py Normal file
View file

@ -0,0 +1,16 @@
import tomllib
from dataclasses import dataclass
@dataclass
class Config:
homeserver: str
user_id: str
password: str
help_message: str
required_power_level: int
log_level: str
def load_config(path: str = "config.toml") -> Config:
with open(path, "rb") as f:
data = tomllib.load(f)
return Config(**data)

13
docker-compose.yml Normal file
View file

@ -0,0 +1,13 @@
version: '3.8'
services:
thread-bot:
build: .
restart: unless-stopped
volumes:
- ./config.toml:/app/config.toml:ro
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

17
example.config.toml Normal file
View file

@ -0,0 +1,17 @@
homeserver = "https://matrix.org"
user_id = "@user:example.org"
password = "your_password"
help_message = """
🤖 Thread Bot - An assistant to keep order in the chat room
Commands:
!thread_bot help - show this message
Functional:
- Automatically deletes non-trad posts from users with permission level below 50
- Requires permission level 50+ for bot to work
If you have any questions about the bot's operation, please contact the administrators.
"""
required_power_level = 50
log_level = "INFO"

141
main.py Normal file
View file

@ -0,0 +1,141 @@
import asyncio
import logging
from typing import Optional
from time import time
from nio import (
AsyncClient,
MatrixRoom,
RoomMessage,
InviteMemberEvent,
JoinError
)
from config import load_config, Config
class ThreadBot:
def __init__(self, config: Config):
self.config = config
self.client: Optional[AsyncClient] = None
self.start_time = time() # Сохраняем время запуска бота
self.setup_logging()
def setup_logging(self) -> None:
logging.basicConfig(
level=self.config.log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
self.logger = logging.getLogger("ThreadBot")
async def logout(self) -> None:
if self.client and self.client.logged_in:
try:
await self.client.logout()
self.logger.info("Successfully logged out")
except Exception as e:
self.logger.error(f"Error during logout: {e}")
finally:
await self.client.close()
async def handle_invite(self, room: MatrixRoom, event: InviteMemberEvent) -> None:
# Проверяем, что приглашение пришло после запуска бота
if event.server_timestamp / 1000 < self.start_time:
return
if event.state_key != self.client.user_id:
return
try:
self.logger.info(f"Received invite to room {room.room_id}")
await self.client.join(room.room_id)
await self.client.room_send(
room_id=room.room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"body": f"To work properly, I need moderator rights (power level 50+) to delete posts"
}
)
except JoinError as e:
self.logger.error(f"Error joining room {room.room_id}: {e}")
async def handle_message(self, room: MatrixRoom, event: RoomMessage) -> None:
# Проверяем, что сообщение пришло после запуска бота
if event.server_timestamp / 1000 < self.start_time:
return
if event.sender == self.client.user_id:
return
try:
if event.body == "!thread_bot help":
power_levels = await self.client.room_get_state_event(
room.room_id,
"m.room.power_levels"
)
user_level = power_levels.content.get("users", {}).get(event.sender, 0)
if user_level < self.config.required_power_level:
await self.client.room_send(
room_id=room.room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"body": self.config.help_message
}
)
return
is_threaded = bool(event.source.get('content', {}).get('m.relates_to', {}).get('rel_type') == 'm.thread')
power_levels = await self.client.room_get_state_event(
room.room_id,
"m.room.power_levels"
)
user_level = power_levels.content.get("users", {}).get(event.sender, 0)
if not is_threaded and user_level < self.config.required_power_level:
self.logger.info(f"Removing non-threaded message from {event.sender} in room {room.room_id}")
await self.client.room_redact(
room_id=room.room_id,
event_id=event.event_id,
reason="Messages must be in threads unless sent by admin"
)
except Exception as e:
self.logger.error(f"Error processing message in room {room.room_id}: {e}")
async def start(self) -> None:
self.client = AsyncClient(self.config.homeserver, self.config.user_id)
try:
await self.client.login(self.config.password)
self.logger.info("Successfully logged in")
self.client.add_event_callback(self.handle_message, RoomMessage)
self.client.add_event_callback(self.handle_invite, InviteMemberEvent)
await self.client.sync_forever()
except Exception as e:
self.logger.error(f"Error during bot execution: {e}")
finally:
await self.logout()
async def main() -> None:
config = load_config()
bot = ThreadBot(config)
try:
await bot.start()
except KeyboardInterrupt:
logging.info("Bot stopped by user")
except Exception as e:
logging.error(f"Unexpected error: {e}")
finally:
await bot.logout()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logging.info("Bot stopped by user")
except Exception as e:
logging.error(f"Unexpected error: {e}")

23
requirements.txt Normal file
View file

@ -0,0 +1,23 @@
aiofiles==24.1.0
aiohappyeyeballs==2.4.4
aiohttp==3.11.11
aiohttp_socks==0.10.1
aiosignal==1.3.2
attrs==24.3.0
frozenlist==1.5.0
h11==0.14.0
h2==4.1.0
hpack==4.0.0
hyperframe==6.0.1
idna==3.10
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
matrix-nio==0.25.2
multidict==6.1.0
propcache==0.2.1
pycryptodome==3.21.0
python-socks==2.6.1
referencing==0.35.1
rpds-py==0.22.3
unpaddedbase64==2.1.0
yarl==1.18.3