txlyre 7 months ago
parent
commit
2030097dee
11 changed files with 1077 additions and 818 deletions
  1. 1 0
      .gitignore
  2. 198 148
      actions.py
  3. 468 400
      commands.py
  4. 7 5
      config.py
  5. 6 0
      config.yml.sample
  6. 6 5
      db.py
  7. 66 0
      markov.py
  8. 47 24
      models.py
  9. 144 107
      openkriemy.py
  10. 1 0
      requirements.txt
  11. 133 129
      utils.py

+ 1 - 0
.gitignore

@@ -2,3 +2,4 @@ __pycache__/
 config.yml
 *.session
 *.bak
+venv

+ 198 - 148
actions.py

@@ -2,224 +2,274 @@ from random import randint
 
 from tortoise.contrib.postgres.functions import Random
 from telethon.utils import get_input_document
-from telethon.tl.functions.stickers import CreateStickerSetRequest, AddStickerToSetRequest
+from telethon.tl.functions.stickers import (
+    CreateStickerSetRequest,
+    AddStickerToSetRequest,
+)
 from telethon.tl.functions.messages import UploadMediaRequest, GetStickerSetRequest
-from telethon.tl.types import InputStickerSetID, InputStickerSetShortName, InputStickerSetItem, InputMediaUploadedDocument, InputPeerSelf
+from telethon.tl.types import (
+    InputStickerSetID,
+    InputStickerSetShortName,
+    InputStickerSetItem,
+    InputMediaUploadedDocument,
+    InputPeerSelf,
+)
 from tortoise.expressions import F
 
