Всем привет!

15.02.2023. О том как я делал бота, который файлы с яндекс диска показывает, для лично-производственных целей.

Не маленькое предисловие (хотите – пропустите, оно не про бот)

Сначала думал писать. Потом взглянув с высоты полета - не писать. Ну а после всё-таки решил написать свой опыт ТГ-ботостроения. Парочку моментов, которые нигде не встречаются, может кому и пригодятся. Всё что будет далее — это так сказать проба пера. Хабр давно просматриваю и читаю, а иногда что-то и использую.

Я по образованию инженер-гидротехник - речные порты, причальные стенки, набережные, водозаборы, плотины и ГЭС — вот это всё я учился строить из арматуры, бетона, щебня и песка. А так в производстве начинал с ПГС (промышленно-гражданское строительство, а не песчано-гравийная смесь) в РБ, сейчас работаю в строительстве нефте-газового комплекса в РФ. Это для справки, что б не думали, что я программист). Давно уже работаю в производственно-техническом отделе – ПТО-шник я :-)

В 2022 начал глядеть что там с ЯП делается, а там вроде Python подает надежды, почитал поглядел всякие сравнения ну и выбрал его. Привлекала по отзывам работа с данными, нейросетями и генетическими алгоритмами, для оживления своей проги для рулетки - для чего ж еще. Начал Лутца читать, других книг накачал, что-то полистал. Стал смотреть, изучать инструментарий - версии питона, его юпитерноутбуки, анаконду, VSC, PyCharm. Вот кто бы еще расписал по полкам как этим PyCharm пользоваться, приёмы работы с ним какие-нибудь, нет же в инете (или не нашёл), копипастят друг у друга одно и тоже. GUI пробовал, штатные не впечатлили, как и QT, может не распробовал. Нашел Delphi4Python, полез смотреть что там в стане их делается, попробовал Александрию - понравилось, что есть порт в питон, даже как-то работает, изучаю по мере возможности. Нашел их бесплатный PyScripter, понравился, на нем и сижу пока. Нет у меня таких больших сложностей, его хватает.

Набрасываю по работе скрипты-помогаторы: то архивчик какой сделать, то переименовать что-нибудь в количестве 200-300файлов. И тут в 2023г узнаю про телеграмм и его ботов. По работе в основном вацап был для групповых чатов, потому и не пересекался. Фух, хватит, наверное, погнали про бота).

Задача

Имеется огромный массив нормативно-технической документации: ГОСТы, СП, ОР, РД, СТО, СНиП, СанПиН и пр. и пр. и пр. За время работы много чего такого накопляется у любого ПТО-шника. Много кто таскают для этого внешние диски.

Еще за время ведения объекта идет накопление/создание исполнительной и не только - различной документации: проект, переписка, АВК, АОСР, др. акты, договора, согласования, приказы, разрешительная, допускная. И вот подумалось – кладем это все в облако, а доступ дает бот по запросу, а документ может потребоваться много кому из ИТР, даже когда  находишься в поле или отпуске (а то беги к компу присылай инфу, найти как всегда никто ничего не может))) Вот к боту их и посылать всех, пусть он отдувается). Я ж ленивый, вот и приходится изучать программирование что б комп за себя заставлять работать)).

В целом в мире сейчас идет движуха по BIM технологиям в строительстве. Такой бот будет примерно где-то около такой темы. Планы на будущее: отправляешь боту акт входного контроля, а он оформляет сам журнал верификации…эх, мечты :-)

Идеи и реализация

Бот решил пока сделать для нормативки, обкатается, потом можно будет подумать и про производственные потребности. Файлы будут в облаке по папкам. Бот должен в ответ на запрос присылать нормативку. Файлы в основной своей массе будут pdf, как универсальный формат не зависящий от кодировок и пр.

Функциональность

  • По запросу бот выдает инфу по нормативке. В облако по папкам раскладываю НТД, бот перебирает папку и если находит, то даёт ответ. В начале он у меня скачивал файл из облака на диск, отсылал пользователю, потом удалял файл, дабы не захламлять диск. И тут узкое место – может быть прислан не нужный пользователю файл. Но если бот деплоить (размещать на VPS/VDS) то диска так может и не хватить, поэтому впоследствии переделал на выдачу ссылок на файл

  • Папку поиска можно переключать командой. Команды сделал как русскими так и английскими, как заглавными так и строчными. Например: /gost (или /гост или /ГОСТ) – переключит поиск на папку с ГОСТами; /vsn – на папку с ВСН и т.д.

  • Команды /start, /about и /help – в принципе все стандартно.

  • Есть команда /sms – для отправки сообщения разработчику, т.е. мне).

  • Есть команда /status – на неё бот даёт ответ работает ли он и какая папка поиска текущая.

  • Боту можно отправить файл с НТД. Когда он не находит нормативку – так и пишет, если мол пришлёте, то после проверки потом уже будет. Принимает он pdf, doc форматы и складывает в облако в отдельную папку. Проверять буду вручную, а то мало ли понаприсылают картинок со взрослых сайтов.

  • Ведет отчет, в который пишет id пользователя и что запрашивали, смс-ки или присылали файлы.

  • Отдельно сделаны сервисные команды, нужны для управления ботом. Сделаны буквенно-цифирным кодом что б никто не догадался. По такой команде бот может прислать отчет (типа /покажи отчет) либо выгрузить его в облако (типа /отчет в облако). Есть еще мысля сделать полную остановку бота, но пока такую команду убрал – бъёт ошибки.

Реализация:

Регистрируем бота в ТГ, получаем токен – это как везде.

В яндексе через ID получаем токен для своего облачного диска – поищите, тоже все расписано.

Открываю чей-то пример реализации эхобота и погнал.

Токены храню в отдельном файле cfg_token.py, в нем 2 строчки с токенами:

telebot_token ='тут абракадабра от ТГ'
## токен ЯД
ya_dsk_token = 'тут абракадабра от ЯД'

Сначала использовал модуль PyTelegramBotAPI (Telebot), потом переделал на Aiogram – когда много пользователей, лучше, что б была асинхронность, вроде так. Еще нужен модуль от яндекса.

