mirror of
https://codeberg.org/D3M0N/thread-bot.git
synced 2025-04-11 20:58:47 +02:00
initial commit
This commit is contained in:
parent
1518e48553
commit
a183b6dc3d
8 changed files with 228 additions and 0 deletions
3
.dockerignore
Normal file
3
.dockerignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
__pycache__
|
||||
venv
|
||||
*.pyc
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -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
13
Dockerfile
Normal 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
16
config.py
Normal 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
13
docker-compose.yml
Normal 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
17
example.config.toml
Normal 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
141
main.py
Normal 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
23
requirements.txt
Normal 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
|
Loading…
Add table
Reference in a new issue