-from models import Action, Gif, StickerPack, Admin, BirthDay, VPNServer, AllowedChat
-from utils import (
-  is_valid_name,
-  is_valid_ip
+from models import (
+    Action,
+    Gif,
+    StickerPack,
+    Admin,
+    BirthDay,
+    VPNServer,
+    AllowedChat,
+    MarkovChat,
 )
+from utils import is_valid_name, is_valid_ip
 from config import config
 
+
 async def is_admin(bot, user):
-  admin = await bot.get_entity(config.ADMIN)
+    admin = await bot.get_entity(config.ADMIN)
 
-  if user.id == admin.id:
-    return True
+    if user.id == admin.id:
+        return True
 
-  admin = await Admin.filter(user_id=user.id)
-  if admin:
-    return True
+    admin = await Admin.filter(user_id=user.id)
+    if admin:
+        return True
+
+    return False
 
-  return False
 
 async def add_admin(user):
-  await Admin(user_id=user.id).save()
+    await Admin(user_id=user.id).save()
+
 
 async def delete_admin(user):
-  admin = await Admin.filter(user_id=user.id).first()
-  if not admin:
-    raise IndexError
+    admin = await Admin.filter(user_id=user.id).first()
+    if not admin:
+        raise IndexError
+
+    await admin.delete()
 
-  await admin.delete()
 
 async def create_action(name, template, kind):
-  if not is_valid_name(name):
-    raise SyntaxError
+    if not is_valid_name(name):
+        raise SyntaxError
+
+    await Action(name=name, template=template, kind=kind).save()
+
 
-  await Action(name=name, template=template, kind=kind).save()
-   
 async def find_action(name):
-  if not is_valid_name(name):
-    raise SyntaxError
+    if not is_valid_name(name):
+        raise SyntaxError
+
+    return await Action.filter(name=name).first()
 
-  return await Action.filter(name=name).first()
 
 async def delete_action(name):
-  action = await find_action(name)
-  if not action:
-    raise NameError
+    action = await find_action(name)
+    if not action:
+        raise NameError
 
-  gifs = await action.gifs.all()
-  for gif in gifs:
-    await gif.delete()
+    gifs = await action.gifs.all()
+    for gif in gifs:
+        await gif.delete()
+
+    await action.delete()
 
-  await action.delete()
 
 async def add_gif(action, file_id):
-  await Gif(action=action, file_id=file_id).save()
+    await Gif(action=action, file_id=file_id).save()
+
 
 async def get_random_gif(action):
-  return await action.gifs.all().annotate(order=Random()).order_by('order').first()
+    return await action.gifs.all().annotate(order=Random()).order_by("order").first()
+
 
 async def create_new_pack(bot, sticker):
-  last_pack = await StickerPack.all().order_by('-id').first()
-  set_id = last_pack.id + 1 if last_pack else 1
- 
-  user = await bot.get_entity(config.USER)
-
-  me = await bot.get_me()
-  bot_username = me.username
-  
-  pack = await bot(CreateStickerSetRequest(
-    user_id=user.id,
-    title=f'Messages #{set_id}.',
-    short_name=f'messages{set_id}_by_{bot_username}',
-    stickers=[sticker]
-  ))
-
-  sid = pack.set.id
-  hash = pack.set.access_hash
-
-  await StickerPack(
-    short_name=pack.set.short_name,
-    sid=sid,
-    hash=hash
-  ).save()
-
-  return pack
+    last_pack = await StickerPack.all().order_by("-id").first()
+    set_id = last_pack.id + 1 if last_pack else 1
+
+    user = await bot.get_entity(config.USER)
+
+    me = await bot.get_me()
+    bot_username = me.username
+
+    pack = await bot(
+        CreateStickerSetRequest(
+            user_id=user.id,
+            title=f"Messages #{set_id}.",
+            short_name=f"messages{set_id}_by_{bot_username}",
+            stickers=[sticker],
+        )
+    )
+
+    sid = pack.set.id
+    hash = pack.set.access_hash
+
+    await StickerPack(short_name=pack.set.short_name, sid=sid, hash=hash).save()
+
+    return pack
+
 
 async def get_current_pack(bot):
-  pack = await StickerPack.all().order_by('-id').first()
-  if not pack or pack.stickers_count >= 119:
-    return None
+    pack = await StickerPack.all().order_by("-id").first()
+    if not pack or pack.stickers_count >= 119:
+        return None
+
+    return pack
 
-  return pack
 
 async def add_sticker(bot, file, emoji):
-  file = await bot.upload_file(file)
-  file = InputMediaUploadedDocument(file, 'image/png', [])
-  file = await bot(UploadMediaRequest(InputPeerSelf(), file))
-  file = get_input_document(file)
-  sticker = InputStickerSetItem(document=file, emoji=emoji)
-  
-  pack = await get_current_pack(bot)
-  if not pack:
-    pack = await create_new_pack(bot, sticker)
-  else:
-    await StickerPack.filter(id=pack.id).update(stickers_count=F('stickers_count') + 1)
-
-    pack = await bot(AddStickerToSetRequest(
-      stickerset=InputStickerSetID(
-        id=pack.sid,
-        access_hash=pack.hash
-      ), 
-      sticker=sticker
-    ))    
-    
-  return get_input_document(pack.documents[-1])
+    file = await bot.upload_file(file)
+    file = InputMediaUploadedDocument(file, "image/png", [])
+    file = await bot(UploadMediaRequest(InputPeerSelf(), file))
+    file = get_input_document(file)
+    sticker = InputStickerSetItem(document=file, emoji=emoji)
+
+    pack = await get_current_pack(bot)
+    if not pack:
+        pack = await create_new_pack(bot, sticker)
+    else:
+        await StickerPack.filter(id=pack.id).update(
+            stickers_count=F("stickers_count") + 1
+        )
+
+        pack = await bot(
+            AddStickerToSetRequest(
+                stickerset=InputStickerSetID(id=pack.sid, access_hash=pack.hash),
+                sticker=sticker,
+            )
+        )
+
+    return get_input_document(pack.documents[-1])
+
 
 async def get_birthdays(peer_id):
-  return await BirthDay.filter(peer_id=peer_id).all()
+    return await BirthDay.filter(peer_id=peer_id).all()
+
 
 async def add_or_update_birthday(peer_id, user, date):
-  birthday = await BirthDay.filter(peer_id=peer_id, user_id=user.id).first()
+    birthday = await BirthDay.filter(peer_id=peer_id, user_id=user.id).first()
 
-  if birthday:
-    await BirthDay.filter(id=birthday.id).update(date=date)
- 
-    return False
+    if birthday:
+        await BirthDay.filter(id=birthday.id).update(date=date)
+
+        return False
 
-  await BirthDay(
-    peer_id=peer_id,
-    user_id=user.id,
-    date=date
-  ).save()
+    await BirthDay(peer_id=peer_id, user_id=user.id, date=date).save()
+
+    return True
 
-  return True
 
 async def get_all_birthdays():
-  return await BirthDay.all()
+    return await BirthDay.all()
+
 
 async def add_server(name, ip):
-  if not is_valid_ip(ip):
-    raise ValueError
+    if not is_valid_ip(ip):
+        raise ValueError
+
+    await VPNServer(name=name, ip=ip).save()
 
-  await VPNServer(
-    name=name,
-    ip=ip
-  ).save()
 
 async def add_server(name, ip):
-  if not is_valid_name(name):
-    raise SyntaxError
+    if not is_valid_name(name):
+        raise SyntaxError
+
+    if not is_valid_ip(ip):
+        raise ValueError
 
-  if not is_valid_ip(ip):
-    raise ValueError
+    await VPNServer(name=name, ip=ip).save()
 
-  await VPNServer(
-    name=name,
-    ip=ip
-  ).save()
 
 async def delete_server(name):
-  if not is_valid_name(name):
-    raise SyntaxError
+    if not is_valid_name(name):
+        raise SyntaxError
 
-  server = await VPNServer.filter(
-    name=name
-  ).first()
+    server = await VPNServer.filter(name=name).first()
 
-  if not server:
-    raise IndexError
+    if not server:
+        raise IndexError
+
+    await server.delete()
 
-  await server.delete()
 
 async def list_servers():
-  servers = await VPNServer.all()
+    servers = await VPNServer.all()
 
-  if not servers:
-    return '*пусто*'
+    if not servers:
+        return "*пусто*"
+
+    return ", ".join(map(lambda server: server.name, servers))
 
-  return ', '.join(
-    map(
-      lambda server: server.name,
-      servers
-    )
-  )
 
 async def get_server_ip(name):
-  if not is_valid_name(name):
-    raise SyntaxError
+    if not is_valid_name(name):
+        raise SyntaxError
 
-  server = await VPNServer.filter(
-    name=name
-  ).first()
+    server = await VPNServer.filter(name=name).first()
 
-  if not server:
-    raise IndexError
+    if not server:
+        raise IndexError
+
+    return server.ip
 
-  return server.ip
 
 async def add_allowed(peer_id):
-  await AllowedChat(
-    peer_id=peer_id
-  ).save()
+    await AllowedChat(peer_id=peer_id).save()
+
 
 async def delete_allowed(peer_id):
-  chat = await AllowedChat.filter(
-    peer_id=peer_id
-  ).first()
+    chat = await AllowedChat.filter(peer_id=peer_id).first()
 
-  if not chat:
-    raise IndexError
+    if not chat:
+        raise IndexError
+
+    await chat.delete()
 
-  await chat.delete()
 
 async def is_allowed(peer_id):
-  return await AllowedChat.filter(
-    peer_id=peer_id
-  ).exists()
+    return await AllowedChat.filter(peer_id=peer_id).exists()
+
+
+async def is_markov_enabled(peer_id):
+    return await MarkovChat.filter(peer_id=peer_id).exists()
+
+
+async def enable_markov(peer_id):
+    await MarkovChat(peer_id=peer_id).save()
+
+
+async def set_markov_options(peer_id, **options):
+    chat = await MarkovChat.filter(peer_id=peer_id).first()
+
+    if not chat:
+        raise IndexError
+
+    await MarkovChat.filter(id=chat.id).update(**options)
+
+
+async def get_markov_option(peer_id, option):
+    chat = await MarkovChat.filter(peer_id=peer_id).first()
+
+    if not chat:
+        raise IndexError
+
+    return getattr(chat, option)
+
+
+async def disable_markov(peer_id):
+    chat = await MarkovChat.filter(peer_id=peer_id).first()
+
+    if not chat:
+        raise IndexError
+
+    await chat.delete()
+
+
+async def list_markov_chats():
+    return await MarkovChat.all()

+ 468 - 400
commands.py

@@ -12,568 +12,636 @@ from telethon.errors import MessageEmptyError
 from telethon.utils import get_display_name, get_peer_id
 from telethon.tl.types import MessageEntityCode
 
-from aiofiles.os import (
-  remove,
-  path
-)
+from aiofiles.os import remove, path
 from aiohttp import ClientSession
 from emoji import is_emoji
 from cairosvg import svg2png
 
 from actions import (
-  find_action,
-  create_action,
-  delete_action,
-  add_gif,
-  add_sticker,
-  add_admin,
-  delete_admin,
-  add_or_update_birthday,
-  get_birthdays,
-  add_server,
-  delete_server,
-  add_allowed,
-  delete_allowed,
-  list_servers,
-  get_server_ip
+    find_action,
+    create_action,
+    delete_action,
+    add_gif,
+    add_sticker,
+    add_admin,
+    delete_admin,
+    add_or_update_birthday,
+    get_birthdays,
+    add_server,
+    delete_server,
+    add_allowed,
+    delete_allowed,
+    list_servers,
+    get_server_ip,
+    is_markov_enabled,
+    enable_markov,
+    disable_markov,
+    set_markov_options,
+    get_markov_option,
 )
 from utils import (
-  make_temporary_filename,
-  make_cache_filename,
-  parse_kind,
-  get_user_name,
-  calculate_age,
-  unparse,
-  remove_ansi_escapes
+    make_temporary_filename,
+    make_cache_filename,
+    parse_kind,
+    get_user_name,
+    calculate_age,
+    unparse,
+    remove_ansi_escapes,
 )
 
+
 class Handler:
-  def __init__(self, handler, is_restricted=False, is_public=False):
-    self.handler = handler
-    self.is_restricted = is_restricted
-    self.is_public = is_public
+    def __init__(self, handler, is_restricted=False, is_public=False):
+        self.handler = handler
+        self.is_restricted = is_restricted
+        self.is_public = is_public
+
 
 async def newadmin_handler(bot, event, command):
-  if command.argc < 1:
-    await event.reply('Пожалуйста, укажите пользователя!')
+    if command.argc < 1:
+        await event.reply("Пожалуйста, укажите пользователя!")
+
+        return
+
+    try:
+        target = await bot.get_entity(command.args[0])
+    except ValueError:
+        await event.reply("Недопустимое имя пользователя!")
 
-    return
+        return
 
-  try:
-    target = await bot.get_entity(command.args[0])
-  except ValueError:
-    await event.reply('Недопустимое имя пользователя!')
+    try:
+        await add_admin(target)
+    except IntegrityError:
+        await event.reply("Данный пользователь уже является администратором!")
 
-    return
+        return
 
-  try:
-    await add_admin(target)
-  except IntegrityError:
-    await event.reply('Данный пользователь уже является администратором!')
+    await event.reply("Готово!~~")
 
-    return
- 
-  await event.reply('Готово!~~')
 
 async def deladmin_handler(bot, event, command):
-  if command.argc < 1:
-    await event.reply('Пожалуйста, укажите пользователя!')
+    if command.argc < 1:
+        await event.reply("Пожалуйста, укажите пользователя!")
 
-    return
+        return
 
-  try:
-    target = await bot.get_entity(command.args[0])
-  except ValueError:
-    await event.reply('Недопустимое имя пользователя!')
+    try:
+        target = await bot.get_entity(command.args[0])
+    except ValueError:
+        await event.reply("Недопустимое имя пользователя!")
 
-    return
+        return
 
-  try:
-    await delete_admin(target)
-  except IndexError:
-    await event.reply('Данный пользователь не является администратором!')
+    try:
+        await delete_admin(target)
+    except IndexError:
+        await event.reply("Данный пользователь не является администратором!")
+
+        return
+
+    await event.reply("Готово!~~")
 
-    return
- 
-  await event.reply('Готово!~~')
 
 async def newaction_handler(bot, event, command):
-  if command.argc < 3:
-    await event.reply('Пожалуйста, укажите тип, имя и шаблон действия!')
+    if command.argc < 3:
+        await event.reply("Пожалуйста, укажите тип, имя и шаблон действия!")
 
-    return
+        return
 
-  try:
-    kind = parse_kind(command.args[0])
-  except ValueError:
-    await event.reply('Неверный тип действия!!!')
+    try:
+        kind = parse_kind(command.args[0])
+    except ValueError:
+        await event.reply("Неверный тип действия!!!")
 
-    return
+        return
 
-  try:
-    await create_action(command.args[1], ' '.join(command.args[2:]), kind)
-  except SyntaxError:
-    await event.reply('Недопустимое имя действия!!!')
+    try:
+        await create_action(command.args[1], " ".join(command.args[2:]), kind)
+    except SyntaxError:
+        await event.reply("Недопустимое имя действия!!!")
+
+        return
+    except IntegrityError:
+        await event.reply("Действие с таким названием уже существует!")
 
-    return
-  except IntegrityError:
-    await event.reply('Действие с таким названием уже существует!')
+        return
 
-    return
+    await event.reply("Действие создано!")
 
-  await event.reply('Действие создано!')
 
 async def delaction_handler(bot, event, command):
-  if command.argc < 1:
-    await event.reply('Пожалуйста, укажите имя действия!')
+    if command.argc < 1:
+        await event.reply("Пожалуйста, укажите имя действия!")
 
-    return
+        return
+
+    try:
+        await delete_action(command.args[0])
+    except SyntaxError:
+        await event.reply("Недопустимое имя действия!!!")
 
-  try:
-    await delete_action(command.args[0])
-  except SyntaxError:
-    await event.reply('Недопустимое имя действия!!!')
+        return
+    except NameError:
+        await event.reply("Действия с таким названием не существует!")
 
-    return
-  except NameError:
-    await event.reply('Действия с таким названием не существует!')
+        return
 
-    return
+    await event.reply("Действие удалено!")
 
-  await event.reply('Действие удалено!')
 
 async def addgif_handler(bot, event, command):
-  if command.argc < 1:
-    await event.reply('Пожалуйста, укажите имя действия!')
+    if command.argc < 1:
+        await event.reply("Пожалуйста, укажите имя действия!")
 
-    return
+        return
 
-  gif = await event.get_reply_message()
-  if not gif or not gif.gif:
-    await event.reply('Пожалуйста, добавьте GIF!')
+    gif = await event.get_reply_message()
+    if not gif or not gif.gif:
+        await event.reply("Пожалуйста, добавьте GIF!")
 
-    return
+        return
 
-  try:
-    action = await find_action(command.args[0])
+    try:
+        action = await find_action(command.args[0])
+
+        await add_gif(action, gif.file.id)
+    except SyntaxError:
+        await event.reply("Недопустимое имя действия!!!")
 
-    await add_gif(action, gif.file.id)
-  except SyntaxError:
-    await event.reply('Недопустимое имя действия!!!')
+        return
+    except NameError:
+        await event.reply("Нет такого действия!")
 
-    return
-  except NameError:
-    await event.reply('Нет такого действия!')
+        return
 
-    return
+    await event.reply("Готово!~~")
 
-  await event.reply('Готово!~~')
 
 async def addserver_handler(bot, event, command):
-  if command.argc < 2:
-    await event.reply('Пожалуйста, укажите имя и адрес сервера!')
+    if command.argc < 2:
+        await event.reply("Пожалуйста, укажите имя и адрес сервера!")
 
-    return
+        return
 
-  try:
-    await add_server(command.args[0], command.args[1])
-  except SyntaxError:
-    await event.reply('Недопустимое имя сервера!!')
+    try:
+        await add_server(command.args[0], command.args[1])
+    except SyntaxError:
+        await event.reply("Недопустимое имя сервера!!")
+
+        return
+    except ValueError:
+        await event.reply("Пожалуйста, введите корректный IPv4-/IPv6-адрес!")
 
-    return
-  except ValueError:
-    await event.reply('Пожалуйста, введите корректный IPv4-/IPv6-адрес!')
+        return
+    except IntegrityError:
+        await event.reply("Данный сервер уже был добавлен ранее!")
 
-    return
-  except IntegrityError:
-    await event.reply('Данный сервер уже был добавлен ранее!')
+        return
 
-    return
+    await event.reply("Готово!~~")
 
-  await event.reply('Готово!~~')
 
 async def delserver_handler(bot, event, command):
-  if command.argc < 1:
-    await event.reply('Пожалуйста, укажите имя сервера!')
+    if command.argc < 1:
+        await event.reply("Пожалуйста, укажите имя сервера!")
 
-    return
+        return
 
-  try:
-    await delete_server(command.args[0])
-  except SyntaxError:
-    await event.reply('Недопустимое имя сервера!!')
+    try:
+        await delete_server(command.args[0])
+    except SyntaxError:
+        await event.reply("Недопустимое имя сервера!!")
+
+        return
+    except IndexError:
+        await event.reply("Сервер с таким именем не найден!")
 
-    return
-  except IndexError:
-    await event.reply('Сервер с таким именем не найден!')
+        return
 
-    return
+    await event.reply("Готово!~~")
 
-  await event.reply('Готово!~~')
 
 async def allow_handler(bot, event, command):
-  try:
-    await add_allowed(get_peer_id(event.peer_id))
-  except IntegrityError:
-    await event.reply('Данный чат уже добавлен в список разрешённых!')
+    try:
+        await add_allowed(get_peer_id(event.peer_id))
+    except IntegrityError:
+        await event.reply("Данный чат уже добавлен в список разрешённых!")
 
-    return
+        return
 
-  await event.reply('Готово!~~')
+    await event.reply("Готово!~~")
 
-async def disallow_handler(bot, event, command):
-  try:
-    await delete_allowed(get_peer_id(event.peer_id))
-  except IndexError:
-    await event.reply('Данный чат не найден в списке разрешённых!!')
 
-    return
+async def disallow_handler(bot, event, command):
+    try:
+        await delete_allowed(get_peer_id(event.peer_id))
+    except IndexError:
+        await event.reply("Данный чат не найден в списке разрешённых!!")
+
+        return
+
+    await event.reply("Готово!~~")
+
+
+async def markov_handler(bot, event, command):
+    if command.argc < 1:
+        await event.reply("Некорректный синтаксис команды!!")
+
+        return
+
+    peer_id = get_peer_id(event.peer_id)
+
+    if command.args[0] == "enable":
+        try:
+            await enable_markov(peer_id)
+        except:
+            await event.reply("Ошибка!!!!!")
+
+            return
+
+        await event.reply("Готово!~~")
+    elif command.args[0] == "disable":
+        try:
+            await disable_markov(peer_id)
+        except:
+            await event.reply("Ошибка!!!!!")
+
+            return
+
+        await event.reply("Готово!~~")
+    elif command.args[0] == "is_enabled":
+        await event.reply(str(await is_markov_enabled(peer_id)))
+    elif command.args[0] == "set" and command.argc == 3:
+        try:
+            await set_markov_options(
+                peer_id, **{command.args[1]: float(command.args[2])}
+            )
+        except:
+            await event.reply("Ошибка!!!!!")
+
+            return
+
+        await event.reply("Готово!~~")
+    elif command.args[0] == "get" and command.argc == 2:
+        try:
+            await event.reply(str(await get_markov_option(peer_id, command.args[1])))
+        except:
+            await event.reply("Ошибка!!!!!")
+    elif command.args[0] == "say":
+        await bot.__markov_say(bot, peer_id)
+    elif command.args[0] == "reply":
+        await bot.__markov_say(bot, peer_id, reply_to=peer_id)
+    elif command.args[0] == "is_ready":
+        await event.reply(str(bot.__markov.is_ready))
+    elif command.args[0] == "corpus_size":
+        await event.reply(str(len(bot.__markov.corpus)))
+    else:
+        await event.reply("Некорректный синтаксис команды!!!")
 
-  await event.reply('Готово!~~')
 
 # Very, very, VERY evil code...
 async def make_message_shot(bot, message):
-  if message.sender is None:
-    sender_id = message.peer_id.channel_id
+    if message.sender is None:
+        sender_id = message.peer_id.channel_id
 
-    full_name = await bot.get_entity(sender_id)
-    full_name = full_name.title
-  else:
-    sender_id = message.sender.id
+        full_name = await bot.get_entity(sender_id)
+        full_name = full_name.title
+    else:
+        sender_id = message.sender.id
 
-    full_name = get_display_name(message.sender)
+        full_name = get_display_name(message.sender)
 
-  output_path = make_temporary_filename('png')
-  avatar_path = await make_cache_filename(sender_id, 'png')
+    output_path = make_temporary_filename("png")
+    avatar_path = await make_cache_filename(sender_id, "png")
 
-  if not await path.isfile(avatar_path):
-    await bot.download_profile_photo(sender_id, file=avatar_path, download_big=True)
+    if not await path.isfile(avatar_path):
+        await bot.download_profile_photo(sender_id, file=avatar_path, download_big=True)
 
-    # TO-DO: make it better.
-    mproc = await create_subprocess_shell(
-      f'mogrify -format png {avatar_path}'
-    )
-    await mproc.communicate()
+        # TO-DO: make it better.
+        mproc = await create_subprocess_shell(f"mogrify -format png {avatar_path}")
+        await mproc.communicate()
 
-  if not await path.isfile(avatar_path):
-    avatar_path = './resources/placeholder.png'
+    if not await path.isfile(avatar_path):
+        avatar_path = "./resources/placeholder.png"
 
-  data = bytes()
+    data = bytes()
 
-  data += pack('I', len(output_path))
-  data += bytes(output_path, encoding='ASCII')
-  data += pack('I', len(avatar_path))
-  data += bytes(avatar_path, encoding='ASCII')
+    data += pack("I", len(output_path))
+    data += bytes(output_path, encoding="ASCII")
+    data += pack("I", len(avatar_path))
+    data += bytes(avatar_path, encoding="ASCII")
 
-  username = bytes(full_name, encoding='UTF-8')
-  data += pack('I', len(username))
-  data += username
+    username = bytes(full_name, encoding="UTF-8")
+    data += pack("I", len(username))
+    data += username
 
-  data += pack('I', sender_id % 7)
+    data += pack("I", sender_id % 7)
 
-  text = bytes(unparse(message.raw_text, [entity for entity, _ in message.get_entities_text()]), encoding='UTF-8')
-  data += pack('I', len(text))
-  data += text
-
-  proc = await create_subprocess_shell(
-    './makeshot/makeshot',
-    stdin=PIPE
-  )
-  await proc.communicate(input=data)
+    text = bytes(
+        unparse(
+            message.raw_text, [entity for entity, _ in message.get_entities_text()]
+        ),
+        encoding="UTF-8",
+    )
+    data += pack("I", len(text))
+    data += text
 
-  pproc = await create_subprocess_shell(
-    f'pngcrush -reduce -ow {output_path}'
-  )
-  await pproc.communicate()
+    proc = await create_subprocess_shell("./makeshot/makeshot", stdin=PIPE)
+    await proc.communicate(input=data)
 
-  return output_path
+    pproc = await create_subprocess_shell(f"pngcrush -reduce -ow {output_path}")
+    await pproc.communicate()
 
-async def save_handler(bot, event, command):
-  message = await event.get_reply_message()
-  if not message:
-    await event.reply('Пожалуйста, укажите сообщение для сохранения!')
+    return output_path
 
-    return
 
-  emoji = '⚡'
-  if command.argc >= 1:
-    emoji = command.args[0]
+async def save_handler(bot, event, command):
+    message = await event.get_reply_message()
+    if not message:
+        await event.reply("Пожалуйста, укажите сообщение для сохранения!")
 
-    if len(emoji) not in range(1, 6)\
-    or not all(map(is_emoji, emoji)):
-      await event.reply('Указан некорректный эмодзи!!!')
+        return
 
-      return
+    emoji = "⚡"
+    if command.argc >= 1:
+        emoji = command.args[0]
 
-  path = await make_message_shot(bot, message)
+        if len(emoji) not in range(1, 6) or not all(map(is_emoji, emoji)):
+            await event.reply("Указан некорректный эмодзи!!!")
 
-  try:
-    file = await add_sticker(bot, path, emoji)
+            return
 
-    await bot.send_file(
-      message.peer_id,
-      file=file,
-      reply_to=message
-    )
-  finally:
-    await remove(path)
+    path = await make_message_shot(bot, message)
 
-async def bday_handler(bot, event, command):
-  if command.argc >= 1:
     try:
-      date = datetime.strptime(' '.join(command.args), '%d.%m.%Y')
-    except ValueError:
-      await event.reply('Дата не может быть распознана. Пожалуйста, введите свой день рождения в следующем формате: 01.01.1970 (день, месяц, год).')
+        file = await add_sticker(bot, path, emoji)
 
-      return
+        await bot.send_file(message.peer_id, file=file, reply_to=message)
+    finally:
+        await remove(path)
 
-    if date >= datetime.now():
-      await event.reply('День рождения не может быть в будущем...')
 
-      return
+async def bday_handler(bot, event, command):
+    if command.argc >= 1:
+        try:
+            date = datetime.strptime(" ".join(command.args), "%d.%m.%Y")
+        except ValueError:
+            await event.reply(
+                "Дата не может быть распознана. Пожалуйста, введите свой день рождения в следующем формате: 01.01.1970 (день, месяц, год)."
+            )
 
-    if await add_or_update_birthday(get_peer_id(event.peer_id), event.sender, date):
-      await event.reply('День рождения успешно добавлен!!!')
-    else:
-      await event.reply('День рождения успешно обновлён!!!')
+            return
 
-    return
+        if date >= datetime.now():
+            await event.reply("День рождения не может быть в будущем...")
 
-  birthdays = await get_birthdays(get_peer_id(event.peer_id))
-  if not birthdays:
-    await event.reply('Пока пусто...')
+            return
 
-    return
+        if await add_or_update_birthday(get_peer_id(event.peer_id), event.sender, date):
+            await event.reply("День рождения успешно добавлен!!!")
+        else:
+            await event.reply("День рождения успешно обновлён!!!")
 
-  birthdays = map(lambda birthday: (birthday.user_id, calculate_age(birthday.date)), birthdays)
-  birthdays = sorted(birthdays, key=lambda birthday: birthday[1].days_until)
+        return
 
-  birthdays_list = ''
-  for user_id, age in birthdays:
-    try:
-      entity = await bot.get_entity(user_id)
-    except ValueError:
-      await bot.get_participants(await event.get_chat())
+    birthdays = await get_birthdays(get_peer_id(event.peer_id))
+    if not birthdays:
+        await event.reply("Пока пусто...")
 
-      try:
-        entity = await bot.get_entity(user_id)
-      except ValueError:
-        continue
+        return
 
-    birthdays_list += f'{get_user_name(entity)} ❯ {age.age_now} ➔ {age.age} ❯ {age.date_string} ❯ {age.days_until}\n'
+    birthdays = map(
+        lambda birthday: (birthday.user_id, calculate_age(birthday.date)), birthdays
+    )
+    birthdays = sorted(birthdays, key=lambda birthday: birthday[1].days_until)
 
-  await event.reply(f'Дни рождения:\n\n{birthdays_list}')
+    birthdays_list = ""
+    for user_id, age in birthdays:
+        try:
+            entity = await bot.get_entity(user_id)
+        except ValueError:
+            await bot.get_participants(await event.get_chat())
 
-async def vpn_handler(bot, event, command):
-  if command.argc < 1:
-    await event.reply(f'Пожалуйста, укажите имя сервера! Доступные сервера: {await list_servers()}.')
+            try:
+                entity = await bot.get_entity(user_id)
+            except ValueError:
+                continue
 
-    return
+        birthdays_list += f"{get_user_name(entity)} ❯ {age.age_now} ➔ {age.age} ❯ {age.date_string} ❯ {age.days_until}\n"
 
-  try:
-    ip = await get_server_ip(command.args[0])
-  except SyntaxError:
-    await event.reply('Недопустимое имя сервера!!')
+    await event.reply(f"Дни рождения:\n\n{birthdays_list}")
 
-    return
-  except IndexError:
-    await event.reply('Сервер с таким именем не найден!')
 
-    return
+async def vpn_handler(bot, event, command):
+    if command.argc < 1:
+        await event.reply(
+            f"Пожалуйста, укажите имя сервера! Доступные сервера: {await list_servers()}."
+        )
 
-  if event.sender is None:
-    sender_id = event.peer_id.channel_id
-  else:
-    sender_id = event.sender.id
+        return
 
-  async with ClientSession() as session:
     try:
-      async with session.post(
-        f'http://{ip}:9217/api/obtain',
-        data={'user_id': sender_id}
-      ) as resp:
-        data = await resp.json()
+        ip = await get_server_ip(command.args[0])
+    except SyntaxError:
+        await event.reply("Недопустимое имя сервера!!")
 
-        profile = data['profile']
-    except:
-      await event.reply('Произошла ошибка при попытке обращения к API сервера… :(')
-
-      return
+        return
+    except IndexError:
+        await event.reply("Сервер с таким именем не найден!")
 
-  try:
-    await bot.send_message(
-      await bot.get_entity(sender_id),
-      f'Ваш файл конфигурации WireGuard (сервер {command.args[0]}):\n\n```{profile}```'
-    )
-  except:
-    await event.reply('Произошла ошибка при отправке файла конфигурации в Ваши личные сообщения… :с')
+        return
 
-    return
-
-  await event.reply('Готово!!~~ Файл конфигурации WireGuard отправлен в Ваши личные сообщения!')
+    if event.sender is None:
+        sender_id = event.peer_id.channel_id
+    else:
+        sender_id = event.sender.id
 
-async def run_handler(bot, event, command):
-  if command.argc < 1:
     async with ClientSession() as session:
-      try:
-        async with session.get(
-          'https://farlands.txlyre.website/langs'
-        ) as resp:
-          text = await resp.read()
-          text = text.decode('UTF-8')
-
-          await event.reply(f'Доступные языки:\n`{text}`')
-      except:
-        await event.reply('Произошла ошибка при попытке обращения к API… :(')
+        try:
+            async with session.post(
+                f"http://{ip}:9217/api/obtain", data={"user_id": sender_id}
+            ) as resp:
+                data = await resp.json()
 
-    return
+                profile = data["profile"]
+        except:
+            await event.reply(
+                "Произошла ошибка при попытке обращения к API сервера… :("
+            )
 
-  match = re.match(r'^(\w+)(?:\s|\n)((?:\n|.)*)$', command.args_string)
-  if not match:
-    await event.reply('Пожалуйста, не оставляйте ввод пустым!')
+            return
 
-    return
-
-  language_name, text = match.groups()
+    try:
+        await bot.send_message(
+            await bot.get_entity(sender_id),
+            f"Ваш файл конфигурации WireGuard (сервер {command.args[0]}):\n\n```{profile}```",
+        )
+    except:
+        await event.reply(
+            "Произошла ошибка при отправке файла конфигурации в Ваши личные сообщения… :с"
+        )
 
-  if text.startswith('```') and text.endswith('```'):
-    text = text[3:-3]
+        return
 
-  text = text.replace('\xA0', ' ') # i hate telegram
+    await event.reply(
+        "Готово!!~~ Файл конфигурации WireGuard отправлен в Ваши личные сообщения!"
+    )
 
-  async with ClientSession() as session:
-    try:
-      async with session.post(
-        f'https://farlands.txlyre.website/run/{language_name}',
-        data=text
-      ) as resp:
-        if resp.status in (404, 500):
-          info = await resp.json()
 
-          await event.reply(f'Произошла ошибка при попытке обращения к API… :(\nОтвет API: {info["detail"]}')
+async def run_handler(bot, event, command):
+    if command.argc < 1:
+        async with ClientSession() as session:
+            try:
+                async with session.get("https://farlands.txlyre.website/langs") as resp:
+                    text = await resp.read()
+                    text = text.decode("UTF-8")
 
-          return
-        elif resp.status != 200:
-          await event.reply('Сервер API временно недоступен. Пожалуйста, попробуйте ещё раз чуть позже.')
+                    await event.reply(f"Доступные языки:\n`{text}`")
+            except:
+                await event.reply("Произошла ошибка при попытке обращения к API… :(")
 
-          return
+        return
 
-        text = await resp.read()
-        text = text.decode('UTF-8')[:4096]
-    except:
-      await event.reply('Произошла ошибка при попытке обращения к API… :(')
+    match = re.match(r"^(\w+)(?:\s|\n)((?:\n|.)*)$", command.args_string)
+    if not match:
+        await event.reply("Пожалуйста, не оставляйте ввод пустым!")
 
-      return
+        return
 
-  text = remove_ansi_escapes(text).strip()
-  text = filter(lambda c: (c in ' \t\n' or ord(c) >= 32) and ord(c) not in range(128, 159), text)
-  text = ''.join(text)
-  text = text.replace('`', '')
+    language_name, text = match.groups()
 
-  if not text:
-    await event.reply('<пусто>')
+    if text.startswith("```") and text.endswith("```"):
+        text = text[3:-3]
 
-    return
+    text = text.replace("\xa0", " ")  # i hate telegram
 
-  try:
-    try:
-      await event.reply(f'```\n{text}```')
-    except ValueError:
-      await event.reply(text, parse_mode=None)
-  except MessageEmptyError:
-    await event.reply('<Telegram не смог декодировать текст сообщения>')
+    async with ClientSession() as session:
+        try:
+            async with session.post(
+                f"https://farlands.txlyre.website/run/{language_name}", data=text
+            ) as resp:
+                if resp.status in (404, 500):
+                    info = await resp.json()
+
+                    await event.reply(
+                        f'Произошла ошибка при попытке обращения к API… :(\nОтвет API: {info["detail"]}'
+                    )
+
+                    return
+                elif resp.status != 200:
+                    await event.reply(
+                        "Сервер API временно недоступен. Пожалуйста, попробуйте ещё раз чуть позже."
+                    )
+
+                    return
+
+                text = await resp.read()
+                text = text.decode("UTF-8")[:4096]
+        except:
+            await event.reply("Произошла ошибка при попытке обращения к API… :(")
+
+            return
+
+    text = remove_ansi_escapes(text).strip()
+    text = filter(
+        lambda c: (c in " \t\n" or ord(c) >= 32) and ord(c) not in range(128, 159), text
+    )
+    text = "".join(text)
+    text = text.replace("`", "")
 
-async def sylvy_handler(bot, event, command):
-  if command.argc < 1:
-    await event.reply('Пожалуйста, не оставляйте ввод пустым!')
+    if not text:
+        await event.reply("<пусто>")
 
-    return
+        return
 
-  async with ClientSession() as session:
     try:
-      async with session.post(
-        'https://sylvy-engine.txlyre.website/api/compute',
-        json={'program': command.args_string, 'stringify': True}
-      ) as resp:       
-        data = await resp.json()
+        try:
+            await event.reply(f"```\n{text}```")
+        except ValueError:
+            await event.reply(text, parse_mode=None)
+    except MessageEmptyError:
+        await event.reply("<Telegram не смог декодировать текст сообщения>")
 
-        if data['status'] != 'ok':
-          await event.reply(f'Ошибка API Sylvy: {data["data"]["message"]}')
 
-          return
+async def sylvy_handler(bot, event, command):
+    if command.argc < 1:
+        await event.reply("Пожалуйста, не оставляйте ввод пустым!")
 
-        result = data['data']['result']
-    except:
-      await event.reply('Произошла ошибка при попытке обращения к API Sylvy… :(')
+        return
 
-      return
+    async with ClientSession() as session:
+        try:
+            async with session.post(
+                "https://sylvy-engine.txlyre.website/api/compute",
+                json={"program": command.args_string, "stringify": True},
+            ) as resp:
+                data = await resp.json()
 
-  if result['status'] == 'TIMEOUT':
-     await event.reply(f'Максимальное время исполнения истекло (более тридцати секунд)!!!')
+                if data["status"] != "ok":
+                    await event.reply(f'Ошибка API Sylvy: {data["data"]["message"]}')
 
-     return
-  elif result['status'] != 'SUCCESS':
-     await event.reply(f'Ошибка исполнения!!!\n\n```{result["stdout"]}```')
+                    return
 
-     return
+                result = data["data"]["result"]
+        except:
+            await event.reply("Произошла ошибка при попытке обращения к API Sylvy… :(")
 
-  if result['plots']:
-    plots = []
+            return
 
-    for plot in result['plots']:
-      buffer = BytesIO()
+    if result["status"] == "TIMEOUT":
+        await event.reply(
+            f"Максимальное время исполнения истекло (более тридцати секунд)!!!"
+        )
 
-      svg2png(
-        bytestring=plot,
-        write_to=buffer
-      )
+        return
+    elif result["status"] != "SUCCESS":
+        await event.reply(f'Ошибка исполнения!!!\n\n```{result["stdout"]}```')
 
-      plots.append(buffer.getvalue())
+        return
 
-    await bot.send_file(
-      event.peer_id,
-      file=plots,
-      reply_to=event
-    )
+    if result["plots"]:
+        plots = []
 
-    return
+        for plot in result["plots"]:
+            buffer = BytesIO()
 
-  text = ''
+            svg2png(bytestring=plot, write_to=buffer)
 
-  if result['stdout']:
-    text += result['stdout']
-    text += '\n'
+            plots.append(buffer.getvalue())
 
-  text += result['output']
-  text = text.rstrip()
+        await bot.send_file(event.peer_id, file=plots, reply_to=event)
 
-  await event.reply(
-    text,
-    formatting_entities=[
-      MessageEntityCode(
-        offset=0,
-        length=len(text)
-      )
-    ]
-  )
+        return
 
-COMMANDS = {
-  'newadmin':  Handler(newadmin_handler,  is_restricted=True),
-  'deladmin':  Handler(deladmin_handler,  is_restricted=True),
-  'newaction': Handler(newaction_handler, is_restricted=True),
-  'delaction': Handler(delaction_handler, is_restricted=True),
-  'addgif':    Handler(addgif_handler,    is_restricted=True),
-  'addserver': Handler(addserver_handler, is_restricted=True),
-  'delserver': Handler(delserver_handler, is_restricted=True),
-  'allow':     Handler(allow_handler,     is_restricted=True),
-  'disallow':  Handler(disallow_handler, is_restricted=True),
+    text = ""
+
+    if result["stdout"]:
+        text += result["stdout"]
+        text += "\n"
 
-  'save':      Handler(save_handler),
+    text += result["output"]
+    text = text.rstrip()
 
-  'bday':      Handler(bday_handler),
+    await event.reply(
+        text, formatting_entities=[MessageEntityCode(offset=0, length=len(text))]
+    )
 
-  'vpn':       Handler(vpn_handler),
 
-  'sylvy':     Handler(sylvy_handler, is_public=True),
-  'run':       Handler(run_handler, is_public=True),
+COMMANDS = {
+    "newadmin": Handler(newadmin_handler, is_restricted=True),
+    "deladmin": Handler(deladmin_handler, is_restricted=True),
+    "newaction": Handler(newaction_handler, is_restricted=True),
+    "delaction": Handler(delaction_handler, is_restricted=True),
+    "addgif": Handler(addgif_handler, is_restricted=True),
+    "addserver": Handler(addserver_handler, is_restricted=True),
+    "delserver": Handler(delserver_handler, is_restricted=True),
+    "allow": Handler(allow_handler, is_restricted=True),
+    "disallow": Handler(disallow_handler, is_restricted=True),
+    "markov": Handler(markov_handler, is_restricted=True),
+    "save": Handler(save_handler),
+    "bday": Handler(bday_handler),
+    "vpn": Handler(vpn_handler),
+    "sylvy": Handler(sylvy_handler, is_public=True),
+    "run": Handler(run_handler, is_public=True),
 }

+ 7 - 5
config.py

@@ -1,10 +1,12 @@
 from yaml import load
 from yaml import Loader
 
+
 class Config:
-  def __init__(self, data):
-    self.__dict__.update(**data)
+    def __init__(self, data):
+        self.__dict__.update(**data)
+
 
-with open('config.yml') as f:
-  config = load(f, Loader=Loader)
-  config = Config(config)
+with open("config.yml") as f:
+    config = load(f, Loader=Loader)
+    config = Config(config)

+ 6 - 0
config.yml.sample

@@ -7,3 +7,9 @@ USER: '@txlyre'
 API_TOKEN: ''
 API_ID: 0
 API_HASH: ''
+
+MARKOV_REBUILD_RATE: 100
+MARKOV_CORPUS_SIZE: 10000
+MARKOV_STATE_SIZE: 3
+MARKOV_CHAIN_PATH: './markov_chain.json'
+MARKOV_CORPUS_PATH: './markov_corpus.json'

+ 6 - 5
db.py

@@ -4,9 +4,10 @@ from config import config
 
 import models
 
+
 async def init_db():
-  await Tortoise.init(
-    db_url=f'postgres://{config.DB_USER}:{config.DB_PASSWORD}@localhost:5432/{config.DB_NAME}',
-    modules={'models': ['models']}
-  )
-  await Tortoise.generate_schemas() 
+    await Tortoise.init(
+        db_url=f"postgres://{config.DB_USER}:{config.DB_PASSWORD}@localhost:5432/{config.DB_NAME}",
+        modules={"models": ["models"]},
+    )
+    await Tortoise.generate_schemas()

+ 66 - 0
markov.py

@@ -0,0 +1,66 @@
+import os.path
+import json
+import atexit
+
+import markovify
+
+from config import config
+
+
+class Markov:
+    def __init__(self):
+        self.corpus = []
+        self.chain = None
+
+        self.load()
+
+        atexit.register(self.save)
+
+    @property
+    def is_ready(self):
+        return self.chain is not None
+
+    def generate(self):
+        words = self.chain.walk()
+        if not words:
+            return self.generate()
+
+        return " ".join(words)
+
+    def rebuild(self):
+        self.chain = markovify.Chain(self.corpus, config.MARKOV_STATE_SIZE).compile()
+
+    def extend_corpus(self, text):
+        text = text.strip()
+        if not text:
+            return
+
+        text = text.replace("\n", " ")
+        text = text.split(" ")
+        text = map(lambda word: word.strip, text)
+        text = filter(bool, text)
+        text = list(text)
+
+        self.corpus.insert(0, text)
+
+        if len(self.corpus) > config.MARKOV_CORPUS_SIZE:
+            self.corpus = self.corpus[: config.MARKOV_CORPUS_SIZE]
+
+        if len(self.corpus) % config.MARKOV_REBUILD_RATE == 0:
+            self.rebuild()
+
+    def load(self):
+        if os.path.isfile(config.MARKOV_CHAIN_PATH):
+            with open(config.MARKOV_CHAIN_PATH, "r") as f:
+                self.chain = markovify.Chain.from_json(f.read())
+
+        if os.path.isfile(config.MARKOV_CORPUS_PATH):
+            with open(config.MARKOV_CORPUS_PATH, "r") as f:
+                self.corpus = json.load(f)
+
+    def save(self):
+        with open(config.MARKOV_CHAIN_PATH, "w") as f:
+            f.write(self.chain.to_json())
+
+        with open(config.MARKOV_CORPUS_PATH, "w") as f:
+            json.dump(self.corpus, f)

+ 47 - 24
models.py

@@ -1,40 +1,63 @@
 from tortoise.models import Model
-from tortoise.fields import IntField, BigIntField, CharField, TextField, BooleanField, DateField, ForeignKeyField
+from tortoise.fields import (
+    IntField,
+    BigIntField,
+    CharField,
+    TextField,
+    BooleanField,
+    DateField,
+    ForeignKeyField,
+    FloatField,
+)
+
 
 class Action(Model):
-  id = IntField(pk=True)
-  name = CharField(max_length=64, unique=True)
-  template = TextField()
-  kind = IntField()
+    id = IntField(pk=True)
+    name = CharField(max_length=64, unique=True)
+    template = TextField()
+    kind = IntField()
+
 
 class Gif(Model):
-  id = IntField(pk=True)
-  action = ForeignKeyField('models.Action', related_name='gifs')
-  file_id = CharField(max_length=64, unique=True)
+    id = IntField(pk=True)
+    action = ForeignKeyField("models.Action", related_name="gifs")
+    file_id = CharField(max_length=64, unique=True)
+
 
 class StickerPack(Model):
-  id = IntField(pk=True)
-  short_name = CharField(max_length=256, unique=True)
-  sid = BigIntField()
-  hash = BigIntField()
+    id = IntField(pk=True)
+    short_name = CharField(max_length=256, unique=True)
+    sid = BigIntField()
+    hash = BigIntField()
+
+    stickers_count = IntField(default=0)
 
-  stickers_count = IntField(default=0)
 
 class Admin(Model):
-  id = IntField(pk=True)
-  user_id = BigIntField(unique=True)
+    id = IntField(pk=True)
+    user_id = BigIntField(unique=True)
+
 
 class BirthDay(Model):
-  id = IntField(pk=True)
-  peer_id = BigIntField()
-  user_id = BigIntField()
-  date = DateField()
+    id = IntField(pk=True)
+    peer_id = BigIntField()
+    user_id = BigIntField()
+    date = DateField()
+
 
 class VPNServer(Model):
-  id = IntField(pk=True)
-  name = CharField(max_length=64, unique=True)
-  ip = CharField(max_length=15, unique=True)
+    id = IntField(pk=True)
+    name = CharField(max_length=64, unique=True)
+    ip = CharField(max_length=15, unique=True)
+
 
 class AllowedChat(Model):
-  id = IntField(pk=True)
-  peer_id = BigIntField(unique=True)
+    id = IntField(pk=True)
+    peer_id = BigIntField(unique=True)
+
+
+class MarkovChat(Model):
+    id = IntField(pk=True)
+    peer_id = BigIntField(unique=True)
+    opt_reply_prob = FloatField(default=1.0)
+    opt_message_prob = FloatField(default=0.8)

+ 144 - 107
openkriemy.py

@@ -1,3 +1,4 @@
+from random import random, uniform
 from asyncio import run, sleep
 from datetime import datetime, timedelta, date, time
 
@@ -6,152 +7,188 @@ from telethon.events import NewMessage
 from telethon.utils import resolve_bot_file_id, get_peer_id
 
 from actions import get_all_birthdays
-from utils import (
-  parse_command,
-  get_link_to_user,
-  calculate_age,
-  Kind
-)
+from utils import parse_command, get_link_to_user, calculate_age, Kind
 from config import config
 from db import init_db
 from actions import (
-  find_action,
-  get_random_gif,
-  is_admin,
-  is_allowed
+    find_action,
+    get_random_gif,
+    is_admin,
+    is_allowed,
+    is_markov_enabled,
+    get_markov_option,
+    list_markov_chats,
 )
 from commands import COMMANDS
+from markov import Markov
+
+bot = TelegramClient("openkriemy", config.API_ID, config.API_HASH).start(
+    bot_token=config.API_TOKEN
+)
+markov = Markov()
+
+
+async def markov_say(bot, peer_id, reply_to=None):
+    if not markov.is_ready:
+        return
+
+    text = markov.generate()
+
+    async with bot.action(peer_id, "typing"):
+        amount = 0
+        for _ in range(len(text)):
+            amount += round(uniform(0.05, 0.2), 2)
+
+        await sleep(min(amount, 8))
+
+    await bot.send_message(peer_id, message=text, reply_to=reply_to)
+
+
+# Wait isn't that illegal??
+bot.__markov = markov
+bot.__markov_say = markov_say
 
-bot = TelegramClient(
-  'openkriemy',
-  config.API_ID,
-  config.API_HASH
-).start(bot_token=config.API_TOKEN)
 
 @bot.on(NewMessage)
 async def on_message(event):
-  try:
-    command = parse_command(event.text)
-  except ValueError:
-    return
+    peer_id = get_peer_id(event.peer_id)
 
-  handler = COMMANDS.get(command.name, None)
+    try:
+        command = parse_command(event.text)
+    except ValueError:
+        if await is_markov_enabled(peer_id):
+            markov.extend_corpus(event.text)
 
-  if handler and handler.is_public:
-    await handler.handler(bot, event, command)
+            reply_prob = await get_markov_option(peer_id, "opt_reply_prob")
 
-    return
+            reply = await event.get_reply_message()
+            if (
+                reply and get_peer_id(reply.from_id) == await bot.get_peer_id("me")
+            ) or random() > reply_prob:
+                await markov_say(bot, peer_id, reply_to=event)
 
-  if not await is_allowed(get_peer_id(event.peer_id)):
-    if not handler or not handler.is_restricted:
-      return
+        return
 
-  if handler:
-    if handler.is_restricted\
-   and not await is_admin(bot, event.sender):
-      await event.reply('К сожалению, данная команда Вам недоступна.')
-    else:
-      await handler.handler(bot, event, command)
+    handler = COMMANDS.get(command.name, None)
 
-    return
+    if handler and handler.is_public:
+        await handler.handler(bot, event, command)
 
-  try:
-    action = await find_action(command.name)
-  except SyntaxError:
-    return
+        return
 
-  if not action:
-    return
+    if not await is_allowed(peer_id):
+        if not handler or not handler.is_restricted:
+            return
 
-  reply_to = None
-  target = None
+    if handler:
+        if handler.is_restricted and not await is_admin(bot, event.sender):
+            await event.reply("К сожалению, данная команда Вам недоступна.")
+        else:
+            await handler.handler(bot, event, command)
 
-  if action.kind != Kind.NO_TARGET:
-    target = await event.get_reply_message()
+        return
 
-    if not target:
-      try:
-        target = await bot.get_entity(command.args[0])
-      except (ValueError, IndexError):
-        if action.kind != Kind.NO_TARGET_MAYBE:
-          await event.reply('Это действие нужно применить на кого-то!')
+    try:
+        action = await find_action(command.name)
+    except SyntaxError:
+        return
 
-          return
-    else:
-      reply_to = target
-      target = target.sender
+    if not action:
+        return
+
+    reply_to = None
+    target = None
 
-    if target is None:
-      target = await bot.get_entity(event.peer_id.channel_id)
+    if action.kind != Kind.NO_TARGET:
+        target = await event.get_reply_message()
 
-    if action.kind == Kind.CANNOT_APPLY_TO_SELF\
-   and target.id == event.sender.id:
-      await event.reply('Данное действие нельзя применять к самому себе...')
+        if not target:
+            try:
+                target = await bot.get_entity(command.args[0])
+            except (ValueError, IndexError):
+                if action.kind != Kind.NO_TARGET_MAYBE:
+                    await event.reply("Это действие нужно применить на кого-то!")
 
-      return
+                    return
+        else:
+            reply_to = target
+            target = target.sender
 
-  try:
-    await event.delete()
-  except:
-    pass
+        if target is None:
+            target = await bot.get_entity(event.peer_id.channel_id)
 
-  if event.sender is None:
-    initiator = await bot.get_entity(event.peer_id.channel_id)
-    initiator = initiator.title
-  else:
-    initiator = get_link_to_user(event.sender)
+        if action.kind == Kind.CANNOT_APPLY_TO_SELF and target.id == event.sender.id:
+            await event.reply("Данное действие нельзя применять к самому себе...")
 
-  text = action.template.format(**{
-    'initiator': initiator,
-    'target': get_link_to_user(target) if target else ''
-  })
+            return
 
-  gif = await get_random_gif(action)
+    try:
+        await event.delete()
+    except:
+        pass
+
+    if event.sender is None:
+        initiator = await bot.get_entity(event.peer_id.channel_id)
+        initiator = initiator.title
+    else:
+        initiator = get_link_to_user(event.sender)
+
+    text = action.template.format(
+        **{"initiator": initiator, "target": get_link_to_user(target) if target else ""}
+    )
+
+    gif = await get_random_gif(action)
+
+    if gif:
+        gif = resolve_bot_file_id(gif.file_id)
 
-  if gif:
-    gif = resolve_bot_file_id(gif.file_id)
+    await bot.send_message(event.peer_id, message=text, file=gif, reply_to=reply_to)
 
-  await bot.send_message(
-    event.peer_id,
-    message=text,
-    file=gif,
-    reply_to=reply_to
-  )
 
 async def notify_birthdays():
-  birthdays = await get_all_birthdays()
+    birthdays = await get_all_birthdays()
 
-  for birthday in birthdays:
-    age = calculate_age(birthday.date)
+    for birthday in birthdays:
+        age = calculate_age(birthday.date)
 
-    if age.days_until < 1:
-      try:
-        try:
-          entity = await bot.get_entity(birthday.user_id)
-        except ValueError:
-          await bot.get_participants(birthday.peer_id)
+        if age.days_until < 1:
+            try:
+                try:
+                    entity = await bot.get_entity(birthday.user_id)
+                except ValueError:
+                    await bot.get_participants(birthday.peer_id)
 
-          entity = await bot.get_entity(birthday.user_id)
+                    entity = await bot.get_entity(birthday.user_id)
+
+                await bot.send_message(
+                    birthday.peer_id,
+                    f"{get_link_to_user(entity)}, поздравляю с днём рождения!!~~",
+                )
+            except:
+                pass
 
-        await bot.send_message(
-          birthday.peer_id,
-          f'{get_link_to_user(entity)}, поздравляю с днём рождения!!~~'
-        )
-      except:
-        pass
 
 async def notify_birthdays_loop():
-  interval = datetime.combine(date.today(), time(hour=0, minute=0))
+    interval = datetime.combine(date.today(), time(hour=0, minute=0))
 
-  while True:
-    await sleep(
-      ((interval - datetime.now()) % timedelta(days=1)).total_seconds()
-    )
+    while True:
+        await sleep(((interval - datetime.now()) % timedelta(days=1)).total_seconds())
+
+        await notify_birthdays()
+
+
+async def markov_say_loop():
+    while True:
+        await sleep(random(30, 60 * 5))
+
+        for chat in await list_markov_chats():
+            if random() > chat.opt_message_prob:
+                await markov_say(bot, chat.peer_id)
 
-    await notify_birthdays()
 
 with bot:
-  bot.loop.run_until_complete(init_db())
-  bot.loop.create_task(notify_birthdays_loop())
-  bot.start()
-  bot.run_until_disconnected()
+    bot.loop.run_until_complete(init_db())
+    bot.loop.create_task(notify_birthdays_loop())
+    bot.loop.create_task(markov_say_loop())
+    bot.start()
+    bot.run_until_disconnected()

+ 1 - 0
requirements.txt

@@ -5,3 +5,4 @@ aiohttp
 aiofiles
 telethon
 emoji
+markovify

+ 133 - 129
utils.py

@@ -11,194 +11,198 @@ from aiofiles.os import path
 
 from telethon.utils import get_display_name
 from telethon.tl.types import (
-  MessageEntityBold,
-  MessageEntityItalic,
-  MessageEntityStrike,
-  MessageEntityCode,
-  MessageEntityPre,
-  MessageEntitySpoiler,
-  MessageEntityUnderline,
-  MessageEntityUrl,
-  MessageEntityMention,
-  MessageEntityBotCommand
+    MessageEntityBold,
+    MessageEntityItalic,
+    MessageEntityStrike,
+    MessageEntityCode,
+    MessageEntityPre,
+    MessageEntitySpoiler,
+    MessageEntityUnderline,
+    MessageEntityUrl,
+    MessageEntityMention,
+    MessageEntityBotCommand,
 )
 
-Command = namedtuple(
-  'Command', 
-  'name argc args args_string'
-)
+Command = namedtuple("Command", "name argc args args_string")
 
-Age = namedtuple(
-  'Age',
-  'days_until age age_now date_string'
-)
+Age = namedtuple("Age", "days_until age age_now date_string")
+
+ANSI_ESCAPE_RE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
 
-ANSI_ESCAPE_RE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
 
 class Kind(IntEnum):
-  CANNOT_APPLY_TO_SELF = 0
-  CAN_APPLY_TO_SELF    = 1
-  NO_TARGET            = 2
-  NO_TARGET_MAYBE      = 3
+    CANNOT_APPLY_TO_SELF = 0
+    CAN_APPLY_TO_SELF = 1
+    NO_TARGET = 2
+    NO_TARGET_MAYBE = 3
+
 
 def parse_command(text):
-  text = text.strip()
+    text = text.strip()
+
+    if not text.startswith("/"):
+        raise ValueError
 
-  if not text.startswith('/'):
-    raise ValueError
+    text = text.split(" ")
 
-  text = text.split(' ')
+    if len(text) < 1:
+        raise ValueError
 
-  if len(text) < 1:
-    raise ValueError
+    command = text[0][1:].lower()
 
-  command = text[0][1:].lower()
+    if "@" in command:
+        command = command.split("@")
+        command = command[0]
 
-  if '@' in command:
-    command = command.split('@')
-    command = command[0]
+    args = text[1:]
+    argc = len(args)
+    args_string = " ".join(args)
 
-  args = text[1:]
-  argc = len(args)
-  args_string = ' '.join(args)
+    return Command(name=command, argc=argc, args=args, args_string=args_string)
 
-  return Command(
-    name=command, 
-    argc=argc, 
-    args=args, 
-    args_string=args_string
-  )
 
 def get_user_name(user):
-  full_name = get_display_name(user)
-  if not full_name:
-    full_name = user.username
-  
-  if not full_name:
-    full_name = '?'
+    full_name = get_display_name(user)
+    if not full_name:
+        full_name = user.username
+
+    if not full_name:
+        full_name = "?"
+
+    return full_name
 
-  return full_name
 
 def get_link_to_user(user):
-  full_name = get_user_name(user)
+    full_name = get_user_name(user)
+
+    if user.username:
+        return f"[{full_name}](@{user.username})"
 
-  if user.username:
-    return f'[{full_name}](@{user.username})'
+    return f"[{full_name}](tg://user?id={user.id})"
 
-  return f'[{full_name}](tg://user?id={user.id})'
 
 def is_valid_name(name):
-  return name.isidentifier()
+    return name.isidentifier()
+
 
 def make_temporary_filename(ext):
-  uid = uuid4().hex
+    uid = uuid4().hex
+
+    return f"tmp_{uid}.{ext}"
+
+
+CACHE_DIR = "./cache"
 
-  return f'tmp_{uid}.{ext}'
-  
-CACHE_DIR = './cache'
 
 async def make_cache_filename(id, ext):
-  if not await path.isdir(CACHE_DIR):
-    await mkdir(CACHE_DIR)
+    if not await path.isdir(CACHE_DIR):
+        await mkdir(CACHE_DIR)
+
+    return f"{CACHE_DIR}/{id}.{ext}"
 
-  return f'{CACHE_DIR}/{id}.{ext}'
 
 def parse_kind(kind):
-  kind = int(kind)
+    kind = int(kind)
 
-  if kind < 0 or kind > 3:
-    raise ValueError
+    if kind < 0 or kind > 3:
+        raise ValueError
+
+    return kind
 
-  return kind
 
 def calculate_age(date):
-  now = datetime.now().date()
-  birthday_date = date.replace(year=now.year)
+    now = datetime.now().date()
+    birthday_date = date.replace(year=now.year)
+
+    if now > birthday_date:
+        birthday_date = date.replace(year=now.year + 1)
 
-  if now > birthday_date:
-    birthday_date = date.replace(year=now.year + 1)
+    delta = birthday_date - now
+    age = (birthday_date - date).days // 365
+    age_now = age - 1
 
-  delta = birthday_date - now
-  age = (birthday_date - date).days // 365
-  age_now = age - 1
+    return Age(
+        days_until=delta.days,
+        age=age,
+        age_now=age_now,
+        date_string=date.strftime("%d.%m.%Y"),
+    )
 
-  return Age(
-    days_until=delta.days,
-    age=age,
-    age_now=age_now,
-    date_string=date.strftime('%d.%m.%Y')
-  )
 
 DELIMITERS = {
-  MessageEntityBold:       '**',
-  MessageEntityItalic:     '__',
-  MessageEntityStrike:     '~~',
-  MessageEntityCode:       '`',
-  MessageEntityPre:        '`',
-  MessageEntityUnderline:  '\ue000',
-  MessageEntitySpoiler:    '\ue001',
-  MessageEntityUrl:        '\ue002',
-  MessageEntityMention:    '\ue003',
-  MessageEntityBotCommand: '\ue003'
+    MessageEntityBold: "**",
+    MessageEntityItalic: "__",
+    MessageEntityStrike: "~~",
+    MessageEntityCode: "`",
+    MessageEntityPre: "`",
+    MessageEntityUnderline: "\ue000",
+    MessageEntitySpoiler: "\ue001",
+    MessageEntityUrl: "\ue002",
+    MessageEntityMention: "\ue003",
+    MessageEntityBotCommand: "\ue003",
 }
 
+
 class LookupTable:
-  def __init__(self):
-    self._start = {}
-    self._end = {}
+    def __init__(self):
+        self._start = {}
+        self._end = {}
+
+    def insert(self, entity):
+        delimiter = DELIMITERS.get(type(entity))
+        if not delimiter:
+            return
 
-  def insert(self, entity):
-    delimiter = DELIMITERS.get(type(entity))
-    if not delimiter:
-      return
+        start = entity.offset
+        end = entity.offset + entity.length
 
-    start = entity.offset
-    end = entity.offset + entity.length
+        if start in self._start:
+            self._start[start].append(delimiter)
+        else:
+            self._start[start] = [delimiter]
 
-    if start in self._start:
-      self._start[start].append(delimiter)
-    else:
-      self._start[start] = [delimiter]
+        if end in self._end:
+            self._end[end].insert(0, delimiter)
+        else:
+            self._end[end] = [delimiter]
 
-    if end in self._end:
-      self._end[end].insert(0, delimiter)
-    else:
-      self._end[end] = [delimiter]
+    def _lookup(self, position):
+        if position in self._start:
+            return "".join(self._start[position])
+        elif position in self._end:
+            return "".join(self._end[position])
 
-  def _lookup(self, position):
-    if position in self._start:
-      return ''.join(self._start[position])
-    elif position in self._end:
-      return ''.join(self._end[position]) 
+    def process(self, text):
+        text = list(text) + [""]
+        result = ""
 
-  def process(self, text):
-    text = list(text) + ['']
-    result = ''
+        for position, character in zip(range(len(text)), text):
+            delimiter = self._lookup(position)
+            if delimiter:
+                result += delimiter
 
-    for position, character in zip(range(len(text)), text):     
-      delimiter = self._lookup(position)
-      if delimiter:
-        result += delimiter
+            result += character
 
-      result += character
+        return result
 
-    return result
 
 def unparse(text, entities):
-  table = LookupTable()
+    table = LookupTable()
 
-  for entity in entities:
-    table.insert(entity)
+    for entity in entities:
+        table.insert(entity)
+
+    return table.process(text)
 
-  return table.process(text)
 
 def is_valid_ip(ip):
-  try:
-    ip_address(ip)
-  except:
-    return False
+    try:
+        ip_address(ip)
+    except:
+        return False
+
+    return True
 
-  return True
 
 def remove_ansi_escapes(text):
-  return ANSI_ESCAPE_RE.sub('', text)
+    return ANSI_ESCAPE_RE.sub("", text)