pip install aiogram
pip install yadisk

Блок импорта, все стандартно:

from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
from aiogram.utils import executor

import cfg_token
import yadisk
import glob, os
import sys
import time
from datetime import datetime

import sqlite3 as sl

Возможно лишние есть, пока делаешь по 100500раз попробуешь, то одно, то другое.

Начальные установки:

bot = Bot(token=cfg_token.telebot_token)
dp = Dispatcher(bot)
codirovk = 'utf-8'
# токен яндекс диска
y = yadisk.YaDisk(token=cfg_token.ya_dsk_token)
# загружаемый файл должен содержать в своем имени
format_name_files =['ГОСТ', 'Гост', 'гост', 'GOST', 'Gost', 'gost', 'SP', 'sp',
'СП', 'сп', 'VSN', 'vsn', 'ВСН', 'всн', 'STO', 'sto', 'СТО', 'сто']
# загружаемый файл должен иметь расширение
format_ext_files = ['.pdf', '.doc', '.docx', '.rtf']
search_dir = 'GOST' #папка по умолчанию стартовая для поиска

Кодировку пришлось указывать явно для текстового файла отчета, поэтому наличествует переменная codirovk. А то в одном файле даже азиатская ероглифическая начала иногда прорываться.

##----записать данные в рапорт---------------
def report_to_txt(str15):
    try:
        with open('Report.txt', 'a', encoding=codirovk) as file4:
            file4.write(str15)
    except Exception as e:
        print('Ошибка: '+e)

Когда начал делать, то тренировался на папке ГОСТ. В облако там создавалась спец папка для загрузки файлов и отчетов, потом поправлю когда надо будет. Отработка команд как и в других примерах. Здесь их сократил, там у меня побольше написано, для растолкования

@dp.message_handler(commands=['start', 'старт'])  ## команда /start
async def process_start_command(message: types.Message):
    set_base_bot(message.from_user.id, 'GOST')
    await bot.send_message(message.from_user.id, "Прива! Я бот-помошник! Ищу НТД и выдаю их Вам")
    await bot.send_message(message.from_user.id, "Текущая папка для поиска НТД: "+get_base_bot(message.from_user.id)+'. Её можно переключить командой (см. /help)')
    await bot.send_message(message.from_user.id, "Введите запрос на НТД (можно только номер или часть наименования):")

@dp.message_handler(commands=['help', 'хелп']) ## команда /help
async def process_help_command(message: types.Message):
    # тут не выставляем папку поиска, берем ее из базы
    await bot.send_message(message.from_user.id, "Введите запрос на НТД (можно только номер или часть наименования) и отправьте мне, а я поищу где-то и если найду, то отправлю Вам файл, по 1шт за раз.")
    await bot.send_message(message.from_user.id, "В данный момент включен поиск в папке: "+get_base_bot(message.from_user.id))

Команда для переключения папки поиска, остальные сделаны аналогично:

@dp.message_handler(commands=['GOST', 'gost', 'ГОСТ', 'гост']) ## команда /GOST
async def process_gost_command(message: types.Message):
    set_base_bot(message.from_user.id, 'GOST')
    await bot.send_message(message.from_user.id, "Установлена текущая папка для поиска НТД: "+get_base_bot(message.from_user.id))

Команда для сообщения разработчику, использует ранее приведенную функцию report_to_txt:

@dp.message_handler(commands=['sms', 'смс']) ## команда /sms сообщение разработчику
async def process_sms_command(message: types.Message):
    report_to_txt('\nПользователь id'+str(message.from_user.id)+' отправил сообщение: '+message.text)
    await bot.send_message(message.from_user.id, "Сообщение разработчику отправлено")

Запрос от пользователя. То, что запросил пользователь, может быть не одно, показываю до 7 найденных документов и сколько их всего, а то введут запрос = ‘5’ и выдаст 400шт документов. Поэтому алгоритм настроен на выдачу 1-го точного результата.

Долго мучался с сокращенным вариантом ссылки на файл и где-то в уголке интернета периферическим зрением заметил промелькнувший вариант. Вот много кто про это спрашивает, а прямого ответа нет. ВОТ ОН:  y.publish(file) + y.get_meta(file).public_url (Кто ж знал что public_url надо сзади писать). Обрабатываю так:

@dp.message_handler(content_types=['text'])   ## получаем сообщение от юзера
async def get_text_messages(message: types.Message):
    search_dir = get_base_bot(message.from_user.id)
    report_to_txt('\nПользователь id'+str(message.from_user.id)+' сделал запрос на поиск в папке ' + search_dir +': '+message.text)
    await bot.send_message(message.from_user.id, 'Запускаю процесс поиска в папке ' + search_dir +' : '+message.text)
    if y.check_token():
        # ищем в папке документ содержащий запрос
        if not y.is_dir('/'+search_dir):
            await bot.send_message(message.from_user.id, 'Папка ' + search_dir +'  не обнаружена. Шо-то поломалось. Извините.')
        else:
            Spis = []
            for item in y.listdir(search_dir):
                if message.text in item['name']:
                    if len(Spis) < 7:
                        await bot.send_message(message.from_user.id, 'Обнаружен документ: '+item['name'])
                    Spis.append(item['name'])
            if len(Spis) == 0:
                await bot.send_message(message.from_user.id, 'Извините, пока такого документа не нашлось.')
                await bot.send_message(message.from_user.id, 'Но если Вы мне его сюда скинете, после проверки я его добавлю.')
            if len(Spis) == 1:
                    # ------------ВАР2 - даем ссылку на файл-------------------
                    y.publish('/'+search_dir+'/'+Spis[0])  # делаем публичный файл
                    # шлем ссылку
                    await bot.send_message(message.from_user.id, y.get_meta('/'+search_dir+'/'+Spis[0]).public_url)
                    # ------------ВАР1 - грузим файл в телегу через свой диск
                    # await bot.send_message(message.from_user.id, 'Загружаю. Ждите...')
                    # Скачивает на свой диск
                    # y.download('/'+search_dir+'/'+Spis[0], Spis[0])
                    # Отправляем в телегу
                    # f = open(Spis[0],"rb")
                    # await bot.send_document(message.from_user.id,f)
                    # f.close()
            if len(Spis) > 1:
                await bot.send_message(message.from_user.id, 'Найдено документов: '+str(len(Spis)) + '. Уточните запрос:')
    else:
        await bot.send_message(message.from_user.id, 'Извините по каким-то причинам диск не доступен. Попробуйте в другой раз')
        # ВАР1 -       когда скачиваем файл из облака для телеги на свой диск
        # remove_files() - тут функция удаления загруженного файла

Загруженный файл от пользователя обрабатываю так:

@dp.message_handler(content_types=['document']) # получаем файл от юзера
async def handle_file(message):
    try:
        pr1=0
        pr2=0
        # проверяем, содержит ли имя файла нужное название
        for item in format_name_files:
            if item in message.document.file_name:
                pr1=+1
        # проверяем, нужного ли формата файл
        for item in format_ext_files:
            if item in message.document.file_name:
                pr2=+1
        # если файл такой как надо, то качаем
        if (pr1 > 0) and (pr2 > 0):
            file_id = message.document.file_id
            file = await bot.get_file(file_id)
            file_path = file.file_path
            # ------------Вариант1 загрузки файлов на диск-------------------
            ## await bot.download_file(file_path, os.path.join('Download', message.document.file_name))
            # ------------Вариант загрузки файлов в яндекс-облако------------
            # путь к загружаемым в облако файлам от пользователей
            src = '/GOST/Download/'+ message.document.file_name
            print(src)
            # грузим в облако файл от пользователя
            if y.is_file(src): # если такой файл есть то яндекс даст ошибку, поэтому: вот
                src = '/GOST/Download/Double-'+datetime.now().strftime("%d.%m.%Y-%H.%M.%S")+'-'+ message.document.file_name
            y.upload(await bot.download_file(file_path), src)
            await bot.send_message(message.from_user.id, 'Загрузил. Спасибо. После проверки добавлю в свою базу:)')
            # сделать запись после загрузки в файл Report.txt
            report_to_txt('\nПользователь id='+str(message.from_user.id)+' прислал файл: '+message.document.file_name)
        else:
            await bot.send_message(message.from_user.id, 'Простите, но присланный Вами файл не содержит в имени тип НТД (ГОСТ, СП, ВСН и т.д.) и/или не подходит по формату, нужен .pdf или .doc')
    except Exception as e:
        print('Ошибка: '+e)
        await bot.send_message(message.from_user.id, 'Я наверное не смогу загрузить, шо-то сломалось и выдает ошибку: '+e)

Практически бот готов, поллинг добавляем:

if __name__ == '__main__':
    executor.start_polling(dp)

##  executor.start_polling(dp, skip_updates=True)
##Параметр skip_updates=True позволяет пропустить накопившиеся входящие сообщения, если они нам не важны

Но как Вы могли заметить по коду, кое-что там еще незнакомое мелькает:

get_base_bot(message.from_user.id)
set_base_bot(message.from_user.id, 'GOST')

А возникла следующая проблема: если Ваня выберет папку ГОСТ, и пока будет вбивать запрос, то Катя в это время переключит папку поиска на СП и Ваня получит результат по папке СП. Прикольно. «Шо делать, шо делать? — Шо‑то надо. Мда.»

В мыслях 2 варианта: через файл типа txt и пр., либо через базу данных. Выбрал через встроенный в питон sqlite3. В маленькой DB будут храниться id и выбранная папка. Для работы нужны 2 функции одна устанавливает, другая читает. На самом деле они почти одинаковые и есть мысли сделать одну. Когда наименование папки будет пустое (=’’) – то get, а если указано, то set. Может потом поправлю. Программу же можно улучшать бесконечно.

В итоге вот эти функции:

# запись данных о юзере - установка папки поиска для юзера, шоб друг другу не сбивали
def set_base_bot(user_id, name_dir):
    con = sl.connect('databasebot.db')
    with con:
        cur = con.cursor()
        cur.execute("CREATE TABLE IF NOT EXISTS user_seadir(id INTEGER NOT NULL PRIMARY KEY, seadir TEXT)")
        con.commit()
    with con:
        cur = con.cursor()
        cur.execute("SELECT seadir FROM user_seadir WHERE id = " + str(user_id))
        dat = cur.fetchone()
        if dat is None:
            cur.execute('INSERT INTO user_seadir (id, seadir) values(?, ?)', (user_id, name_dir))
            con.commit()
        else:
            cur.execute('UPDATE user_seadir SET seadir = ? WHERE id = ?', (name_dir, user_id))
            con.commit()
    con.close()

# получение данных о юзере - запрос папки поиска для юзера
def get_base_bot(user_id):
    con = sl.connect('databasebot.db')
    with con:
        cur = con.cursor()
        cur.execute("CREATE TABLE IF NOT EXISTS user_seadir(id INTEGER NOT NULL PRIMARY KEY, seadir TEXT)")
        cur.commit()
    with con:
        cur = con.cursor()
        cur.execute("SELECT seadir FROM user_seadir WHERE id = " + str(user_id))
        dat = cur.fetchone()
        if dat is not None:
            return dat[0]
        else:
            return 'GOST'
            cur.execute('INSERT INTO user_seadir (id, seadir) values(?, ?)', (user_id, 'GOST'))
            con.commit()
    con.close()

По этим функциям еще разбираться надо, где что как коммитить, не идет ли перезапись файла, в общем вопросов по нюансам много, а об них мало кто пишет, и информация в интернете разнится.  

Ну а сейчас Вам будет смешно. Get как надо работает, Set ошибку бьет (выше то уже поправленная):

def set_base_bot(user_id, name_dir):
…
      cur.execute('UPDATE user_seadir SET seadir = ? WHERE id = ?', (user_id, name_dir))

Попоискал я какого хутора не пашет, все оказалось просто – питон тоже не знает, что мне надо :-)

UPD. Пока делал статью, свёл 2 вышеприведенные функции в одну:

def sget_base_bot(user_id, name_dir):
    con = sl.connect('databasebot.db')
    cur = con.cursor()
    with con:
        cur.execute("CREATE TABLE IF NOT EXISTS user_seadir(id INTEGER NOT NULL PRIMARY KEY, seadir TEXT)")
    with con:
        cur.execute("SELECT seadir FROM user_seadir WHERE id = " + str(user_id))
        dat = cur.fetchone()
        if name_dir == '':                    # блок запроса установленной папки
            if dat is not None:
                return dat[0]
            else:
                return 'GOST'
                cur.execute('INSERT INTO user_seadir (id, seadir) values(?, ?)', (user_id, 'GOST'))
        else:                                 # блок установки папки поиска
            if dat is None:
                cur.execute('INSERT INTO user_seadir (id, seadir) values(?, ?)', (user_id, name_dir))
            else:
                cur.execute('UPDATE user_seadir SET seadir = ? WHERE id = ?', (name_dir, user_id))
            return name_dir
    cur.close()
    con.close()

Эта функция стала двойной – set и get в одном флаконе: если строка указана name_dir ,то работает как set – установка папки, если прислано ‘’ (пустая строка) – выдача инфы по установленной папке. Сами папки поиска указаны в ответе на команду /help и есть в меню. Со временем ассортимент будет расширяться.

end UPD

 При первом появлении в боте пользователя, ему папку включаю ГОСТ. А дальше он сам выберет, help и меню в помощь.

Сделал также и меню для бота. Тоже опишу, а то сумбур в инете. Сделал файл с командами txt, даже не файл – просто напишите текст для копипасты хоть в блокноте, хоть в ворде. Вот мой:

Далее идем как везде пишут, доходим до сообщения и далее по пунктам 1-нажать кнопку, 2- появится приглашение, 3-копипастим наш список команд и жмем на отсылку. Усё.

Возможно, моя беда была в многострочном сообщении. Но в конце концов менюшка получилась:

Структура

Структура бота в папке счас такая:

1.__pycache__ - папка

2.bot.pyw

3.cfg_token.py

4.databasebot.db

5.Report.txt

Папка п.1 создается самим Python. В ней он складывает байткодовый файл cfg_token.py. Её лучше никому не показывать, внутри можно токены вытянуть.

Файлы п.4 и п.5 делает программа сама себе – если удалить, вновь сделает).

Сам бот это п.2 - бот и п.3 - токены.

А так на яндекс диске:

Расширение бота сменил с .py на .pyw для не отображения окна консоли (работаю в Win).

Фух, вроде всё. А нет, про хостинг то забыли.

Немного о хостинге

Итак варианты хостинга:

1)   на своем компе, запустил, инет есть – бот работает. Пока так мой работает, т.е. пока я на работе.

2)   Ну в инете есть варианты с VPS/VDS – в основной своей массе платные. Делая бесплатного бота, я не готов к тратам. Вроде на сейчас PythonAnywhere остался более менее как вариант.

3)   С двумя подвариантами – смартфон, он же по сути комп, все время в сети, все время работает, чем не сервер)). Из минусов – возможно садит батарею, но что её только не садит:

  • Pydroid3 – работает), но иногда, а то и часто прерывается. У этой программы крутые модули/пакеты платные, но с аиограмм и яндексом повезло); 

  • UserLAnd с Ubunty CLI – работает еще круче, питон надо ставить (встроенный версии 2.7 вроде), пакеты тоже, еще mc (это что-то вроде нортона командера под dos) для забрать файлы бота из папки загрузок. Там в корне сделал папку bot, в неё и сложил файлы бота.

Команды которые использовал:

cd – переход в корень,

ls - просмотр каталога,

cd bot – переход в папку,

python3 bot.py – запускаю бота (расширение с pyw возвращаю на py)

Еще можно помудрить с сервисами, что б само запускалось и работало – не дошли руки. А еще мысля старый телефон под сервер для этого))

(Это мне выдало, когда на компе и смарте запустил бота, одновременная работа)

(выключаю утром бота на убунту, будет запущен на компе)

  • термукс работать отказался вообще в этом плане, может я не программист или не линуксоид, не победил я его.

Планы

  1. Асинхронность встроенной базы данных – насколько актуально пока не знаю. Может и забить можно). В pip-ах там вроде кто-то сварганил, но, если не ошибаюсь это было для постгрес.

  2. Делал сервисную команду остановки бота (типа /stop bot) через sys.exit() выдает кучу ошибок. Изучил много способов прерывания скрипта, пробовал – бьёт ошибки. Как понял, связано это с асинхронностью бота, возможно aiogram этому причина. Где-то выкладывали вариант бота со списком задач и постепенной остановке процессов бота и красивым, безошибочным остановом. Но, это пока не для меня, сложновато будет. Если не заморачиваться, то может и пойдет. Изучаю пока этот вопрос.

  3. Выпросить у яндекса под бот отдельный диск и там поселить, наделать папок, насыпать нормативки. Для этого только токен в боте поменять. Upd сделано.

  4. Не нравится мне поиск по папке, есть мысля завести отдельную таблицу в БД под ссылки на файлы + сервисную команду на индексацию надо будет придумать. Если файлов каких добавлю в облако, то и индексацию скомандую боту сделать. Либо вести такую таблицу по мере запросов:  вначале в БД поиск, если нет, то поиск на диске.

  5. Ну и шлифовать код, до идеала)

Заключение

Создан инструмент для доступа и наполнения базы НТД через мессенджер.

Вроде все

П.с.1. Конструктивная критика приветствуется категорически.

П.с.2. Эксперименты с ботом не закончил, возможны сбои в работе в процессе, а так велкам кому надо – Normativkabot.

П.с.3. Если вдруг ТГ накроется, думаю не сложно будет GUI набросать с подобным функционалом и запустить по сарафанному радио.

Комментарии (31)


  1. Bluesrocker22
    00.00.0000 00:00
    +1

    Отличный проект, я считаю, сугубо практический. И проблема решается насущная и машина на службе у человека и написание статьи не даст забыть почему выбраны те или иные решения.

    Как по мне только слегка многовато воды в статье, - если свернуть куски кода, то вся статья читается как практический документ-заметка "сделал так, записал чтобы не забыть".


  1. SergeyDeryabin
    00.00.0000 00:00
    +1

    Прикольно, автор безусловно молодец, есть несколько но.

    Имя бота написали, а вот backend похоже забыли запустить. Интересно будет посмотреть как оно будет, если хотя бы 1000 человек запустят бота

    А по последнему пункту

    П.с.3. Если вдруг ТГ накроется, думаю не сложно будет GUI набросать с подобным функционалом и запустить по сарафанному радио.

    расстрою вас, просто набросать GUI мало, да и не скажу что просто совсем. Проще будет только давать ссылку на документ на диске руками


    1. Kraleks Автор
      00.00.0000 00:00

      Спасибо за коммент. Про backend я так и писал - шо пока на работе пашет. Ну а сейчас, после публикации буду держать включенным). А про GUI поглядим, как посвободней на работе будет думаю занятся. Для виндовс не думаю что будет сложно - в голове уже крутится реализация, а для андроид пока ни разу не делал.


  1. IvanSCM
    00.00.0000 00:00
    +1

    Отдельно сделаны сервисные команды, нужны для управления ботом. Сделаны буквенно-цифирным кодом что б никто не догадался.

    Достаточно реализовать проверку на администратора бота по user_id.


    1. Kraleks Автор
      00.00.0000 00:00

      Тоже думал над этим - через отдельную таблицу БД с правами допуска.


      1. night_admin
        00.00.0000 00:00
        +1

        Зачем ещё БД? Права доступа могут в этой же таблице лежать.


        1. Kraleks Автор
          00.00.0000 00:00

          ну да, в принципе)


  1. AdmAlexus
    00.00.0000 00:00
    +2

    Если я правильно понял, то проблему с поиском по нужной папке (когда Вася выбрал одну папку, кто-то выбрал другую и в итоге Вася в обломе) можно без особых затрат решить без какой-либо дополнительной БД\таблицы в БД.
    В том же aiogram есть замечательная вещь - FSM (т.н. "машина состояний"). Она позволит для каждого пользователя в рамках сессии хранить параметры.
    Посмотрите, по ней документации много как и глобально, так и здесь, на хабре.


    1. Kraleks Автор
      00.00.0000 00:00

      Ого. Я про такую штуку и не знал. Спасибо, гляну.


  1. mokhin-denis
    00.00.0000 00:00
    +1

    Отличный проект! Супер! Готов поделиться VPS/VDS для дальнейшей проверки гипотез.


    1. Kraleks Автор
      00.00.0000 00:00

      надо обдумать как это делается)


  1. iig
    00.00.0000 00:00

    Не совсем понятно, зачем нужен бот. А если просто положить файлы на я-диск ?


    1. Kraleks Автор
      00.00.0000 00:00
      +1

      • Звонит руководитель проекта с полей или из леса: "Мы тут глянули проект, там гост 10705, срочно пришли мне его"

      • Ну или: с одной почты/диска выйди, на другой зайди, сам найди, ссылку скопируй...а тут ввел цифры, ентер нажал и результат)

        Сделан для удобства и быстрого доступа. ТГ, вацапы всякие все время работают на компе/телефоне.

        И базу все еще продолжаю наполнять, не все есть. благодаря этому проекту хоть порядок в залежах наведу)


      1. SergeyDeryabin
        00.00.0000 00:00

        Звонит руководитель проекта с полей или из леса: "Мы тут глянули проект, там гост 10705, срочно пришли мне его"

        Это нормально в файлах его хранить? Там не вносятся правки, уточнения, дополнения, изменения?


        1. Kraleks Автор
          00.00.0000 00:00

          Ну, файлы понятны всем среднестатистическим пользователям. А нормативка - да - постоянно что-то меняют/корректируют/отменяют/новое вводят, довольно динамическая система. Тут только отслеживать и заменять устаревшее. А поскольку НТД очень много, то и пришлось реализовывать функцию, когда пользователь пришлет нормативку с целью подновления базы.


          1. SergeyDeryabin
            00.00.0000 00:00

            Нет какой-то системы типа консультанта, где все правки и изменения уже учтены? Самому поддерживать в актуальном состоянии довольно проблематично


            1. Kraleks Автор
              00.00.0000 00:00

              Наверное есть. Но так и так нормативку таскать с объекта на объект, зато предоставить доступ к своим залежам теперь легко: послать к тг-боту)


            1. LeSick
              00.00.0000 00:00
              +1

              Вообще есть – мне в этом году пару раз предлагали по охране труда, например. Денег стоит столько же, сколько Консультант и подобное, но функционал урезанный… Бесплатно не встречал)))

              @Kraleks– отличный проект! ????????


      1. iig
        00.00.0000 00:00

        Звонит руководитель проекта с полей или из леса: "Мы тут глянули проект, там гост 10705, срочно пришли мне его"

        хттпс://корень.файлопомойки/госты/10705/

        с одной почты/диска выйди, на другой зайди

        Наверное, нужно либо настроить доступы, либо всем read only

        Или свой nextcloud развернуть, если хочется рулить файлопомойкой по своим правилам.


  1. AndreyYu
    00.00.0000 00:00

    А может быть просто расшарить ссылку на яндекс.диск и там поиском найти?


    1. Kraleks Автор
      00.00.0000 00:00

      может быть), но это не наш метод, это не спортивно)


  1. magiavr
    00.00.0000 00:00

    Может проще посмотреть в сторону calibe? На ней удобно делать библиотеки и поиск по тегам и доступ по opds.


    1. Kraleks Автор
      00.00.0000 00:00

      интересная система


  1. FlyGst
    00.00.0000 00:00

    "Планы на будущее: отправляешь боту акт входного контроля, а он оформляет сам журнал верификации…эх, мечты" - сделайте сайт с формами... заполняйте и генерируйте нужные формы. Ну или через Гугл документы, но тогда информация перестает быть конфиденциальной.


    1. Kraleks Автор
      00.00.0000 00:00

      Не, там не такая задумка. Формы все есть, они известны. И даже озвученное в принципе реализуемо, Но - всё разрушает человеческий фактор - одни шаблоны не рушат, а других хлебом не корми - дай модифицировать. Ну и еще есть исключения из правил, например когда текста много и надо еще добавлять ячейку эксель для переноса на следующую страницу (например сегодня название материала в ячейке Е5, а завтра надо в Е5 и в Е6 разнести) и красивой печати на бумагу. И вот когда в папке 100-150 таких актов, да таких папок 100шт, то невольно задумываешься об автоматизации)) А возможно я не с той стороны смотрю: сделать форму GUI, заставить всех работать через нее, из нее заносить в БД, из базы формировать акты на выдачу)


      1. iig
        00.00.0000 00:00

        Это выглядит как электронный документооборот.

        Но - всё разрушает человеческий фактор

        Это решается не на уровне питоновского кода и телеграм-ботов.

        одни шаблоны не рушат, а других хлебом не корми - дай модифицировать

        Наверное, каждый решает свои задачи.

        когда в папке 100-150 таких актов, да таких папок 100шт,

        В организации с такими оборотами еще в далекие 2000-е не напилили своих велосипедов на Delphi и VB?


  1. mironovep
    00.00.0000 00:00

    сделали подобный бот на базу литературы на яндекс диске, но проблема обнаружилась что при запуске поиска бот ищет по всем папкам на диске, а там еще и личного мусора полно.
    единственны


    1. Kraleks Автор
      00.00.0000 00:00

      search_dir = 'GOST'
      y.is_dir('/'+search_dir):
      	for item in y.listdir(search_dir):
      		...


  1. ProMix
    00.00.0000 00:00
    +1

    Раз уж вы стольких победили, не могли бы ещё и GitHub победить? Было бы интересно весь код посмотреть

    Только, пока он случайно не победил вас, предупрежу - не опубликуйте токены, посмотрите в сторону .gitignore


    1. Kraleks Автор
      00.00.0000 00:00

      Пока GitHub для меня сложноватая штука, код в принципе весь выложен, копипастой можно собрать, но раз весь надо, то тут и выложу. Под спойлером попытаюсь

      Весь код (истинные сервисные не покажу, сделал замену)))
      #  pip install aiogram
      #  pip install yadisk
      
      from aiogram import Bot, types
      from aiogram.dispatcher import Dispatcher
      from aiogram.utils import executor
      
      import cfg_token
      import yadisk
      import glob, os
      import sys
      import time
      from datetime import datetime
      
      import sqlite3 as sl
      
      bot = Bot(token=cfg_token.telebot_token)
      dp = Dispatcher(bot)
      
      codirovk = 'utf-8'
      
      # токен яндекс диска
      y = yadisk.YaDisk(token=cfg_token.ya_dsk_token)
      
      # загружаемый файл должен содержать в своем имени
      format_name_files =['ГОСТ', 'Гост', 'гост', 'GOST', 'Gost', 'gost',
      'SP', 'sp', 'СП', 'сп', 'VSN', 'vsn', 'ВСН', 'всн', 'STO', 'sto', 'СТО', 'сто',
      'RD', 'rd', 'РД', 'рд', 'Rd', 'Рд', 'Серия', 'СЕРИЯ', 'серия']
      
      # загружаемый файл должен иметь расширение
      format_ext_files = ['.pdf', '.doc', '.docx', '.rtf', '.djvu']
      search_dir = 'GOST' #папка по умолчанию стартовая для поиска
      
      def sget_base_bot(user_id, name_dir):
          con = sl.connect('databasebot.db')
          cur = con.cursor()
          with con:
              cur.execute("CREATE TABLE IF NOT EXISTS user_seadir(id INTEGER NOT NULL PRIMARY KEY, seadir TEXT)")
          with con:
              cur.execute("SELECT seadir FROM user_seadir WHERE id = " + str(user_id))
              dat = cur.fetchone()
              if name_dir == '':                    # блок запроса установленной папки
                  if dat is not None:
                      return dat[0]
                  else:
                      return 'GOST'
                      cur.execute('INSERT INTO user_seadir (id, seadir) values(?, ?)', (user_id, 'GOST'))
              else:                                 # блок установки папки поиска
                  if dat is None:
                      cur.execute('INSERT INTO user_seadir (id, seadir) values(?, ?)', (user_id, name_dir))
                  else:
                      cur.execute('UPDATE user_seadir SET seadir = ? WHERE id = ?', (name_dir, user_id))
                  return name_dir
          cur.close()
          con.close()
      
      #----------------записать данные в рапорт---------------
      def report_to_txt(str15):
          try:
              with open('Report.txt', 'a', encoding=codirovk) as file4:
                  file4.write(str15)
          except Exception as e:
              print('Ошибка: '+e)
      
      #--------------- bot command user begin ----------------------------------------
      
      @dp.message_handler(commands=['start', 'старт'])  ## команда /start
      async def process_start_command(message: types.Message):
          await bot.send_message(message.from_user.id, "Прива! Я бот-помошник! Ищу НТД и выдаю их Вам")
          await bot.send_message(message.from_user.id, "Работаю, правда, только в рабочее время в основном")
          await bot.send_message(message.from_user.id, "Имейте ввиду - я различаю большие и малые буквы. А НТД загружаю по 1шт за раз")
          await bot.send_message(message.from_user.id, "Также можно мне прислать файл НТД, которого у меня нет и я его добавлю")
          await bot.send_message(message.from_user.id, "Дополнительную информацию можно получить по команде /help")
          await bot.send_message(message.from_user.id, "Текущая папка для поиска НТД: "+sget_base_bot(message.from_user.id, 'GOST')+'. Её можно переключить командой (см. /help)')
          await bot.send_message(message.from_user.id, "Введите запрос на НТД (можно только номер или часть наименования):")
      
      @dp.message_handler(commands=['help', 'хелп']) ## команда /help
      async def process_help_command(message: types.Message):
          # тут не высттавляем папку поиска, берем ее из базы
          await bot.send_message(message.from_user.id, "Введите запрос на НТД (можно только номер или часть наименования) и отправьте мне, а я поищу где-то и если найду, то отправлю Вам файл, по 1шт за раз.")
          await bot.send_message(message.from_user.id, "Еще мне можно прислать то чего нет пока у меня, после проверки добавлю к себе и тогда оно будет:).")
          await bot.send_message(message.from_user.id, "Доступны команды: /start (или /старт) - инфа при старте бота")
          await bot.send_message(message.from_user.id, "/about (или /абут) - инфа о боте и разработчике")
          await bot.send_message(message.from_user.id, "/status (или /статус) - выдает ответ о работе. Если ответа нет - не в сети")
          await bot.send_message(message.from_user.id, "/sms MESSAGE (или /смс <текст сообщения>) — отправить любое сообщение MESSAGE разработчику бота.")
          await bot.send_message(message.from_user.id, "/gost , /sp , /vsn, /sto, /rd (или русскими буквами)- Установка папки для поиска по виду НТД.")
          await bot.send_message(message.from_user.id, "В данный момент включен поиск в папке: "+sget_base_bot(message.from_user.id, ''))
      
      @dp.message_handler(commands=['GOST', 'gost', 'ГОСТ', 'гост']) ## команда /GOST
      async def process_gost_command(message: types.Message):
          await bot.send_message(message.from_user.id, "Установлена текущая папка для поиска НТД: "+sget_base_bot(message.from_user.id, 'GOST'))
      
      @dp.message_handler(commands=['SP', 'sp', 'СП', 'сп']) ## команда /SP
      async def process_sp_command(message: types.Message):
          await bot.send_message(message.from_user.id, "Установлена текущая папка для поиска НТД: "+sget_base_bot(message.from_user.id, 'SP'))
      
      @dp.message_handler(commands=['RD', 'rd', 'РД', 'рд']) ## команда /SP
      async def process_rd_command(message: types.Message):
          await bot.send_message(message.from_user.id, "Установлена текущая папка для поиска НТД: "+sget_base_bot(message.from_user.id, 'RD'))
      
      @dp.message_handler(commands=['VSN', 'vsn', 'ВСН', 'всн']) ## команда /VSN
      async def process_vsn_command(message: types.Message):
          await bot.send_message(message.from_user.id, "Установлена текущая папка для поиска НТД: "+sget_base_bot(message.from_user.id, 'VSN'))
      
      @dp.message_handler(commands=['STO', 'sto', 'СТО', 'сто']) ## команда /STO
      async def process_sto_command(message: types.Message):
          await bot.send_message(message.from_user.id, "Установлена текущая папка для поиска НТД: "+sget_base_bot(message.from_user.id, 'STO'))
      
      @dp.message_handler(commands=['Serii', 'SERII', 'serii', 'Серии', 'СЕРИИ', 'серии', 'Серия', 'СЕРИЯ', 'серия']) ## команда /SERII
      async def process_serii_command(message: types.Message):
          await bot.send_message(message.from_user.id, "Установлена текущая папка для поиска НТД: "+sget_base_bot(message.from_user.id, 'Serii'))
      
      @dp.message_handler(commands=['about', 'абут', 'разработчик']) ## команда /about
      async def process_about_command(message: types.Message):
          await bot.send_message(message.from_user.id, "Разработчик revalpam@ya.ru. Бот создан для оперативного получения документации в лично-производственных целях.")
      
      @dp.message_handler(commands=['status', 'статус']) ## команда /status
      async def process_status_command(message: types.Message):
          await bot.send_message(message.from_user.id, "Работаю. Тут я. А шо такое? Вводите запрос и тисните Ентер:)")
          await bot.send_message(message.from_user.id, "Текущая папка для поиска НТД: "+sget_base_bot(message.from_user.id, ''))
      
      @dp.message_handler(commands=['sms', 'смс']) ## команда /sms сообщение разработчику
      async def process_sms_command(message: types.Message):
          report_to_txt('\nПользователь id'+str(message.from_user.id)+' отправил сообщение: '+message.text)
          await bot.send_message(message.from_user.id, "Сообщение разработчику отправлено")
      
      #--------------- bot command user end ------------------------------------------
      
      #--------------- service command admin begin -----------------------------------
      # выгружает файл отчета в облако и удаляет!!! его с диска
      @dp.message_handler(commands=['rep123456789']) ## команда /rep отчет
      async def process_rep_command(message: types.Message):
          try:
              # путь к загружаемым в облако файлам от пользователей
              src = '/ReportBot'
              if not y.is_dir(src):
                  y.mkdir(src)
              destin_file = src+ '/Report-'+datetime.now().strftime("%d.%m.%Y-%H.%M.%S")+'.txt'
              if y.is_file(destin_file):
                  y.remove(destin_file, permanently=True)
              # грузим в облако файлы
              if os.path.exists('Report.txt'):
                  y.upload('Report.txt', destin_file)
                  os.remove('Report.txt')
              await bot.send_message(message.from_user.id, "Отчёт обработан")
          except Exception as e:
              print('Ошибка: '+e)
      
      # показывает файл отчета с диска
      @dp.message_handler(commands=['view-rep123456789']) ## команда показать отчет
      async def process_view_report_command(message: types.Message):
          try:
              if os.path.exists('Report.txt'):
                  with open('Report.txt', encoding=codirovk) as f5:
                      ch_file = len(f5.read())
                  if  ch_file<2400:
                      with open('Report.txt', 'r', encoding=codirovk) as f5:
                          await bot.send_message(message.from_user.id, f5.read())
                  else:
                      await bot.send_message(message.from_user.id, "Отчёт слишком большой. Лучше смотреть в облаке после команды /rep")
              else:
                  await bot.send_message(message.from_user.id, "Отчёт не создавался пока")
          except Exception as e:
              print('Ошибка: '+e)
      
      ##@dp.message_handler(commands=['stop']) ## команда /stop  работает криво - много ошибок иде показывает
      ##async def process_stop_command(message: types.Message):
      ##    await bot.send_message(message.from_user.id, "Останавливаюсь.")
      ##    sys.exit()
      #--------------- service command admin end -------------------------------------
      
      @dp.message_handler(content_types=['text'])   ## получаем сообщение от юзера
      async def get_text_messages(message: types.Message):
          search_dir = sget_base_bot(message.from_user.id, '')
          report_to_txt('\nПользователь id'+str(message.from_user.id)+' сделал запрос на поиск в папке ' + search_dir +': '+message.text)
          await bot.send_message(message.from_user.id, 'Запускаю процесс поиска в папке ' + search_dir +' : '+message.text)
          if y.check_token():
              if not y.is_dir('/'+search_dir):
                  await bot.send_message(message.from_user.id, 'Папка ' + search_dir +'  не обнаружена. Шо-то поломалось. Извините.')
              else:
                  Spis = []
                  for item in y.listdir(search_dir):
                      if message.text in item['name']:
                          if len(Spis) < 7:
                              await bot.send_message(message.from_user.id, 'Обнаружен документ: '+item['name'])
                          Spis.append(item['name'])
                  if len(Spis) == 0:
                      await bot.send_message(message.from_user.id, 'Извините, пока такого документа в папке '+ search_dir +' не нашлось.')
                      await bot.send_message(message.from_user.id, 'Но если Вы мне его сюда скинете, после проверки я его добавлю.')
                  if len(Spis) == 1:
                          # ------------даем ссылку на файл-------------------
                          y.publish('/'+search_dir+'/'+Spis[0])  # делаем публичный файл
                          # шлем ссылку
                          await bot.send_message(message.from_user.id, y.get_meta('/'+search_dir+'/'+Spis[0]).public_url)
                  if len(Spis) > 1:
                      await bot.send_message(message.from_user.id, 'Найдено документов: '+str(len(Spis)) + '. Уточните запрос:')
          else:
              await bot.send_message(message.from_user.id, 'Извините по каким-то причинам диск не доступен. Попробуйте в другой раз')
      
      @dp.message_handler(content_types=['document']) # получаем файл от юзера
      async def handle_file(message):
          try:
              # если файл такой как надо, то качаем
              str_nam_file = str(message.document.file_name)
              len_str_nf = len(str_nam_file)
              if str_nam_file.endswith(tuple(format_name_files), 0, len_str_nf) and str_nam_file.endswith(tuple(format_ext_files), 0, len_str_nf):
                  file_id = message.document.file_id
                  file = await bot.get_file(file_id)
                  file_path = file.file_path
                  # ------------Вариант загрузки файлов в яндекс-облако------------
                  # путь к загружаемым в облако файлам от пользователей
                  if not y.is_dir('/DownloadBot'):
                      y.mkdir('/DownloadBot')
                  src = '/DownloadBot/'+ message.document.file_name
                  print(src)
                  # грузим в облако файл от пользователя
                  if y.is_file(src): # если такой файл есть то яндя даст ошибку, поэтому: вот
                      src = '/DownloadBot/Double-'+datetime.now().strftime("%d.%m.%Y-%H.%M.%S")+'-'+ message.document.file_name
                  y.upload(await bot.download_file(file_path), src)
                  await bot.send_message(message.from_user.id, 'Загрузил. Спасибо. После проверки добавлю в свою базу:)')
                  # сделать запись после загрузки в файл Report.txt
                  report_to_txt('\nПользователь id'+str(message.from_user.id)+' прислал файл: '+message.document.file_name)
              else:
                  await bot.send_message(message.from_user.id, 'Простите, но присланный Вами файл не содержит в имени тип НТД (ГОСТ, СП, ВСН и т.д.) и/или не подходит по формату, нужен .pdf или .doc')
          except Exception as e:
              print('Ошибка: '+e)
              await bot.send_message(message.from_user.id, 'Я наверное не смогу загрузить, шо-то сломалось и выдает ошибку: '+e)
      
      if __name__ == '__main__':
          executor.start_polling(dp)
      
      ##  executor.start_polling(dp, skip_updates=True)
      ##Параметр skip_updates=True позволяет пропустить накопившиеся входящие сообщения, если они нам не важны


  1. Kraleks Автор
    00.00.0000 00:00

    Дабы не закидывать в ЯД файлы вручную, набросал скрипт. Использует он тот же файл с токенами, что и бот cfg_token. Возле скрипта кладу файлы, внутри в нем отмечаю папку куда надо закинуть (правлю или как вариант заготовить на каждую отдельно) и запускаю его. После загрузки он удаляет обработанные файлы.

    код скрипта наполнения папки на ЯД
    import cfg_token
    import yadisk
    import glob, os, shutil
    import sys
    import time
    from datetime import datetime
    
    # токен яндекс диска
    y = yadisk.YaDisk(token=cfg_token.ya_dsk_token)
    
    # загружаемый файл должен иметь расширение
    format_move_files = ('.pdf', '.doc', '.docx', '.djvu', '.rar')#, '.rtf') '.zip', - зипы плохо обраб. ЯД
    up_dir = 'Serii' #папка для хранения
    
    ## 'GOST'  'SP'  'VSN' 'STO'
    
    def up_to_dir(file_name):
        try:
            # путь к загружаемым в облако файлам от пользователей
            src = '/'+up_dir
            dst = src+'/'+file_name
            if not y.is_dir(src): # если папки нет, то создать
                y.mkdir(src)
             # грузим в облако файл
            if os.path.exists(file_name):
                if y.is_file(dst): # если такой файл есть то яндя даст ошибку, поэтому: вот
                    dst = src+'/Double-'+datetime.now().strftime("%d.%m.%Y-%H.%M.%S")+'-'+ file_name
                y.upload(file_name, dst)
                print('Загружен файл: '+file_name+' в облако '+dst)
                os.remove(file_name)
                print('Удален файл: '+file_name)
        except Exception as e:
            print('Ошибка: ', e)
    
    k_file=0
    file_in_dir = os.listdir(os.getcwd())
    for file in file_in_dir:
        if file.endswith(format_move_files, 0, len(file)):  #'.pdf'  if file.endswith('.pdf'):tuple(
            print('Обрабатываю файл: ',file)
            up_to_dir(file)
            k_file=k_file+1
            print('-----------------------------------------------')
        else:
            continue
    print('Обработано файлов: '+str(k_file))       
    input("Работа завершена. Тисни ентер.")