Compare commits
No commits in common. "master" and "gh-pages" have entirely different histories.
@ -1,27 +0,0 @@
|
||||
# Project
|
||||
.idea/
|
||||
.github/
|
||||
pyproject.toml
|
||||
.flake8
|
||||
.*ignore
|
||||
.isort.cfg
|
||||
Makefile
|
||||
readme.md
|
||||
|
||||
# Cache
|
||||
*.py[cod]
|
||||
__pycache__
|
||||
|
||||
# Texts
|
||||
*.mo
|
||||
|
||||
# Environment
|
||||
*.env
|
||||
env_file
|
||||
setenv.py
|
||||
|
||||
# Docker
|
||||
docker-compose*.yml
|
||||
Dockerfile
|
||||
|
||||
.venv
|
19
.github/workflows/dockerimage.yml
vendored
@ -1,19 +0,0 @@
|
||||
name: Build Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build and publish
|
||||
uses: elgohr/Publish-Docker-Github-Action@master
|
||||
with:
|
||||
name: Kylmakalle/tgvkbot:latest
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
snapshot: true
|
11
.gitignore
vendored
@ -1,11 +0,0 @@
|
||||
.idea/
|
||||
__pycache__/
|
||||
data/migrations/*
|
||||
!data/migrations/__init__.py
|
||||
config.py
|
||||
*.pem
|
||||
env_file
|
||||
openssl_config
|
||||
venv/
|
||||
commands/
|
||||
.venv
|
10
Dockerfile
@ -1,10 +0,0 @@
|
||||
FROM python:3.6-slim AS builder
|
||||
RUN apt-get update && apt-get install -y gcc
|
||||
COPY requirements.txt .
|
||||
RUN pip install --user -r requirements.txt
|
||||
|
||||
FROM python:3.6-slim
|
||||
COPY --from=builder /root/.local /root/.local
|
||||
COPY . .
|
||||
ENV PATH=/root/.local/bin:$PATH
|
||||
ENTRYPOINT bash -c "python manage.py makemigrations data && python manage.py migrate data && python telegram.py"
|
16
LICENSE
Normal file
@ -0,0 +1,16 @@
|
||||
Copyright (c) 2017 А. Сергей
|
||||
|
||||
Данная лицензия разрешает лицам, получившим копию данного программного обеспечения и сопутствующей документации
|
||||
(в дальнейшем именуемыми «Программное Обеспечение»), безвозмездно использовать Программное Обеспечение без ограничений,
|
||||
включая неограниченное право на использование, копирование, изменение, слияние, публикацию,
|
||||
а также лицам, которым предоставляется данное Программное Обеспечение, при соблюдении следующих условий:
|
||||
|
||||
Указанное выше уведомление об авторском праве и данные условия должны быть включены во все копии или значимые части
|
||||
данного Программного Обеспечения.
|
||||
|
||||
ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ,
|
||||
ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ,
|
||||
СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ, НО НЕ ОГРАНИЧИВАЯСЬ ИМИ.
|
||||
НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО КАКИМ-ЛИБО ИСКАМ,
|
||||
ЗА УЩЕРБ ИЛИ ПО ИНЫМ ТРЕБОВАНИЯМ, В ТОМ ЧИСЛЕ, ПРИ ДЕЙСТВИИ КОНТРАКТА, ДЕЛИКТЕ ИЛИ ИНОЙ СИТУАЦИИ,
|
||||
ВОЗНИКШИМ ИЗ-ЗА ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫХ ДЕЙСТВИЙ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
|
89
README.md
@ -1,80 +1,27 @@
|
||||
# tgvkbot
|
||||
Общайтесь в VK через Telegram бота.
|
||||
# Общие функции
|
||||
|
||||
- Бот от разработчика - [@tgvkbot](https://t.me/tgvkbot)
|
||||
- [Создание и развёртывание](installation/README.md)
|
||||
- [Использование](usage/README.md)
|
||||
- [Канал в Telegram](https://t.me/tg_vk) - обновления, поддержа
|
||||
|
||||
- Канал - [@tg_vk](https://t.me/tg_vk)
|
||||
Бот позволяет обмениваться сообщениями ВК используя Telegram, имеется возможность пересылки сообщений, а так же выбора диалога.
|
||||
<p align="center"><img src ="assets/rsz_sunset (1).jpg" /></p>
|
||||
|
||||
- Чат - https://t.me/joinchat/BZq6jwxeTh04qBzilM5x3g
|
||||
Поддерживаются различные вложения
|
||||
|
||||
<p align="center"><img src ="assets/documents.PNG" /></p>
|
||||
|
||||
|
||||
# Простая Установка (Ubuntu)
|
||||
```bash
|
||||
git clone https://github.com/Kylmakalle/tgvkbot
|
||||
cd tgvkbot
|
||||
./install.sh
|
||||
В том числе и аудиозаписи
|
||||
<p align="center"><img src ="assets/vk_music.jpg" /></p>
|
||||
|
||||
Токен Telegram бота: 123456789:AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLL
|
||||
VK APP ID (можно оставить пустым):
|
||||
```
|
||||
Список диалогов
|
||||
<p align="center"><img src ="assets/rsz_dialogs (1).jpg" /></p>
|
||||
|
||||
Далее потребуется ввести пароль от `sudo` пользователя и Telegram-token, остальные переменные необязательны.
|
||||
И поиск по диалогам
|
||||
|
||||
_Установщик поставит Docker и docker-compose, настроит переменные окружения и запустит контейнер для обновлений, а затем поднимет бота с его базой данных._
|
||||
<p align="center"><img src ="assets/rsz_search (1).jpg" /></p>
|
||||
|
||||
### Обновление
|
||||
Бот автоматически обновляется через образ на [dockerhub](https://hub.docker.com/r/kylmakalle/tgvkbot/tags?page=1&ordering=last_updated), где на всякий случай фиксируются версии каждого коммита.
|
||||
|
||||
Стандартный установщик поднимает [watchtower](https://containrrr.dev/watchtower), который раз в час проверяет обновления.
|
||||
|
||||
|
||||
### Ограничение пользователей
|
||||
Если по каким-то причинам хочется чтобы ботом пользовались исключительно определенные пользователи, то это можно сделать изменив файл конфигурации.
|
||||
Потребуется прописать параметр в таком виде, где числа - Telegram ID пользователей через запятую.
|
||||
|
||||
`ALLOWED_USER_IDS=12345678,001238091`
|
||||
|
||||
ID можно узнать командой `/id` в боте или через других ботов/софт.
|
||||
|
||||
|
||||
### Кастомизация
|
||||
С недавнего времени бот поднимается с помощью готового docker образа. Если нужно сделать какую-то специфичную правку, то бота можно поднять через отдельный файл командой
|
||||
|
||||
`docker-compose -f docker-compose.local.yml up`
|
||||
|
||||
Все остальные действия (`restart`, `stop`, ...) привязанные к "локальному боту" нужно выполнять с флагом `-f docker-compose.local.yml`
|
||||
|
||||
Не забудьте отключить бота из образа командой `docker-compose down`
|
||||
|
||||
# Установка в Dokku
|
||||
|
||||
Подробнее о деплое через Dokku можно прочитать [здесь](https://dokku.com/docs/deployment/application-deployment/).
|
||||
|
||||
На сервере:
|
||||
|
||||
```bash
|
||||
dokku apps:create tgvkbot
|
||||
dokku postgres:create tgvkbot_db
|
||||
dokku postgres:link tgvkbot_db tgvkbot
|
||||
|
||||
dokku config:set tgvkbot BOT_TOKEN=<tg_token> [VK_APP_ID=<vk_app_id> ALLOWED_USER_IDS=<tg_user_ids,...> MAX_FILE_SIZE=<num> ...]
|
||||
```
|
||||
|
||||
На локальном компьютере/где угодно в папке с репозиторием:
|
||||
|
||||
```bash
|
||||
git remote add dokku dokku@<dokku_host_url>:tgvkbot
|
||||
git push dokku
|
||||
```
|
||||
|
||||
|
||||
# Сервисы музыки (Устаревшие)
|
||||
Ниже прокси для музыки, которые использовали ранее. Сейчас они нерелевантны, но код открыт и в боте есть поддержка кастомных бэкендов музыки.
|
||||
|
||||
API - https://github.com/Kylmakalle/thatmusic-api
|
||||
|
||||
Token Refresher - https://github.com/Kylmakalle/vk-audio-token/tree/refresh-api
|
||||
|
||||
|
||||
# Лицензия
|
||||
MIT
|
||||
- [Создание и развёртывание](installation/README.md)
|
||||
- [Использование](usage/README.md)
|
||||
- [Канал в Telegram](https://t.me/tg_vk) - обновления, поддержа
|
||||
|
4
_config.yml
Normal file
@ -0,0 +1,4 @@
|
||||
theme: jekyll-theme-slate
|
||||
title: tgvkbot
|
||||
description: 'Общайтесь ВКонтакте с помощью Telegram бота'
|
||||
google_analytics: UA-101930895-1
|
10
_includes/google-analytics.html
Normal file
@ -0,0 +1,10 @@
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-101930895-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
</script>
|
425
_sass/jekyll-theme-slate.scss
Normal file
@ -0,0 +1,425 @@
|
||||
@import "rouge-github";
|
||||
|
||||
/*******************************************************************************
|
||||
MeyerWeb Reset
|
||||
*******************************************************************************/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
Theme Styles
|
||||
*******************************************************************************/
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
color:#373737;
|
||||
background: #212121;
|
||||
font-size: 20px;
|
||||
font-family: 'Myriad Pro', Calibri, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 10px 0;
|
||||
font-weight: 700;
|
||||
color:#222222;
|
||||
font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-bottom: 10px;
|
||||
font-size: 32px;
|
||||
background: url('../images/bg_hr.png') repeat-x bottom;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0 15px 0;
|
||||
}
|
||||
|
||||
footer p {
|
||||
color: #f2f2f2;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #0F79D0;
|
||||
text-shadow: none;
|
||||
|
||||
transition: color 0.5s ease;
|
||||
transition: text-shadow 0.5s ease;
|
||||
-webkit-transition: color 0.5s ease;
|
||||
-webkit-transition: text-shadow 0.5s ease;
|
||||
-moz-transition: color 0.5s ease;
|
||||
-moz-transition: text-shadow 0.5s ease;
|
||||
-o-transition: color 0.5s ease;
|
||||
-o-transition: text-shadow 0.5s ease;
|
||||
-ms-transition: color 0.5s ease;
|
||||
-ms-transition: text-shadow 0.5s ease;
|
||||
}
|
||||
|
||||
a:hover, a:focus {
|
||||
text-decoration: underline;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #F2F2F2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
max-width: 739px;
|
||||
padding: 5px;
|
||||
margin: 10px 0 10px 0;
|
||||
border: 1px solid #ebebeb;
|
||||
|
||||
box-shadow: 0 0 5px #ebebeb;
|
||||
-webkit-box-shadow: 0 0 5px #ebebeb;
|
||||
-moz-box-shadow: 0 0 5px #ebebeb;
|
||||
-o-box-shadow: 0 0 5px #ebebeb;
|
||||
-ms-box-shadow: 0 0 5px #ebebeb;
|
||||
}
|
||||
|
||||
p img {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
color: #222;
|
||||
background-color: #fff;
|
||||
|
||||
font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace;
|
||||
font-size: 14px;
|
||||
|
||||
border-radius: 2px;
|
||||
-moz-border-radius: 2px;
|
||||
-webkit-border-radius: 2px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,.1);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 3px;
|
||||
margin: 0 3px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
pre code {
|
||||
display: block;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 0 0 20px;
|
||||
border-left: 3px solid #bbb;
|
||||
}
|
||||
|
||||
|
||||
ul, ol, dl {
|
||||
margin-bottom: 15px
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
list-style: disc;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-position: inside;
|
||||
list-style: decimal;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
dl dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
dl dd {
|
||||
padding-left: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
dl p {
|
||||
padding-left: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 1px;
|
||||
margin-bottom: 5px;
|
||||
border: none;
|
||||
background: url('../images/bg_hr.png') repeat-x center;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid #373737;
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
font-family: 'Lucida Grande', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
padding: 10px;
|
||||
background: #373737;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px;
|
||||
border: 1px solid #373737;
|
||||
}
|
||||
|
||||
form {
|
||||
background: #f2f2f2;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
Full-Width Styles
|
||||
*******************************************************************************/
|
||||
|
||||
.outer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inner {
|
||||
position: relative;
|
||||
max-width: 640px;
|
||||
padding: 20px 10px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#forkme_banner {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top:0;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
padding: 10px 50px 10px 10px;
|
||||
color: #fff;
|
||||
background: url('../images/blacktocat.png') #0090ff no-repeat 95% 50%;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,.5);
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
#header_wrap {
|
||||
background: #212121;
|
||||
background: -moz-linear-gradient(top, #373737, #212121);
|
||||
background: -webkit-linear-gradient(top, #373737, #212121);
|
||||
background: -ms-linear-gradient(top, #373737, #212121);
|
||||
background: -o-linear-gradient(top, #373737, #212121);
|
||||
background: linear-gradient(top, #373737, #212121);
|
||||
}
|
||||
|
||||
#header_wrap .inner {
|
||||
padding: 50px 10px 30px 10px;
|
||||
}
|
||||
|
||||
#project_title {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
text-shadow: #111 0px 0px 10px;
|
||||
}
|
||||
|
||||
#project_tagline {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
background: none;
|
||||
text-shadow: #111 0px 0px 10px;
|
||||
}
|
||||
|
||||
#downloads {
|
||||
position: absolute;
|
||||
width: 210px;
|
||||
z-index: 10;
|
||||
bottom: -40px;
|
||||
right: 0;
|
||||
height: 70px;
|
||||
background: url('../images/icon_download.png') no-repeat 0% 90%;
|
||||
}
|
||||
|
||||
.zip_download_link {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 90px;
|
||||
height:70px;
|
||||
text-indent: -5000px;
|
||||
overflow: hidden;
|
||||
background: url(../images/sprite_download.png) no-repeat bottom left;
|
||||
}
|
||||
|
||||
.tar_download_link {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 90px;
|
||||
height:70px;
|
||||
text-indent: -5000px;
|
||||
overflow: hidden;
|
||||
background: url(../images/sprite_download.png) no-repeat bottom right;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.zip_download_link:hover {
|
||||
background: url(../images/sprite_download.png) no-repeat top left;
|
||||
}
|
||||
|
||||
.tar_download_link:hover {
|
||||
background: url(../images/sprite_download.png) no-repeat top right;
|
||||
}
|
||||
|
||||
#main_content_wrap {
|
||||
background: #f2f2f2;
|
||||
border-top: 1px solid #111;
|
||||
border-bottom: 1px solid #111;
|
||||
}
|
||||
|
||||
#main_content {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
#footer_wrap {
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
Small Device Styles
|
||||
*******************************************************************************/
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
body {
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
#downloads {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.inner {
|
||||
min-width: 300px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
#project_title {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
}
|
209
_sass/rouge-github.scss
Normal file
@ -0,0 +1,209 @@
|
||||
.highlight table td { padding: 5px; }
|
||||
.highlight table pre { margin: 0; }
|
||||
.highlight .cm {
|
||||
color: #777772;
|
||||
font-style: italic;
|
||||
}
|
||||
.highlight .cp {
|
||||
color: #797676;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .c1 {
|
||||
color: #777772;
|
||||
font-style: italic;
|
||||
}
|
||||
.highlight .cs {
|
||||
color: #797676;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
.highlight .c, .highlight .cd {
|
||||
color: #777772;
|
||||
font-style: italic;
|
||||
}
|
||||
.highlight .err {
|
||||
color: #a61717;
|
||||
background-color: #e3d2d2;
|
||||
}
|
||||
.highlight .gd {
|
||||
color: #000000;
|
||||
background-color: #ffdddd;
|
||||
}
|
||||
.highlight .ge {
|
||||
color: #000000;
|
||||
font-style: italic;
|
||||
}
|
||||
.highlight .gr {
|
||||
color: #aa0000;
|
||||
}
|
||||
.highlight .gh {
|
||||
color: #797676;
|
||||
}
|
||||
.highlight .gi {
|
||||
color: #000000;
|
||||
background-color: #ddffdd;
|
||||
}
|
||||
.highlight .go {
|
||||
color: #888888;
|
||||
}
|
||||
.highlight .gp {
|
||||
color: #555555;
|
||||
}
|
||||
.highlight .gs {
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .gu {
|
||||
color: #aaaaaa;
|
||||
}
|
||||
.highlight .gt {
|
||||
color: #aa0000;
|
||||
}
|
||||
.highlight .kc {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .kd {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .kn {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .kp {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .kr {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .kt {
|
||||
color: #445588;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .k, .highlight .kv {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .mf {
|
||||
color: #009999;
|
||||
}
|
||||
.highlight .mh {
|
||||
color: #009999;
|
||||
}
|
||||
.highlight .il {
|
||||
color: #009999;
|
||||
}
|
||||
.highlight .mi {
|
||||
color: #009999;
|
||||
}
|
||||
.highlight .mo {
|
||||
color: #009999;
|
||||
}
|
||||
.highlight .m, .highlight .mb, .highlight .mx {
|
||||
color: #009999;
|
||||
}
|
||||
.highlight .sb {
|
||||
color: #d14;
|
||||
}
|
||||
.highlight .sc {
|
||||
color: #d14;
|
||||
}
|
||||
.highlight .sd {
|
||||
color: #d14;
|
||||
}
|
||||
.highlight .s2 {
|
||||
color: #d14;
|
||||
}
|
||||
.highlight .se {
|
||||
color: #d14;
|
||||
}
|
||||
.highlight .sh {
|
||||
color: #d14;
|
||||
}
|
||||
.highlight .si {
|
||||
color: #d14;
|
||||
}
|
||||
.highlight .sx {
|
||||
color: #d14;
|
||||
}
|
||||
.highlight .sr {
|
||||
color: #009926;
|
||||
}
|
||||
.highlight .s1 {
|
||||
color: #d14;
|
||||
}
|
||||
.highlight .ss {
|
||||
color: #990073;
|
||||
}
|
||||
.highlight .s {
|
||||
color: #d14;
|
||||
}
|
||||
.highlight .na {
|
||||
color: #008080;
|
||||
}
|
||||
.highlight .bp {
|
||||
color: #797676;
|
||||
}
|
||||
.highlight .nb {
|
||||
color: #0086B3;
|
||||
}
|
||||
.highlight .nc {
|
||||
color: #445588;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .no {
|
||||
color: #008080;
|
||||
}
|
||||
.highlight .nd {
|
||||
color: #3c5d5d;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .ni {
|
||||
color: #800080;
|
||||
}
|
||||
.highlight .ne {
|
||||
color: #990000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .nf {
|
||||
color: #990000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .nl {
|
||||
color: #990000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .nn {
|
||||
color: #555555;
|
||||
}
|
||||
.highlight .nt {
|
||||
color: #000080;
|
||||
}
|
||||
.highlight .vc {
|
||||
color: #008080;
|
||||
}
|
||||
.highlight .vg {
|
||||
color: #008080;
|
||||
}
|
||||
.highlight .vi {
|
||||
color: #008080;
|
||||
}
|
||||
.highlight .nv {
|
||||
color: #008080;
|
||||
}
|
||||
.highlight .ow {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .o {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.highlight .w {
|
||||
color: #bbbbbb;
|
||||
}
|
||||
.highlight {
|
||||
background-color: #f8f8f8;
|
||||
}
|
5
assets/css/style.scss
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
---
|
||||
|
||||
@import "jekyll-theme-slate";
|
||||
@import "style.css";
|
BIN
assets/dialogs.jpg
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
assets/documents.PNG
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
assets/forwardvk.PNG
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/groupmessage.jpg
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
assets/privatemessage.jpg
Normal file
After Width: | Height: | Size: 136 KiB |
BIN
assets/rsz_dialogs (1).jpg
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
assets/rsz_forwardtg.jpg
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
assets/rsz_groupmessage.jpg
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/rsz_privatemessage.jpg
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
assets/rsz_reply.jpg
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
assets/rsz_search (1).jpg
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
assets/rsz_sunset (1).jpg
Normal file
After Width: | Height: | Size: 41 KiB |
1
assets/sample.txt
Normal file
@ -0,0 +1 @@
|
||||
|
BIN
assets/sticker.jpg
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
assets/sunset.jpg
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
assets/vk_music.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
116
bot.py
@ -1,116 +0,0 @@
|
||||
import io
|
||||
import logging
|
||||
import re
|
||||
import tempfile
|
||||
import ujson
|
||||
|
||||
import aiohttp
|
||||
import django.conf
|
||||
import wget
|
||||
from PIL import Image
|
||||
from aiogram import Bot
|
||||
from aiogram.dispatcher import Dispatcher
|
||||
from aiogram.types import ParseMode, MediaGroup, InlineKeyboardMarkup, InlineKeyboardButton, ChatActions
|
||||
from aiogram.utils.exceptions import *
|
||||
from aiogram.utils.parts import safe_split_text, split_text, MAX_MESSAGE_LENGTH
|
||||
from aiogram.utils import context
|
||||
from aiovk import TokenSession, API
|
||||
from aiovk.drivers import HttpDriver
|
||||
from aiovk.exceptions import *
|
||||
from aiovk.mixins import LimitRateDriverMixin
|
||||
|
||||
from config import *
|
||||
|
||||
django.conf.ENVIRONMENT_VARIABLE = SETTINGS_VAR
|
||||
os.environ.setdefault(SETTINGS_VAR, "settings")
|
||||
# Ensure settings are read
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
||||
from data.models import *
|
||||
|
||||
|
||||
class VkSession(TokenSession):
|
||||
API_VERSION = API_VERSION
|
||||
|
||||
|
||||
class RateLimitedDriver(LimitRateDriverMixin, HttpDriver):
|
||||
requests_per_period = 1
|
||||
period = 0.4
|
||||
|
||||
|
||||
DRIVERS = {}
|
||||
|
||||
|
||||
async def get_driver(vk_token=None):
|
||||
if vk_token:
|
||||
if vk_token in DRIVERS:
|
||||
return DRIVERS[vk_token]
|
||||
else:
|
||||
new_driver = RateLimitedDriver()
|
||||
DRIVERS[vk_token] = new_driver
|
||||
return new_driver
|
||||
else:
|
||||
return RateLimitedDriver()
|
||||
|
||||
|
||||
async def get_vk_chat(cid):
|
||||
return VkChat.objects.get_or_create(cid=cid)
|
||||
|
||||
|
||||
def get_max_photo(obj, keyword='photo'):
|
||||
maxarr = []
|
||||
max_photo_re = re.compile(f'{keyword}_([0-9]*)')
|
||||
for k, v in obj.items():
|
||||
m = max_photo_re.match(k)
|
||||
if m:
|
||||
maxarr.append(int(m.group(1)))
|
||||
if maxarr:
|
||||
return keyword + '_' + str(max(maxarr))
|
||||
|
||||
|
||||
async def get_content(url, docname='tgvkbot.document', chrome_headers=True, rewrite_name=False,
|
||||
custom_ext=''):
|
||||
try:
|
||||
with aiohttp.ClientSession(headers=CHROME_HEADERS if chrome_headers else {}) as session:
|
||||
r = await session.request('GET', url)
|
||||
direct_url = str(r.url)
|
||||
tempdir = tempfile.gettempdir()
|
||||
filename_options = {'out': docname} if rewrite_name else {'default': docname}
|
||||
if direct_url != url:
|
||||
r.release()
|
||||
c = await session.request('GET', direct_url)
|
||||
file = wget.detect_filename(direct_url, headers=dict(c.headers), **filename_options)
|
||||
temppath = os.path.join(tempdir, file + custom_ext)
|
||||
with open(temppath, 'wb') as f:
|
||||
f.write(await c.read())
|
||||
else:
|
||||
file = wget.detect_filename(direct_url, headers=dict(r.headers), **filename_options)
|
||||
temppath = os.path.join(tempdir, file + custom_ext)
|
||||
with open(temppath, 'wb') as f:
|
||||
f.write(await r.read())
|
||||
content = open(temppath, 'rb')
|
||||
return {'content': content, 'file_name': file, 'custom_ext': custom_ext, 'temp_path': tempdir}
|
||||
except Exception:
|
||||
return {'url': url, 'docname': docname}
|
||||
|
||||
|
||||
bot = Bot(token=BOT_TOKEN)
|
||||
dp = Dispatcher(bot)
|
||||
dp.loop.set_task_factory(context.task_factory)
|
||||
import traceback
|
||||
|
||||
|
||||
@dp.errors_handler()
|
||||
async def all_errors_handler(dp, update, e):
|
||||
if 'message' in dir(update) and update.message:
|
||||
user = update.message.from_user.full_name
|
||||
user_id = update.message.from_user.id
|
||||
else:
|
||||
user = update.callback_query.from_user.full_name
|
||||
user_id = update.callback_query.from_user.id
|
||||
|
||||
logging.exception(f'The update was: {ujson.dumps(update.to_python(), indent=4)}', exc_info=True)
|
||||
|
||||
return True
|
42
config.py
@ -1,42 +0,0 @@
|
||||
import os
|
||||
|
||||
ALLOWED_USER_IDS = os.environ.get('ALLOWED_USER_IDS', '')
|
||||
|
||||
DATABASE_USER = os.environ.get('POSTGRES_USER', 'postgres')
|
||||
DATABASE_PASSWORD = os.environ.get('POSTGRES_PASSWORD', 'postgres')
|
||||
DATABASE_HOST = os.environ.get('DATABASE_HOST', 'db')
|
||||
DATABASE_PORT = os.environ.get('DATABASE_PORT', '5432')
|
||||
DATABASE_NAME = os.environ.get('POSTGRES_DB', 'tgvkbot')
|
||||
|
||||
DATABASE_URL = os.environ.get('DATABASE_URL', '')
|
||||
|
||||
VK_APP_ID = os.environ.get('VK_APP_ID', '2685278') # Kate mobile
|
||||
|
||||
AUDIO_URL = os.environ.get('AUDIO_URL', '')
|
||||
AUDIO_ACCESS_URL = os.environ.get('AUDIO_ACCESS_URL',
|
||||
'')
|
||||
TOKEN_REFRESH_URL = os.environ.get('TOKEN_REFRESH_URL', '')
|
||||
AUDIO_SEARCH_URL = os.environ.get('AUDIO_SEARCH_URL', '')
|
||||
AUDIO_PROXY_URL = os.environ.get('AUDIO_PROXY_URL', '')
|
||||
AUDIO_HEADERS = {
|
||||
'user-agent': 'KateMobileAndroid/52.1 lite-445 (Android 4.4.2; SDK 19; x86; unknown Android SDK built for x86; en)'}
|
||||
|
||||
CHROME_HEADERS = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'}
|
||||
BOT_TOKEN = os.environ.get('BOT_TOKEN')
|
||||
|
||||
SETTINGS_VAR = os.environ.get('SETTINGS_VAR', 'DJANGO_TGVKBOT_SETTINGS_MODULE')
|
||||
|
||||
MAX_FILE_SIZE = os.environ.get('MAX_FILE_SIZE', 52428800)
|
||||
|
||||
API_VERSION = os.environ.get('API_VERSION', '5.124')
|
||||
AUDIO_API_VERSION = os.environ.get('API_VERSION', '5.78')
|
||||
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', '!jh4wm=%s%l&jv7-lru6hg)mq2pk&rd@i*s0*c!v!zv01cf9iw')
|
||||
|
||||
SENTRY_URL = os.environ.get('SENTRY_URL', None)
|
||||
|
||||
if SENTRY_URL:
|
||||
import sentry_sdk
|
||||
|
||||
sentry_sdk.init(SENTRY_URL)
|
105
data/models.py
@ -1,105 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
from django.db import models
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
|
||||
class AsyncManager(models.Manager):
|
||||
""" A model manager which uses the AsyncQuerySet. """
|
||||
|
||||
async def get_query_set(self):
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(None, AsyncQuerySet(self.model, using=self._db))
|
||||
|
||||
|
||||
class AsyncQuerySet(QuerySet):
|
||||
""" A queryset which allows DB operations to be pre-triggered so that they run in the
|
||||
background while the application can continue doing other processing.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AsyncQuerySet, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class TgUser(models.Model):
|
||||
objects = AsyncManager()
|
||||
|
||||
# id пользователя на сервере Telegram
|
||||
uid = models.BigIntegerField(unique=True)
|
||||
|
||||
# имя
|
||||
first_name = models.CharField(max_length=256)
|
||||
|
||||
# фамилия
|
||||
last_name = models.CharField(
|
||||
max_length=256,
|
||||
null=True,
|
||||
default=None,
|
||||
)
|
||||
|
||||
# username
|
||||
username = models.CharField(
|
||||
max_length=256,
|
||||
null=True,
|
||||
default=None,
|
||||
)
|
||||
|
||||
BLOCKED = -1
|
||||
BASE = 0
|
||||
|
||||
STATUSES = (
|
||||
(BLOCKED, 'Заблокирован'),
|
||||
(BASE, 'Базовый'),
|
||||
)
|
||||
|
||||
status = models.IntegerField(
|
||||
choices=STATUSES,
|
||||
default=BASE
|
||||
)
|
||||
|
||||
|
||||
class VkUser(models.Model):
|
||||
objects = AsyncManager()
|
||||
|
||||
token = models.TextField(unique=True)
|
||||
is_polling = models.BooleanField(default=False)
|
||||
owner = models.ForeignKey(TgUser, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class VkChat(models.Model):
|
||||
objects = AsyncManager()
|
||||
|
||||
cid = models.BigIntegerField(unique=True)
|
||||
|
||||
|
||||
class TgChat(models.Model):
|
||||
objects = AsyncManager()
|
||||
|
||||
cid = models.BigIntegerField(unique=True)
|
||||
|
||||
|
||||
class Forward(models.Model):
|
||||
objects = AsyncManager()
|
||||
|
||||
owner = models.ForeignKey(TgUser, on_delete=models.CASCADE)
|
||||
tgchat = models.ForeignKey(TgChat, on_delete=models.CASCADE)
|
||||
vkchat = models.ForeignKey(VkChat, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
objects = AsyncManager()
|
||||
|
||||
vk_chat = models.BigIntegerField()
|
||||
vk_id = models.BigIntegerField(null=True)
|
||||
tg_chat = models.BigIntegerField()
|
||||
tg_id = models.BigIntegerField()
|
||||
|
||||
|
||||
class MessageMarkup(models.Model):
|
||||
objects = AsyncManager()
|
||||
|
||||
message_id = models.BigIntegerField()
|
||||
|
||||
chat_id = models.BigIntegerField()
|
||||
|
||||
buttons = models.TextField(null=True, blank=True)
|
@ -1,32 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
|
||||
services:
|
||||
bot_local:
|
||||
build: .
|
||||
volumes:
|
||||
- .:/src
|
||||
container_name: tgvkbot_local
|
||||
logging:
|
||||
options:
|
||||
max-size: "10M"
|
||||
max-file: "10"
|
||||
restart: always
|
||||
env_file:
|
||||
- env_file
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: postgres:9-alpine
|
||||
container_name: tgvkbot_db
|
||||
volumes:
|
||||
- "dbdata:/var/lib/postgresql/data"
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: 'tgvkbot'
|
||||
POSTGRES_PASSWORD: 'postgres'
|
||||
|
||||
volumes:
|
||||
dbdata:
|
@ -1,30 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
|
||||
services:
|
||||
bot:
|
||||
image: kylmakalle/tgvkbot:latest
|
||||
container_name: tgvkbot
|
||||
logging:
|
||||
options:
|
||||
max-size: "10M"
|
||||
max-file: "10"
|
||||
restart: always
|
||||
env_file:
|
||||
- env_file
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: 'true'
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: postgres:9-alpine
|
||||
container_name: tgvkbot_db
|
||||
volumes:
|
||||
- "dbdata:/var/lib/postgresql/data"
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: 'tgvkbot'
|
||||
POSTGRES_PASSWORD: 'postgres'
|
||||
|
||||
volumes:
|
||||
dbdata:
|
59
install.sh
@ -1,59 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
ASKED_FOR_SUDO=""
|
||||
|
||||
if [ ! $(which docker) ]; then
|
||||
echo "🔑 Пароль sudo потребуется для установки Docker"
|
||||
sudo true
|
||||
ASKED_FOR_SUDO="1"
|
||||
|
||||
echo "🔨 Устанавливаем docker..."
|
||||
curl -fsSL https://get.docker.com/ | sh
|
||||
user="$(id -un 2>/dev/null || true)"
|
||||
sudo groupadd docker
|
||||
sudo usermod -aG docker $user
|
||||
else
|
||||
echo "👌 Docker уже установлен"
|
||||
fi
|
||||
|
||||
if [ ! $(which docker-compose) ]; then
|
||||
if [ ! $ASKED_FOR_SUDO ]; then
|
||||
echo "🔑 Пароль sudo потребуется для установки docker-compose"
|
||||
sudo true
|
||||
ASKED_FOR_SUDO="1"
|
||||
fi
|
||||
echo "🔨 Устанавливаем docker-compose..."
|
||||
# Install docker-compose
|
||||
COMPOSE_VERSION=$(git ls-remote https://github.com/docker/compose | grep refs/tags | grep -oE "[0-9]+\.[0-9][0-9]+\.[0-9]+$" | sort --version-sort | tail -n 1)
|
||||
sudo sh -c "curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose"
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
sudo sh -c "curl -L https://raw.githubusercontent.com/docker/compose/${COMPOSE_VERSION}/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose"
|
||||
else
|
||||
echo "👌 Docker-compose уже установлен"
|
||||
fi
|
||||
|
||||
# Нужно убедиться, что бот встанет и переменные окружения настроятся
|
||||
set -e
|
||||
|
||||
echo "⚙️ Настраиваем переменные окружения..."
|
||||
python3 setenv.py
|
||||
|
||||
if [ ! "$(docker ps -a | grep watchtower)" ]; then
|
||||
echo "🔄 Поднимаем систему обновлений watchtower..."
|
||||
docker run -d \
|
||||
--name watchtower \
|
||||
--restart always \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e TZ='Europe/Moscow' \
|
||||
-e WATCHTOWER_CLEANUP='true' \
|
||||
-e WATCHTOWER_INCLUDE_STOPPED='true' \
|
||||
-e WATCHTOWER_MONITOR_ONLY='false' \
|
||||
-e WATCHTOWER_LABEL_ENABLE='true' \
|
||||
containrrr/watchtower:latest
|
||||
else
|
||||
echo "👌 Апдейтер watchtower уже запущен"
|
||||
fi
|
||||
|
||||
echo "🚀 Запускаем бота..."
|
||||
docker-compose up -d
|
||||
|
||||
echo "✅ Готово"
|
77
installation/README.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Создание и развёртывание бота.
|
||||
|
||||
В первую очередь, _почему же нужно создавать именно своего бота?_
|
||||
- Ограничения <a href="https://core.telegram.org/bots/faq#broadcasting-to-users" target="_blank">Telegram</a>
|
||||
- Ограничения <a href="https://vk.com/dev/api_requests?f=3.1.%20%D0%A7%D0%B0%D1%81%D1%82%D0%BE%D1%82%
|
||||
D0%BD%D1%8B%D0%B5%20%D0%BE%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D1%8F" target="_blank">VK</a>
|
||||
- Гарантия сохранности переписки и полного контроля над ботом
|
||||
|
||||
Бот имеет полностью открытый исходный код, с которым можно ознакомиться перейдя по ссылке _View on GitHub_ в верхнем углу станицы.
|
||||
|
||||
# ⚠️ ВНИМАНИЕ
|
||||
Гайд далее рассказывает об устаревшем методе установки бота на heroku. К сожалению, пришлось забросить такой вариант из-за модификаций бота и отключения старых VK API. Исходники старого бота лежат в ветке [heroku-old](https://github.com/Kylmakalle/tgvkbot/tree/heroku-old)
|
||||
|
||||
Для энтузиастов есть [инструкция](https://github.com/Kylmakalle/tgvkbot/blob/master/README.md) по установке бота на свой сервер.
|
||||
|
||||
Если вы боитесь командной строки, рекомендую воспользоваться готовым ботом от разработчика [@tgVKbot](https://t.me/tgVKbot)
|
||||
|
||||
|
||||
|
||||
|
||||
## Heroku
|
||||
|
||||
Развёртывать бота мы будем на Heroku, поэтому там нужно завести аккаунт <a href="https://heroku.com" target="_blank">heroku.com</a>
|
||||
|
||||
Т.к мы будем использовать бесплатный тип хостинга, то на него накладываются некоторые <a href="https://devcenter.heroku.com/articles/free-dyno-hours#usage" target="_blank">ограничения</a>, дабы частично их смягчить можно <a href="https://dashboard.heroku.com/account/billing" target="_blank">привязать карту</a> (никаких плат не взимается), тогда бот сможет работать полный месяц без остановок (а не предусмотренные 550ч/мес изначально)
|
||||
|
||||
Подробнее можно ознакомиться <a href="https://devcenter.heroku.com/articles/free-dyno-hours" target="_blank">тут</a>
|
||||
|
||||
## VK
|
||||
|
||||
По <a href="https://vk.com/editapp?act=create" target="_blank">ссылке</a> нужно создать своё Standalone-приложение, затем во вкладке _Настройки_ перевести _Состояние_ в _Приложение включено и видно всем_, не забудьте сохранить изменения!
|
||||
|
||||
На этом этапе мы сохраняем себе куда-нибудь **ID приложения**
|
||||
|
||||
## Telegram
|
||||
|
||||
<a href="https://t.me/BotFather" target="_blank">Создаём бота</a>, в качестве имени удобно будет использовать _VK_, юзернейм роли не играет. Сохраняем **токен бота**, который мы получили от BotFather
|
||||
|
||||
_Опционально:_
|
||||
|
||||
Настройка команд бота
|
||||
|
||||
```
|
||||
dialogs - Список диалогов
|
||||
search - Поиск диалогов
|
||||
chat - Текущий чат
|
||||
leave - Покинуть текущий чат
|
||||
start - Подключить ВК
|
||||
stop - Отключить ВК
|
||||
```
|
||||
[Логотип VK](vklogo.jpg) для аватарки бота.
|
||||
|
||||
<h2><a name="heroku--vk--telegram"></a>Heroku + VK + Telegram</h2>
|
||||
Получив ID приложения и токен бота, можно смело начинать развёртывать бота на Heroku.
|
||||
|
||||
Нажав на кнопку ниже вы попадёте на страницу развёртывания, где будут поля:
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/Kylmakalle/tgvkbot/tree/master)
|
||||
- **Имя приложения** - можно оставить пустым, Heroku выберет за вас.
|
||||
- **Регион развёртывания** - выбираем Европу, чтобы бот был пошустрее.
|
||||
- **Telegram API Token** - токен бота полученный ранее.
|
||||
- **VK APP ID** - ID приложения ВКонтакте полученный ранее.
|
||||
|
||||
|
||||
|
||||
После успешного развёртывания можно смело пользоваться ботом.
|
||||
|
||||
|
||||
Если вы вдруг что-то напутали с токеном и/или ID, их всегда можно настроить перейдя в _Settings->Config Vars_ соответствующего приложения в своей панели управления <a href="https://dashboard.heroku.com/ " target="_blank">dashboard.heroku.com</a>
|
||||
|
||||
Если всё прошло гладко, то можно ознакомиться с [Использованием](/tgvkbot/usage/)
|
||||
|
||||
## Обновление
|
||||
Обо всех **ВАЖНЫХ** обновлениях можно будет узнать в <a href="https://t.me/tg_vk" target="_blank">Telegram канале</a>
|
||||
|
||||
К сожалению простого механизма обновления бота нет и не предусмотрено, поэтому единственный способ - повторное <a href="#heroku--vk--telegram">развёртывание</a> бота на Heroku с уже имеющимися токеном бота и ID приложения из VK.
|
||||
При обновлении **обязательно** удалить предыдущее приложение с ботом на Heroku. Делается это в _Settings_ соответствующего приложения, в самом низу страницы. После обновления не забудьте заново залогиниться ВК с помощью `/start`
|
BIN
installation/vklogo.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
24
manage.py
@ -1,24 +0,0 @@
|
||||
import sys
|
||||
from config import *
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault(SETTINGS_VAR, "settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError:
|
||||
# The above import may fail for some other reason. Ensure that the
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
try:
|
||||
import django
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
)
|
||||
raise
|
||||
import django.conf
|
||||
|
||||
django.conf.ENVIRONMENT_VARIABLE = SETTINGS_VAR
|
||||
execute_from_command_line(sys.argv)
|
@ -1,16 +0,0 @@
|
||||
aiogram==1.2.2
|
||||
aiohttp==2.3.10
|
||||
aiovk==1.3.0
|
||||
async-timeout==2.0.1
|
||||
attrs==17.4.0
|
||||
Django==2.0.4
|
||||
multidict==4.1.0
|
||||
Pillow==5.1.0
|
||||
psycopg2-binary==2.7.4
|
||||
pytz==2018.3
|
||||
sentry-sdk==0.20.3
|
||||
ujson==1.35
|
||||
urllib3==1.25.3
|
||||
wget==3.2
|
||||
yarl==1.1.1
|
||||
dj-database-url==0.5.0
|
77
setenv.py
@ -1,77 +0,0 @@
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import urlopen, Request
|
||||
|
||||
from config import API_VERSION, VK_APP_ID
|
||||
|
||||
ENV_FILE_TEMPLATE = """
|
||||
POSTGRES_DB=tgvkbot
|
||||
POSTGRES_PASSWORD=postgres
|
||||
BOT_TOKEN=%(tg_token)s
|
||||
VK_APP_ID=%(vk_app_id)s
|
||||
ALLOWED_USER_IDS=%(allowed_user_ids)s
|
||||
"""
|
||||
|
||||
ENV_FILE = 'env_file'
|
||||
|
||||
def check_token(token):
|
||||
response = urlopen("https://api.telegram.org/bot{token}/{method}".format(token=token, method='getMe'))
|
||||
if response.code == 200:
|
||||
return True
|
||||
else:
|
||||
raise HTTPError
|
||||
|
||||
|
||||
def get_auth_page(app_id):
|
||||
AUTH_URL = 'https://oauth.vk.com/authorize'
|
||||
params = {'client_id': app_id,
|
||||
'redirect_uri': 'https://oauth.vk.com/blank.html',
|
||||
'display': 'mobile',
|
||||
'response_type': 'token',
|
||||
'v': API_VERSION
|
||||
}
|
||||
post_args = urlencode(params).encode('UTF-8')
|
||||
request = Request(AUTH_URL, post_args)
|
||||
response = urlopen(request)
|
||||
if response.code == 200:
|
||||
return True
|
||||
else:
|
||||
raise HTTPError
|
||||
|
||||
|
||||
def set_env():
|
||||
while True:
|
||||
tg_token = input('Токен Telegram бота: ')
|
||||
tg_token = tg_token.strip()
|
||||
try:
|
||||
print('⏳ Проверяем токен...')
|
||||
check_token(tg_token)
|
||||
break
|
||||
except HTTPError:
|
||||
print('❌ Токен бота неверный или нерабочий, попробуйте снова!')
|
||||
|
||||
while True:
|
||||
vk_app_id = input('VK APP ID (можно оставить пустым): ')
|
||||
vk_app_id = vk_app_id.strip()
|
||||
if vk_app_id:
|
||||
try:
|
||||
get_auth_page(vk_app_id)
|
||||
break
|
||||
except HTTPError:
|
||||
print('❌ VK APP ID неверный, попробуйте снова!')
|
||||
else:
|
||||
print('ℹ️ Будет использован VK APP ID {} от Kate Mobile'.format(VK_APP_ID))
|
||||
break
|
||||
|
||||
with open(ENV_FILE, 'w') as env_file:
|
||||
env_file.write(
|
||||
ENV_FILE_TEMPLATE % {'tg_token': tg_token, 'vk_app_id': vk_app_id or VK_APP_ID, 'allowed_user_ids': ''})
|
||||
|
||||
print('✅ Переменные успешно установлены в {}'.format(ENV_FILE))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
set_env()
|
||||
except KeyboardInterrupt:
|
||||
print('\n⚠️ Настройка переменных окружуения была прервана!')
|
24
settings.py
@ -1,24 +0,0 @@
|
||||
from config import *
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
DATABASES = {}
|
||||
|
||||
if DATABASE_URL:
|
||||
import dj_database_url
|
||||
# Reads string from DATABASE_URL env by default
|
||||
DATABASES['default'] = dj_database_url.config()
|
||||
else:
|
||||
DATABASES['default'] = {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': DATABASE_NAME,
|
||||
'USER': DATABASE_USER,
|
||||
'PASSWORD': DATABASE_PASSWORD,
|
||||
'HOST': DATABASE_HOST,
|
||||
'PORT': DATABASE_PORT
|
||||
}
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'data',
|
||||
)
|
976
telegram.py
@ -1,976 +0,0 @@
|
||||
from aiogram import types
|
||||
from aiogram.bot.api import FILE_URL
|
||||
from aiogram.utils import executor, json
|
||||
from aiohttp.client_exceptions import ContentTypeError
|
||||
|
||||
from bot import *
|
||||
from config import *
|
||||
from vk_messages import vk_polling_tasks, vk_polling
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
oauth_link = re.compile(
|
||||
'https://(oauth|api)\.vk\.com/blank\.html#access_token=([a-z0-9]*)&expires_in=[0-9]*&user_id=[0-9]*')
|
||||
|
||||
|
||||
async def get_pages_switcher(markup, page, pages):
|
||||
if page != 0:
|
||||
leftbutton = InlineKeyboardButton('◀', callback_data='page{}'.format(page - 1)) # callback
|
||||
else:
|
||||
leftbutton = InlineKeyboardButton('Поиск 🔍', callback_data='search')
|
||||
if page + 1 < len(pages):
|
||||
rightbutton = InlineKeyboardButton('▶', callback_data='page{}'.format(page + 1))
|
||||
else:
|
||||
rightbutton = None
|
||||
|
||||
if rightbutton:
|
||||
markup.row(leftbutton, rightbutton)
|
||||
else:
|
||||
markup.row(leftbutton)
|
||||
|
||||
|
||||
async def logged(uid, reply_to_message_id=None, to_chat=None):
|
||||
vk_user = VkUser.objects.filter(owner__uid=uid).first()
|
||||
if vk_user:
|
||||
return True
|
||||
else:
|
||||
await bot.send_message(to_chat or uid, 'Вход не выполнен! /start для входа',
|
||||
reply_to_message_id=reply_to_message_id)
|
||||
return False
|
||||
|
||||
|
||||
async def update_user_info(from_user: types.User):
|
||||
return TgUser.objects.update_or_create(uid=from_user.id,
|
||||
defaults={'first_name': from_user.first_name,
|
||||
'last_name': from_user.last_name,
|
||||
'username': from_user.username
|
||||
})
|
||||
|
||||
|
||||
async def update_chat_info(from_chat: types.Chat):
|
||||
if from_chat.type == 'private':
|
||||
return None, False
|
||||
return TgChat.objects.update_or_create(cid=from_chat.id)
|
||||
|
||||
|
||||
async def is_forwarding(text):
|
||||
if not text:
|
||||
return False, None
|
||||
if text == '!':
|
||||
return True, None
|
||||
if text.startswith('!'):
|
||||
return True, text[1:]
|
||||
return False, text
|
||||
|
||||
|
||||
async def is_bot_in_iterator(msg: types.Message):
|
||||
iterator = msg.new_chat_members or [msg.left_chat_member] or []
|
||||
me = await bot.me
|
||||
for i in iterator:
|
||||
if me.id == i.id:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
import secrets
|
||||
|
||||
|
||||
def generate_random_id():
|
||||
return secrets.randbelow(2_147_483_647)
|
||||
|
||||
|
||||
async def vk_sender(token, tg_message, **kwargs):
|
||||
session = VkSession(access_token=token, driver=await get_driver(token))
|
||||
kwargs['random_id'] = generate_random_id()
|
||||
try:
|
||||
api = API(session)
|
||||
vk_msg_id = await api('messages.send', **kwargs)
|
||||
except ContentTypeError:
|
||||
kwargs['v'] = session.API_VERSION
|
||||
kwargs['access_token'] = session.access_token
|
||||
try:
|
||||
url, html = await session.driver.post_text(url=session.REQUEST_URL + 'messages.send', data=kwargs)
|
||||
response = json.loads(html)
|
||||
vk_msg_id = response['response']
|
||||
except:
|
||||
log.exception(msg='Error in vk sender', exc_info=True)
|
||||
return None
|
||||
except VkAuthError:
|
||||
vk_user = VkUser.objects.filter(token=token).first()
|
||||
if vk_user:
|
||||
vk_user.delete()
|
||||
await bot.send_message(tg_message.chat.id, 'Вход не выполнен! /start для входа',
|
||||
reply_to_message_id=tg_message.message_id)
|
||||
return
|
||||
except VkAPIError:
|
||||
await asyncio.sleep(5)
|
||||
if kwargs.get('retries', 0) > 4:
|
||||
log.exception(msg='Error in vk sender', exc_info=True)
|
||||
return None
|
||||
else:
|
||||
kwargs['retries'] = kwargs.get('retries', 0) + 1
|
||||
await vk_sender(token, tg_message, **kwargs)
|
||||
except Exception:
|
||||
log.exception(msg='Error in vk sender', exc_info=True)
|
||||
return None
|
||||
Message.objects.create(
|
||||
vk_chat=kwargs['peer_id'],
|
||||
vk_id=vk_msg_id,
|
||||
tg_chat=tg_message.chat.id,
|
||||
tg_id=tg_message.message_id
|
||||
)
|
||||
return vk_msg_id
|
||||
|
||||
|
||||
async def generate_send_options(msg, forward=None, forward_messages_exists=False, message=None):
|
||||
message_options = dict()
|
||||
if forward:
|
||||
if msg.reply_to_message is not None:
|
||||
message_in_db = Message.objects.filter(tg_chat=msg.chat.id,
|
||||
tg_id=msg.reply_to_message.message_id).first()
|
||||
if message_in_db and message_in_db.vk_id:
|
||||
message_options['forward_messages'] = message_in_db.vk_id
|
||||
message_options['peer_id'] = forward.vkchat.cid
|
||||
|
||||
elif msg.reply_to_message is not None:
|
||||
message_in_db = Message.objects.filter(tg_chat=msg.chat.id, tg_id=msg.reply_to_message.message_id).first()
|
||||
if not message_in_db:
|
||||
await msg.reply('Не знаю в какой чат ответить, нет информации в базе данных.')
|
||||
return message_options
|
||||
if forward_messages_exists and message_in_db.vk_id:
|
||||
message_options['forward_messages'] = message_in_db.vk_id
|
||||
message_options['peer_id'] = message_in_db.vk_chat
|
||||
else:
|
||||
await msg.reply('Не понимаю что делать. Нужна помощь? Используй команду /help')
|
||||
return message_options
|
||||
|
||||
if message:
|
||||
message_options['message'] = message
|
||||
return message_options
|
||||
|
||||
|
||||
async def send_vk_action(token, peer_id, action='typing'):
|
||||
vksession = VkSession(access_token=token, driver=await get_driver(token))
|
||||
api = API(vksession)
|
||||
return # await api('messages.setActivity', peer_id=peer_id, activity=action)
|
||||
|
||||
|
||||
async def upload_attachment(msg, vk_user, file_id, peer_id, attachment_type, upload_field, upload_method,
|
||||
on_server_field='file', save_method='', upload_type=None, default_name='tgvkbot.document',
|
||||
title='tgvkbot.document', rewrite_name=False, custom_ext=''):
|
||||
try:
|
||||
file_info = await bot.get_file(file_id)
|
||||
path = file_info['file_path']
|
||||
if msg.content_type == 'audio':
|
||||
if not custom_ext and '.' in path and path.split('.')[-1] == 'mp3':
|
||||
custom_ext = '.aac'
|
||||
except NetworkError:
|
||||
await msg.reply('Файл слишком большой, максимально допустимый размер <b>20мб!</b>', parse_mode=ParseMode.HTML)
|
||||
return
|
||||
|
||||
url = FILE_URL.format(token=bot._BaseBot__token, path=path)
|
||||
await send_vk_action(vk_user.token, peer_id)
|
||||
content = await get_content(url, default_name, chrome_headers=False, rewrite_name=rewrite_name,
|
||||
custom_ext=custom_ext)
|
||||
filename = (content.get('file_name', '') + content.get('custom_ext', '')) or None
|
||||
if 'content' in content:
|
||||
vksession = VkSession(access_token=vk_user.token, driver=await get_driver(vk_user.token))
|
||||
api = API(vksession)
|
||||
upload_options = {}
|
||||
if attachment_type != 'photo' and upload_type:
|
||||
upload_options['type'] = upload_type
|
||||
if msg.content_type == 'sticker':
|
||||
webp = Image.open(content['content']).convert('RGBA')
|
||||
png = io.BytesIO()
|
||||
webp.save(png, format='png')
|
||||
content['content'] = png.getvalue()
|
||||
if attachment_type == 'video':
|
||||
upload_options['is_private'] = 1
|
||||
upload_server = await api(upload_method, **upload_options)
|
||||
with aiohttp.ClientSession() as session:
|
||||
data = aiohttp.FormData()
|
||||
field_data = {}
|
||||
if filename:
|
||||
field_data['filename'] = filename
|
||||
data.add_field(upload_field, content['content'], content_type='multipart/form-data', **field_data)
|
||||
async with session.post(upload_server['upload_url'], data=data) as upload:
|
||||
file_on_server = json.loads(await upload.text())
|
||||
if msg.content_type != 'sticker':
|
||||
content['content'].close()
|
||||
try:
|
||||
os.remove(os.path.join(content['temp_path'], content['file_name'] + content['custom_ext']))
|
||||
except:
|
||||
pass
|
||||
if attachment_type == 'photo':
|
||||
save_options = {'server': file_on_server['server'], on_server_field: file_on_server[on_server_field],
|
||||
'hash': file_on_server['hash']}
|
||||
elif attachment_type == 'video':
|
||||
return f'{attachment_type}{upload_server["owner_id"]}_{upload_server["video_id"]}_{upload_server["access_key"]}'
|
||||
else:
|
||||
if 'file' not in file_on_server:
|
||||
await msg.reply('<b>Ошибка</b> Не удалось загрузить файл. Файл не должен быть исполняемым.',
|
||||
parse_mode=ParseMode.HTML)
|
||||
return
|
||||
save_options = dict({'file': file_on_server['file']})
|
||||
save_options['title'] = title
|
||||
attachment = await api(save_method, **save_options)
|
||||
if 'type' not in attachment:
|
||||
attachment = attachment[0]
|
||||
else:
|
||||
attachment = attachment[attachment['type']]
|
||||
return f'{attachment_type}{attachment["owner_id"]}_{attachment["id"]}'
|
||||
|
||||
|
||||
async def get_dialogs(token, exclude=None):
|
||||
if not exclude:
|
||||
exclude = []
|
||||
session = VkSession(access_token=token, driver=await get_driver(token))
|
||||
api = API(session)
|
||||
dialogs = await api('messages.getDialogs', count=200)
|
||||
order = []
|
||||
users_ids = []
|
||||
group_ids = []
|
||||
for chat in dialogs.get('items'):
|
||||
chat = chat.get('message', '')
|
||||
if chat:
|
||||
if 'chat_id' in chat:
|
||||
if 2000000000 + chat['chat_id'] not in exclude:
|
||||
chat['title'] = chat['title']
|
||||
order.append({'title': chat['title'], 'id': 2000000000 + chat['chat_id']})
|
||||
elif chat['user_id'] > 0:
|
||||
if chat['user_id'] not in exclude:
|
||||
order.append({'title': 'Диалог ' + str(chat['user_id']), 'id': chat['user_id']})
|
||||
users_ids.append(chat['user_id'])
|
||||
elif chat['user_id'] < 0:
|
||||
if chat['user_id'] not in exclude:
|
||||
order.append({'title': 'Диалог ' + str(chat['user_id']), 'id': chat['user_id']})
|
||||
group_ids.append(chat['user_id'])
|
||||
|
||||
if users_ids:
|
||||
users = await api('users.get', user_ids=', '.join(str(x) for x in users_ids))
|
||||
else:
|
||||
users = []
|
||||
|
||||
if group_ids:
|
||||
groups = await api('groups.getById', group_ids=', '.join(str(abs(x)) for x in group_ids))
|
||||
else:
|
||||
groups = []
|
||||
|
||||
for output in order:
|
||||
if output['id'] > 0:
|
||||
u = next((i for i in users if i['id'] == output['id']), None)
|
||||
if u:
|
||||
output['title'] = f'{u["first_name"]} {u["last_name"]}'
|
||||
else:
|
||||
g = next((i for i in groups if -i['id'] == output['id']), None)
|
||||
if g:
|
||||
output['title'] = g["name"]
|
||||
|
||||
for button in range(len(order)):
|
||||
order[button] = InlineKeyboardButton(order[button]['title'], callback_data=f'chat{order[button]["id"]}')
|
||||
|
||||
rows = [order[x:x + 2] for x in range(0, len(order), 2)]
|
||||
pages = [rows[x:x + 4] for x in range(0, len(rows), 4)]
|
||||
|
||||
return pages
|
||||
|
||||
|
||||
async def search_dialogs(msg: types.Message, user=None):
|
||||
if not user:
|
||||
user, created = await update_user_info(msg.from_user)
|
||||
vkuser = VkUser.objects.filter(owner=user).first()
|
||||
vksession = VkSession(access_token=vkuser.token, driver=await get_driver(vkuser.token))
|
||||
api = API(vksession)
|
||||
markup = InlineKeyboardMarkup(row_width=1)
|
||||
await bot.send_chat_action(msg.chat.id, 'typing')
|
||||
result = await api('messages.searchDialogs', q=msg.text, limit=10)
|
||||
for chat in result:
|
||||
title = None
|
||||
data = None
|
||||
if chat['type'] == 'profile':
|
||||
title = f'{chat["first_name"]} {chat["last_name"]}'
|
||||
data = f'chat{chat["id"]}'
|
||||
elif chat['type'] == 'chat':
|
||||
title = chat['title']
|
||||
data = f'chat{2000000000 + chat["id"]}'
|
||||
elif chat['type'] == 'page':
|
||||
title = await chat['name']
|
||||
data = f'chat{-chat["id"]}'
|
||||
if title and data:
|
||||
markup.add(InlineKeyboardButton(text=title, callback_data=data))
|
||||
markup.add(InlineKeyboardButton('Поиск 🔍', callback_data='search'))
|
||||
if markup.inline_keyboard:
|
||||
text = f'<b>Результат поиска по</b> <i>{msg.text}</i>'
|
||||
else:
|
||||
text = f'<b>Результат поиска по</b> <i>{msg.text}</i>'
|
||||
await bot.send_message(msg.chat.id, text, reply_markup=markup, parse_mode=ParseMode.HTML)
|
||||
|
||||
|
||||
async def refresh_token(vkuser):
|
||||
try:
|
||||
with aiohttp.ClientSession() as session:
|
||||
r = await session.request('GET', TOKEN_REFRESH_URL, params={'token': vkuser.token})
|
||||
data = await r.json()
|
||||
if data['ok']:
|
||||
vkuser.token = data['token']
|
||||
vkuser.save()
|
||||
session.close()
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@dp.callback_query_handler(func=lambda call: call and call.message and call.data and call.data.startswith('logged'))
|
||||
async def check_logged(call: types.CallbackQuery):
|
||||
vkuser = VkUser.objects.filter(owner__uid=call.from_user.id).count()
|
||||
if vkuser:
|
||||
await handle_join(call.message, edit=True, chat_id=call.message.chat.id, message_id=call.message.message_id,
|
||||
exclude=True)
|
||||
else:
|
||||
await bot.answer_callback_query(call.id, 'Вход не выполнен! Сперва нужно выполнить вход в ВК через бота',
|
||||
show_alert=True)
|
||||
|
||||
|
||||
@dp.callback_query_handler(func=lambda call: call and call.message and call.data and call.data.startswith('page'))
|
||||
async def page_switcher(call: types.CallbackQuery):
|
||||
# user, created = await update_user_info(call.from_user)
|
||||
# tgchat, tgchat_created = await update_chat_info(call.message.chat)
|
||||
page = int(call.data.split('page')[-1])
|
||||
message_markup = MessageMarkup.objects.filter(
|
||||
chat_id=call.message.chat.id,
|
||||
message_id=call.message.message_id,
|
||||
).first()
|
||||
if message_markup:
|
||||
pages = json.loads(message_markup.buttons)
|
||||
markup = InlineKeyboardMarkup()
|
||||
for row in pages[page]:
|
||||
markup.row(*[InlineKeyboardButton(**button) for button in row])
|
||||
await get_pages_switcher(markup, page, pages)
|
||||
await bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=markup)
|
||||
await bot.answer_callback_query(call.id)
|
||||
else:
|
||||
await bot.answer_callback_query(call.id, 'Нет данных в Базе Данных', show_alert=True)
|
||||
|
||||
|
||||
async def get_dialog_info(api, vk_chat_id, name_case='nom'):
|
||||
title = ''
|
||||
photo = ''
|
||||
dialog_type = ''
|
||||
if vk_chat_id >= 2000000000:
|
||||
dialog_info = await api('messages.getChat', chat_id=vk_chat_id - 2000000000)
|
||||
title = dialog_info['title']
|
||||
max_photo = get_max_photo(dialog_info)
|
||||
if max_photo:
|
||||
photo = dialog_info[max_photo]
|
||||
else:
|
||||
photo = None
|
||||
dialog_type = 'chat'
|
||||
elif vk_chat_id > 0:
|
||||
dialog_info = await api('users.get', user_ids=vk_chat_id, fields='photo_max', name_case=name_case)
|
||||
first_name = dialog_info[0]['first_name']
|
||||
last_name = dialog_info[0]['last_name'] or ''
|
||||
title = first_name + ' ' + last_name
|
||||
photo = dialog_info[0]['photo_max']
|
||||
dialog_type = 'user'
|
||||
elif vk_chat_id < 0:
|
||||
dialog_info = await api('groups.getById', group_ids=abs(vk_chat_id))
|
||||
title = dialog_info[0]['name']
|
||||
max_photo = get_max_photo(dialog_info[0])
|
||||
if max_photo:
|
||||
photo = dialog_info[0][max_photo]
|
||||
else:
|
||||
photo = None
|
||||
dialog_type = 'group'
|
||||
|
||||
return {'title': title, 'photo': photo, 'type': dialog_type}
|
||||
|
||||
|
||||
@dp.callback_query_handler(func=lambda call: call and call.message and call.data and call.data.startswith('ping'))
|
||||
async def ping_button(call: types.CallbackQuery):
|
||||
tg_chat_id = int(call.data.split('ping')[-1])
|
||||
try:
|
||||
await bot.send_message(tg_chat_id, f'<a href="tg://user?id={call.from_user.id}">Ping!</a>',
|
||||
parse_mode=ParseMode.HTML)
|
||||
await bot.answer_callback_query(call.id, 'Ping!')
|
||||
except BadRequest:
|
||||
await bot.answer_callback_query(call.id, 'Нет доступа к чату, бот кикнут или чат удалён!', show_alert=True)
|
||||
|
||||
|
||||
@dp.callback_query_handler(
|
||||
func=lambda call: call and call.message and call.data and call.data.startswith('deleteforward'))
|
||||
async def delete_forward(call: types.CallbackQuery):
|
||||
forward_id = int(call.data.split('deleteforward')[-1])
|
||||
forward_in_db = Forward.objects.filter(id=forward_id).first()
|
||||
if forward_in_db:
|
||||
forward_in_db.delete()
|
||||
|
||||
markup = InlineKeyboardMarkup()
|
||||
|
||||
message_markup = MessageMarkup.objects.filter(
|
||||
message_id=call.message.message_id,
|
||||
chat_id=call.message.chat.id
|
||||
).first()
|
||||
|
||||
buttons = json.loads(message_markup.buttons)
|
||||
|
||||
for row in buttons:
|
||||
if row[1]['callback_data'] == call.data:
|
||||
buttons.remove(row)
|
||||
else:
|
||||
markup.row(*[InlineKeyboardButton(**button) for button in row])
|
||||
if message_markup:
|
||||
if buttons:
|
||||
message_markup.buttons = json.dumps(buttons)
|
||||
message_markup.save()
|
||||
await bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=markup)
|
||||
else:
|
||||
await bot.edit_message_text(
|
||||
'У Вас нет связанных чатов. Чтобы привязать чат, добавьте бота в группу, а если бот уже добавлен - используйте команду /dialogs',
|
||||
call.message.chat.id, call.message.message_id)
|
||||
await bot.answer_callback_query(call.id, 'Успешно удалено!')
|
||||
else:
|
||||
await bot.edit_message_text('<b>Что-то пошло не так, нет информации в Базе Данных</b>',
|
||||
message_id=call.message.message_id, chat_id=call.message.chat.id, reply_markup=None)
|
||||
await bot.answer_callback_query(call.id, 'Ошибка!')
|
||||
|
||||
|
||||
@dp.callback_query_handler(func=lambda call: call and call.message and call.data and call.data.startswith('setinfo'))
|
||||
async def set_info(call: types.CallbackQuery):
|
||||
user, created = await update_user_info(call.from_user)
|
||||
tgchat, tgchat_created = await update_chat_info(call.message.chat)
|
||||
vk_chat_id = int(call.data.split('setinfo')[-1])
|
||||
vkuser = VkUser.objects.filter(owner=user).first()
|
||||
if vkuser:
|
||||
ME = await bot.me
|
||||
can_edit = False
|
||||
if not call.message.chat.all_members_are_administrators and (
|
||||
(await bot.get_chat_member(call.message.chat.id, ME.id)).status == 'administrator'):
|
||||
can_edit = True
|
||||
if not can_edit:
|
||||
admins = await bot.get_chat_administrators(call.message.chat.id)
|
||||
for admin in admins:
|
||||
if admin.user.id == ME.id and admin.can_change_info:
|
||||
can_edit = True
|
||||
break
|
||||
if can_edit:
|
||||
vksession = VkSession(access_token=vkuser.token, driver=await get_driver(vkuser.token))
|
||||
api = API(vksession)
|
||||
dialog_info = await get_dialog_info(api, vk_chat_id, name_case='nom')
|
||||
if dialog_info.get('title', ''):
|
||||
await bot.set_chat_title(call.message.chat.id, dialog_info['title'])
|
||||
if dialog_info.get('photo', ''):
|
||||
content = await get_content(dialog_info['photo'])
|
||||
await bot.set_chat_photo(call.message.chat.id, content['content'])
|
||||
content['content'].close()
|
||||
try:
|
||||
os.remove(os.path.join(content['temp_path'], content['file_name'] + content['custom_ext']))
|
||||
except:
|
||||
pass
|
||||
|
||||
if dialog_info['type'] == 'user':
|
||||
dialog_info = await get_dialog_info(api, vk_chat_id, name_case='ins')
|
||||
text = f'Чат успешно привязан к диалогу c <i>{dialog_info["title"]}</i>'
|
||||
elif dialog_info['type'] == 'group':
|
||||
text = f'Чат успешно привязан к диалогу с сообществом <i>{dialog_info["title"]}</i>'
|
||||
else:
|
||||
text = f'Чат успешно привязан к диалогу <i>{dialog_info["title"]}</i>'
|
||||
|
||||
await bot.edit_message_text(text, call.message.chat.id, call.message.message_id, parse_mode=ParseMode.HTML)
|
||||
await bot.answer_callback_query(call.id)
|
||||
else:
|
||||
await bot.answer_callback_query(call.id,
|
||||
'Недостаточно прав для редактирования информации о группе или бот не администратор!',
|
||||
show_alert=True)
|
||||
|
||||
else:
|
||||
await bot.answer_callback_query(call.id, 'Вход не выполнен! Сперва нужно выполнить вход в ВК через бота',
|
||||
show_alert=True)
|
||||
|
||||
|
||||
@dp.callback_query_handler(func=lambda call: call and call.message and call.data and call.data.startswith('chat'))
|
||||
async def choose_chat(call: types.CallbackQuery):
|
||||
user, created = await update_user_info(call.from_user)
|
||||
tgchat, tgchat_created = await update_chat_info(call.message.chat)
|
||||
vk_chat_id = int(call.data.split('chat')[-1])
|
||||
vkuser = VkUser.objects.filter(owner=user).first()
|
||||
if vkuser:
|
||||
if call.message.chat.type == 'private':
|
||||
vksession = VkSession(access_token=vkuser.token, driver=await get_driver(vkuser.token))
|
||||
api = API(vksession)
|
||||
dialog_info = await get_dialog_info(api, vk_chat_id, name_case='gen')
|
||||
markup = types.ForceReply(selective=False)
|
||||
if dialog_info['type'] == 'user':
|
||||
text = f'Сообщение для <i>{dialog_info["title"]}</i>'
|
||||
elif dialog_info['type'] == 'group':
|
||||
text = f'Сообщение сообществу <i>{dialog_info["title"]}</i>'
|
||||
else:
|
||||
text = f'Сообщение в диалог <i>{dialog_info["title"]}</i>'
|
||||
|
||||
tg_message = await bot.send_message(call.message.chat.id, text, reply_markup=markup,
|
||||
parse_mode=ParseMode.HTML)
|
||||
Message.objects.create(
|
||||
tg_chat=tg_message.chat.id,
|
||||
tg_id=tg_message.message_id,
|
||||
vk_chat=vk_chat_id
|
||||
)
|
||||
await bot.answer_callback_query(call.id)
|
||||
else:
|
||||
forward = Forward.objects.filter(tgchat=tgchat).first()
|
||||
vkchat = (await get_vk_chat(int(vk_chat_id)))[0]
|
||||
if forward:
|
||||
forward.vkchat = vkchat
|
||||
forward.save()
|
||||
else:
|
||||
Forward.objects.create(
|
||||
tgchat=tgchat,
|
||||
vkchat=vkchat,
|
||||
owner=user
|
||||
)
|
||||
markup = InlineKeyboardMarkup()
|
||||
markup.add(InlineKeyboardButton('Установить аватар и название', callback_data=f'setinfo{vkchat.cid}'))
|
||||
text = 'Чат успешно привязан. Я могу автоматически изменить название и установить аватар, сделай бота администратором и убедись в наличии прав на редактирование информации группы'
|
||||
if call.message.chat.type == 'group':
|
||||
text += '\n<b>Внимание!</b> Параметр <i>"All Members Are Administrators"</i> должен быть отключён и боту должна быть присвоена админка в отдельном порядке!'
|
||||
try:
|
||||
await bot.edit_message_text(text, call.message.chat.id, call.message.message_id, reply_markup=markup,
|
||||
parse_mode=ParseMode.HTML)
|
||||
except MessageNotModified:
|
||||
pass
|
||||
await bot.answer_callback_query(call.id)
|
||||
else:
|
||||
await bot.answer_callback_query(call.id, 'Вход не выполнен! Сперва нужно выполнить вход в ВК через бота',
|
||||
show_alert=True)
|
||||
|
||||
|
||||
@dp.callback_query_handler(func=lambda call: call and call.message and call.data and call.data == 'search')
|
||||
async def search_callback(call: types.CallbackQuery):
|
||||
vkuser = VkUser.objects.filter(owner__uid=call.from_user.id).count()
|
||||
if vkuser:
|
||||
markup = types.ForceReply(selective=False)
|
||||
await bot.send_message(call.message.chat.id, '<b>Поиск беседы 🔍</b>', parse_mode=ParseMode.HTML,
|
||||
reply_markup=markup)
|
||||
await bot.answer_callback_query(call.id, 'Поиск беседы 🔍')
|
||||
else:
|
||||
await bot.answer_callback_query(call.id, 'Вход не выполнен! Сперва нужно выполнить вход в ВК через бота',
|
||||
show_alert=True)
|
||||
|
||||
|
||||
@dp.message_handler(commands=['start'])
|
||||
async def send_welcome(msg: types.Message):
|
||||
if ALLOWED_USER_IDS:
|
||||
if str(msg.from_user.id) not in ALLOWED_USER_IDS.replace(' ','').split(','):
|
||||
await msg.reply('⛔️ Бот недоступен для Вашего аккаунта.\nУзнать Telegram ID - /id')
|
||||
return
|
||||
user, created = await update_user_info(msg.from_user)
|
||||
tgchat, tgchat_created = await update_chat_info(msg.chat)
|
||||
if not tgchat:
|
||||
existing_vkuser = VkUser.objects.filter(owner=user).count()
|
||||
if not existing_vkuser:
|
||||
link = 'https://oauth.vk.com/authorize?client_id={}&' \
|
||||
'display=page&redirect_uri=https://oauth.vk.com/blank.html&scope=friends,messages,offline,docs,photos,video,stories,audio' \
|
||||
'&response_type=token&v={}'.format(VK_APP_ID, API_VERSION)
|
||||
mark = InlineKeyboardMarkup()
|
||||
login = InlineKeyboardButton('ВХОД', url=link)
|
||||
mark.add(login)
|
||||
await msg.reply('Привет, этот бот поможет тебе общаться ВКонтакте, войди по кнопке ниже'
|
||||
' и отправь мне то, что получишь в адресной строке.',
|
||||
reply_markup=mark)
|
||||
else:
|
||||
await msg.reply('Вход уже выполнен!\n/stop для выхода.')
|
||||
else:
|
||||
markup = InlineKeyboardMarkup()
|
||||
me = await bot.me
|
||||
markup.add(InlineKeyboardButton('Перейти в бота', url=f'https://t.me/{me.username}?start=login'))
|
||||
await msg.reply('Залогиниться можно только через личный чат с ботом', reply_markup=markup)
|
||||
|
||||
|
||||
@dp.message_handler(commands=['stop'])
|
||||
async def stop_command(msg: types.Message):
|
||||
user, created = await update_user_info(msg.from_user)
|
||||
tgchat, tgchat_created = await update_chat_info(msg.chat)
|
||||
|
||||
existing_vkuser = VkUser.objects.filter(owner=user).first()
|
||||
if not existing_vkuser:
|
||||
await msg.reply('Вход не выполнен! Используй команду /start для входа')
|
||||
else:
|
||||
polling = next((task for task in TASKS if task['token'] == existing_vkuser.token), None)
|
||||
if polling:
|
||||
polling['task'].cancel()
|
||||
driver = DRIVERS.get(existing_vkuser.token, '')
|
||||
if driver:
|
||||
driver.close()
|
||||
del DRIVERS[existing_vkuser.token]
|
||||
existing_vkuser.delete()
|
||||
await msg.reply('Успешный выход!')
|
||||
|
||||
|
||||
@dp.message_handler(commands=['dialogs', 'd'])
|
||||
async def dialogs_command(msg: types.Message):
|
||||
if msg.chat.type == 'private':
|
||||
await handle_join(msg, text='Выберите диалог для быстрого ответа')
|
||||
else:
|
||||
await handle_join(msg, exclude=True)
|
||||
|
||||
|
||||
@dp.message_handler(commands=['read', 'r'])
|
||||
async def read_command(msg: types.Message):
|
||||
user, created = await update_user_info(msg.from_user)
|
||||
tgchat, tgchat_created = await update_chat_info(msg.chat)
|
||||
if await logged(msg.from_user.id, msg.message_id, msg.chat.id):
|
||||
vk_user = VkUser.objects.filter(owner=user).first()
|
||||
if msg.chat.type == 'private':
|
||||
if msg.reply_to_message:
|
||||
message_in_db = Message.objects.filter(tg_chat=msg.chat.id,
|
||||
tg_id=msg.reply_to_message.message_id).first()
|
||||
if message_in_db:
|
||||
vksession = VkSession(access_token=vk_user.token, driver=await get_driver(vk_user.token))
|
||||
api = API(vksession)
|
||||
await api('messages.markAsRead', peer_id=message_in_db.vk_chat)
|
||||
await bot.send_message(msg.chat.id, '<i>Диалог прочитан</i>', parse_mode=ParseMode.HTML)
|
||||
else:
|
||||
await msg.reply('Не знаю какой чат прочесть, нет информации в базе данных')
|
||||
else:
|
||||
forward = Forward.objects.filter(tgchat=tgchat).first()
|
||||
if forward:
|
||||
vksession = VkSession(access_token=vk_user.token, driver=await get_driver(vk_user.token))
|
||||
api = API(vksession)
|
||||
await api('messages.markAsRead', peer_id=forward.vkchat.cid)
|
||||
await bot.send_message(msg.chat.id, '<i>Диалог прочитан</i>', parse_mode=ParseMode.HTML)
|
||||
else:
|
||||
await msg.reply('Этот чат не привязан к диалогу ВКонтакте, для привязки используй команду /dialogs')
|
||||
|
||||
|
||||
@dp.message_handler(commands=['search', 's'])
|
||||
async def search_command(msg: types.Message):
|
||||
user, created = await update_user_info(msg.from_user)
|
||||
vkuser = VkUser.objects.filter(owner=user).count()
|
||||
if vkuser:
|
||||
markup = types.ForceReply(selective=False)
|
||||
await bot.send_message(msg.chat.id, '<b>Поиск беседы 🔍</b>', parse_mode=ParseMode.HTML,
|
||||
reply_markup=markup)
|
||||
else:
|
||||
await bot.answer_callback_query(msg, 'Вход не выполнен! Сперва нужно выполнить вход в ВК через бота',
|
||||
show_alert=True)
|
||||
|
||||
|
||||
@dp.message_handler(commands=['chat', 'chats'])
|
||||
async def chat_command(msg: types.Message):
|
||||
user, created = await update_user_info(msg.from_user)
|
||||
tgchat, tgchat_created = await update_chat_info(msg.chat)
|
||||
forwards = Forward.objects.filter(owner=user)
|
||||
if await logged(msg.from_user.id, msg.message_id, msg.chat.id):
|
||||
if forwards:
|
||||
vk_user = VkUser.objects.filter(owner=user).first()
|
||||
vksession = VkSession(access_token=vk_user.token, driver=await get_driver(vk_user.token))
|
||||
api = API(vksession)
|
||||
markup = InlineKeyboardMarkup()
|
||||
for forward in forwards:
|
||||
chat = await get_dialog_info(api, forward.vkchat.cid)
|
||||
markup.row(*[InlineKeyboardButton(chat['title'], callback_data=f'ping{forward.tgchat.cid}'),
|
||||
InlineKeyboardButton('❌', callback_data=f'deleteforward{forward.pk}')])
|
||||
msg_with_markup = await bot.send_message(msg.chat.id,
|
||||
'Список привязанных диалогов\nНажав на имя диалога, бот пинганёт Вас в соответствующем чате Telegram.\nНажав на "❌", привязка чата будет удалена и все сообщение из диалога ВКонтакте будут попадать напрямую к боту',
|
||||
reply_markup=markup)
|
||||
for row in markup.inline_keyboard:
|
||||
for button in range(len(row)):
|
||||
row[button] = row[button].to_python()
|
||||
MessageMarkup.objects.create(
|
||||
message_id=msg_with_markup.message_id,
|
||||
chat_id=msg_with_markup.chat.id,
|
||||
buttons=json.dumps(markup.inline_keyboard)
|
||||
)
|
||||
else:
|
||||
await bot.send_message(msg.chat.id,
|
||||
'У Вас нет связанных чатов. Чтобы привязать чат, добавьте бота в группу, а если бот уже добавлен - используйте команду /dialogs')
|
||||
|
||||
|
||||
@dp.message_handler(commands=['help'])
|
||||
async def help_command(msg: types.Message):
|
||||
user, created = await update_user_info(msg.from_user)
|
||||
tgchat, tgchat_created = await update_chat_info(msg.chat)
|
||||
HELP_MESSAGE = '/start - Логин в Вконтакте\n' \
|
||||
'/dialogs /d - Список диалогов\n' \
|
||||
'/read /r - Прочесть диалог ВКонтакте\n' \
|
||||
'/search /s - Поиск по диалогам\n' \
|
||||
'/chat - Список связанных чатов с диалогами ВКонтакте, привязать чат к диалогу можно добавив бота в группу\n' \
|
||||
'/stop - Выход из ВКонтакте\n' \
|
||||
'/help - Помощь' \
|
||||
'/id - Узнать Telegram ID'
|
||||
|
||||
await bot.send_message(msg.chat.id, HELP_MESSAGE, parse_mode=ParseMode.HTML)
|
||||
|
||||
@dp.message_handler(commands=['id'])
|
||||
async def id_command(msg: types.Message):
|
||||
user, created = await update_user_info(msg.from_user)
|
||||
await bot.send_message(msg.chat.id, 'Ваш ID: <code>{}</code>'.format(msg.from_user.id), parse_mode=ParseMode.HTML)
|
||||
|
||||
@dp.message_handler(content_types=['text'])
|
||||
async def handle_text(msg: types.Message):
|
||||
user, created = await update_user_info(msg.from_user)
|
||||
tgchat, tgchat_created = await update_chat_info(msg.chat)
|
||||
if msg.chat.type == 'private':
|
||||
m = oauth_link.search(msg.text)
|
||||
if m:
|
||||
if ALLOWED_USER_IDS:
|
||||
if str(msg.from_user.id) not in ALLOWED_USER_IDS.replace(' ', '').split(','):
|
||||
await msg.reply('⛔️ Бот недоступен для Вашего аккаунта.\nУзнать Telegram ID - /id')
|
||||
return
|
||||
await bot.send_chat_action(msg.from_user.id, ChatActions.TYPING)
|
||||
token = m.group(2)
|
||||
if not VkUser.objects.filter(token=token).exists():
|
||||
try:
|
||||
session = VkSession(access_token=token, driver=await get_driver(token))
|
||||
api = API(session)
|
||||
vkuserinfo = await api('account.getProfileInfo', name_case='gen')
|
||||
vkuser, vkuser_created = VkUser.objects.update_or_create(
|
||||
defaults={'token': token, 'is_polling': True}, owner=user)
|
||||
existing_polling = next((task for task in TASKS if task['token'] == vkuser.token), None)
|
||||
if existing_polling:
|
||||
existing_polling['task'].cancel()
|
||||
driver = DRIVERS.get(vkuser.token, '')
|
||||
if driver:
|
||||
driver.close()
|
||||
del DRIVERS[vkuser.token]
|
||||
refreshed_token = await refresh_token(vkuser)
|
||||
TASKS.append({'token': vkuser.token, 'task': asyncio.ensure_future(vk_polling(vkuser))})
|
||||
logged_in = await msg.reply(
|
||||
'Вход выполнен в аккаунт {} {}!\n[Использование](https://akentev.com/tgvkbot/usage/)'.format(
|
||||
vkuserinfo['first_name'], vkuserinfo.get('last_name', '')), parse_mode='Markdown')
|
||||
if refreshed_token:
|
||||
await logged_in.reply('*Вам доступна музыка 🎵*', parse_mode='Markdown')
|
||||
except VkAuthError:
|
||||
await msg.reply('Неверная ссылка, попробуйте ещё раз!')
|
||||
else:
|
||||
await msg.reply('Вход уже выполнен!\n/stop для выхода.')
|
||||
return
|
||||
if await logged(msg.from_user.id, msg.message_id, msg.chat.id):
|
||||
if msg.reply_to_message and msg.reply_to_message.text == 'Поиск беседы 🔍':
|
||||
if msg.chat.type == 'private' or not Message.objects.filter(tg_id=msg.reply_to_message.message_id,
|
||||
tg_chat=msg.reply_to_message.chat.id).exists():
|
||||
await search_dialogs(msg, user)
|
||||
return
|
||||
vk_user = VkUser.objects.filter(owner=user).first()
|
||||
forward = Forward.objects.filter(tgchat=tgchat).first()
|
||||
forward_messages_exists, message = await is_forwarding(msg.text)
|
||||
message_options = await generate_send_options(msg, forward, forward_messages_exists, message)
|
||||
if message_options != {}:
|
||||
vk_message = await vk_sender(vk_user.token, msg, **message_options)
|
||||
if not vk_message:
|
||||
await msg.reply('<b>Произошла ошибка при отправке</b>', parse_mode=ParseMode.HTML)
|
||||
|
||||
|
||||
@dp.message_handler(content_types=['contact'])
|
||||
async def handle_contact(msg: types.Message):
|
||||
new_text = msg.contact.first_name
|
||||
if msg.contact.last_name:
|
||||
new_text += ' ' + msg.contact.last_name
|
||||
new_text += '\n'
|
||||
new_text += msg.contact.phone_number
|
||||
msg.text = new_text
|
||||
await handle_text(msg)
|
||||
|
||||
|
||||
@dp.message_handler(content_types=['photo', 'sticker'])
|
||||
async def handle_photo(msg: types.Message):
|
||||
user, user_created = await update_user_info(msg.from_user)
|
||||
tgchat, tgchat_created = await update_chat_info(msg.chat)
|
||||
if await logged(msg.from_user.id, msg.message_id, msg.chat.id):
|
||||
vk_user = VkUser.objects.filter(owner=user).first()
|
||||
forward = Forward.objects.filter(tgchat=tgchat).first()
|
||||
forward_messages_exists, message = await is_forwarding(msg.caption)
|
||||
message_options = await generate_send_options(msg, forward, forward_messages_exists, message)
|
||||
if msg.content_type == 'photo':
|
||||
file_id = msg.photo[-1].file_id
|
||||
elif msg.content_type == 'sticker':
|
||||
if msg.sticker.to_python()['is_animated']:
|
||||
file_id = msg.sticker.thumb.file_id
|
||||
else:
|
||||
file_id = msg.sticker.file_id
|
||||
if message_options:
|
||||
message_options['attachment'] = await upload_attachment(msg, vk_user, file_id, message_options['peer_id'],
|
||||
attachment_type='photo',
|
||||
upload_field='photo',
|
||||
upload_method='photos.getMessagesUploadServer',
|
||||
on_server_field='photo',
|
||||
save_method='photos.saveMessagesPhoto')
|
||||
if message_options['attachment']:
|
||||
vk_message = await vk_sender(vk_user.token, msg, **message_options)
|
||||
if not vk_message:
|
||||
await msg.reply('<b>Произошла ошибка при отправке</b>', parse_mode=ParseMode.HTML)
|
||||
else:
|
||||
await msg.reply('<b>Ошибка при загрузке файла. Сообщение не отправлено!</b>', parse_mode=ParseMode.HTML)
|
||||
|
||||
|
||||
@dp.message_handler(content_types=['document', 'voice', 'audio'])
|
||||
async def handle_documents(msg: types.Message):
|
||||
user, user_created = await update_user_info(msg.from_user)
|
||||
tgchat, tgchat_created = await update_chat_info(msg.chat)
|
||||
if await logged(msg.from_user.id, msg.message_id, msg.chat.id):
|
||||
vk_user = VkUser.objects.filter(owner=user).first()
|
||||
if tgchat:
|
||||
forward = Forward.objects.filter(tgchat=tgchat).first()
|
||||
else:
|
||||
forward = None
|
||||
forward_messages_exists, message = await is_forwarding(msg.caption)
|
||||
message_options = await generate_send_options(msg, forward, forward_messages_exists, message)
|
||||
file_id = getattr(msg, msg.content_type).file_id
|
||||
if message_options:
|
||||
upload_attachment_options = {
|
||||
'attachment_type': 'doc',
|
||||
'upload_field': 'file',
|
||||
'upload_method': 'docs.getUploadServer',
|
||||
'on_server_field': 'file',
|
||||
'save_method': 'docs.save',
|
||||
}
|
||||
if hasattr(getattr(msg, msg.content_type), 'file_name') and getattr(msg, msg.content_type).file_name:
|
||||
upload_attachment_options['title'] = getattr(msg, msg.content_type).file_name
|
||||
|
||||
if msg.content_type == 'voice':
|
||||
upload_attachment_options['upload_type'] = 'audio_message'
|
||||
|
||||
# https://vk.com/wall-1_395554
|
||||
# if msg.content_type == 'sticker':
|
||||
# if msg.sticker.to_python()['is_animated']:
|
||||
# file_id = msg.sticker.thumb.file_id
|
||||
# upload_attachment_options['upload_type'] = 'graffiti'
|
||||
# upload_attachment_options['rewrite_name'] = True
|
||||
# upload_attachment_options['default_name'] = 'graffiti.png'
|
||||
|
||||
if msg.content_type == 'audio':
|
||||
audioname = ''
|
||||
if msg.audio.performer and msg.audio.title:
|
||||
audioname += msg.audio.performer + ' - ' + msg.audio.title
|
||||
elif msg.audio.performer:
|
||||
audioname += msg.audio.performer
|
||||
elif msg.audio.title:
|
||||
audioname += msg.audio.title
|
||||
else:
|
||||
audioname = f'tgvkbot_audio_{file_id}'
|
||||
upload_attachment_options['title'] = audioname
|
||||
|
||||
message_options['attachment'] = await upload_attachment(msg, vk_user, file_id, message_options['peer_id'],
|
||||
**upload_attachment_options)
|
||||
if message_options['attachment']:
|
||||
vk_message = await vk_sender(vk_user.token, msg, **message_options)
|
||||
if not vk_message:
|
||||
await msg.reply('<b>Произошла ошибка при отправке</b>', parse_mode=ParseMode.HTML)
|
||||
else:
|
||||
await msg.reply('<b>Ошибка при загрузке файла. Сообщение не отправлено!</b>', parse_mode=ParseMode.HTML)
|
||||
|
||||
|
||||
@dp.message_handler(content_types=['video', 'video_note'])
|
||||
async def handle_videos(msg: types.Message):
|
||||
user, user_created = await update_user_info(msg.from_user)
|
||||
tgchat, tgchat_created = await update_chat_info(msg.chat)
|
||||
if await logged(msg.from_user.id, msg.message_id, msg.chat.id):
|
||||
vk_user = VkUser.objects.filter(owner=user).first()
|
||||
if tgchat:
|
||||
forward = Forward.objects.filter(tgchat=tgchat).first()
|
||||
else:
|
||||
forward = None
|
||||
forward_messages_exists, message = await is_forwarding(msg.caption)
|
||||
message_options = await generate_send_options(msg, forward, forward_messages_exists, message)
|
||||
file_id = getattr(msg, msg.content_type).file_id
|
||||
if message_options:
|
||||
upload_attachment_options = {
|
||||
'attachment_type': 'video',
|
||||
'upload_field': 'video_file',
|
||||
'upload_method': 'video.save',
|
||||
}
|
||||
if hasattr(getattr(msg, msg.content_type), 'file_name') and getattr(msg, msg.content_type).file_name:
|
||||
upload_attachment_options['title'] = getattr(msg, msg.content_type).file_name
|
||||
|
||||
message_options['attachment'] = await upload_attachment(msg, vk_user, file_id, message_options['peer_id'],
|
||||
**upload_attachment_options)
|
||||
if message_options['attachment']:
|
||||
vk_message = await vk_sender(vk_user.token, msg, **message_options)
|
||||
if not vk_message:
|
||||
await msg.reply('<b>Произошла ошибка при отправке</b>', parse_mode=ParseMode.HTML)
|
||||
else:
|
||||
await msg.reply('<b>Ошибка при загрузке файла. Сообщение не отправлено!</b>', parse_mode=ParseMode.HTML)
|
||||
|
||||
|
||||
@dp.message_handler(content_types=['new_chat_members'], func=is_bot_in_iterator)
|
||||
async def handle_join(msg: types.Message, edit=False, chat_id=None, message_id=None, text='', exclude=False):
|
||||
user, user_created = await update_user_info(msg.from_user)
|
||||
tgchat, tgchat_created = await update_chat_info(msg.chat)
|
||||
forward = Forward.objects.filter(tgchat=tgchat).first()
|
||||
try:
|
||||
await bot.send_chat_action(msg.chat.id, 'typing')
|
||||
except:
|
||||
return
|
||||
vk_user = VkUser.objects.filter(owner=user).first()
|
||||
pages = None
|
||||
reply_to_message_id = None
|
||||
markup = None
|
||||
if vk_user:
|
||||
if forward:
|
||||
text = text or '<i>Этот чат уже привязан к диалогу ВКонтакте, Вы можете выбрать новый диалог</i>'
|
||||
else:
|
||||
text = text or '<i>Выберите диалог ВКонтакте к которому будет привязан этот чат</i>'
|
||||
markup = InlineKeyboardMarkup()
|
||||
excluded_ids = []
|
||||
if exclude:
|
||||
excluded_ids = [forward.vkchat.cid for forward in Forward.objects.filter(owner=user)]
|
||||
pages = await get_dialogs(vk_user.token, excluded_ids)
|
||||
if pages:
|
||||
for buttons_row in pages[0]:
|
||||
markup.row(*buttons_row)
|
||||
await get_pages_switcher(markup, 0, pages)
|
||||
else:
|
||||
me = await bot.me
|
||||
if msg.chat.type == 'private':
|
||||
text = 'Вход не выполнен! /start для входа'
|
||||
reply_to_message_id = msg.message_id
|
||||
else:
|
||||
text = '<i>Вход не выполнен! Сперва нужно выполнить вход в ВК через бота</i>'
|
||||
markup = InlineKeyboardMarkup()
|
||||
markup.add(InlineKeyboardButton('ВХОД', url=f'https://t.me/{me.username}?start=login'),
|
||||
InlineKeyboardButton('✅ Я залогинился', callback_data=f'logged-{msg.from_user.id}'))
|
||||
if edit:
|
||||
msg_with_markup = await bot.edit_message_text(text=text, chat_id=chat_id, message_id=message_id,
|
||||
reply_markup=markup, parse_mode=ParseMode.HTML)
|
||||
else:
|
||||
msg_with_markup = await bot.send_message(msg.chat.id, text=text, reply_markup=markup, parse_mode=ParseMode.HTML,
|
||||
reply_to_message_id=reply_to_message_id)
|
||||
if pages:
|
||||
for page in pages:
|
||||
for row in page:
|
||||
for button in range(len(row)):
|
||||
row[button] = row[button].to_python()
|
||||
|
||||
MessageMarkup.objects.create(
|
||||
message_id=msg_with_markup.message_id,
|
||||
chat_id=msg_with_markup.chat.id,
|
||||
buttons=json.dumps(pages)
|
||||
)
|
||||
|
||||
|
||||
@dp.message_handler(content_types=types.ContentType.ANY, func=lambda msg: msg.group_chat_created is True)
|
||||
async def handle_new_group(msg: types.Message):
|
||||
await handle_join(msg)
|
||||
|
||||
|
||||
@dp.message_handler(content_types=types.ContentType.ANY, func=lambda message: message.migrate_to_chat_id)
|
||||
async def handle_chat_migration(msg: types.Message):
|
||||
# Юзеру сначала нужно выбрать чат для привязки, а уже ПОТОМ делать миграцию. Иначе старый chatid останется на иналйнкнопках
|
||||
forwards = Forward.objects.filter(tgchat__cid=msg.chat.id)
|
||||
for forward in forwards:
|
||||
forward.tgchat.cid = msg.migrate_to_chat_id
|
||||
forward.tgchat.save()
|
||||
|
||||
markup = InlineKeyboardMarkup()
|
||||
markup.add(InlineKeyboardButton('Установить аватар и название', callback_data=f'setinfo{forward.vkchat.cid}'))
|
||||
await bot.send_message(msg.migrate_to_chat_id,
|
||||
text='Отлично! Теперь можно установить аватар и название. Если кнопка выше не работает, то воспользуйтесь этой.',
|
||||
reply_markup=markup)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
TASKS = vk_polling_tasks()
|
||||
asyncio.gather(*[task['task'] for task in TASKS])
|
||||
|
||||
executor.start_polling(dp)
|
43
usage/README.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Использование
|
||||
|
||||
Команды:
|
||||
|
||||
`/start` - Логин ВКонтакте.
|
||||
|
||||
`/dialogs` - Вывод меню со списком диалогов.
|
||||
|
||||
`/search Иван` - Поиск диалогов по запросу (в данном случае `Иван`), при вызове команды без запроса бот автоматически спросит что искать, следующим шагом можно будет ввести запрос.
|
||||
|
||||
Поддерживаются части слов, транслит, а так же абракадабра, которую вы ненароком напишете забыв сменить раскладку.
|
||||
|
||||
`/chat` - Бот сообщает о том. в каком чате вы сейчас находитесь, все дальнеёшие сообщения без _Reply_ будут отправляться именно в этот чат.
|
||||
|
||||
`/leave` - Вы покидаете текущий чат, дальнейшее общение происходит только с помощью _Reply_ (или при выборе чата из `/search`, `/dialogs`)
|
||||
|
||||
`/stop` - Выход из ВКонтакте, бот полностью стирает всю информацию, для дальнейшей работы требуется повторная инициализация `/start`
|
||||
|
||||
Бот отправляет личные сообщения в таком виде:
|
||||
|
||||
<p align="center"><img src ="/tgvkbot/assets/rsz_privatemessage.jpg" /></p>
|
||||
|
||||
Для сообщений из групповых чатов будет указываться чат после имени отправителя
|
||||
|
||||
<p align="center"><img src ="/tgvkbot/assets/rsz_groupmessage.jpg" /></p>
|
||||
|
||||
Ответ в чат откуда пришло сообщение (ЛС/Беседа) производится с помощью функции _Reply_ в самом _Telegram_
|
||||
|
||||
<p align="center"><img src ="/tgvkbot/assets/rsz_reply.jpg" /></p>
|
||||
|
||||
Однако, не нужно делать _Reply_ если вы находитесь в определённом чате (проверить можно командой `/chat`)
|
||||
|
||||
**Пример:** Если вы находитесь в чате А и вам пришло сообщение из чата Б, то сделав _Reply_ на сообщение из чата Б, вы отправите сообщение в чат Б и **сообщение отправится только в чат Б, без дублей в чат А!** При этом вы останетесь в чате А и сможете дальше продолжить общаться в чате А без использования _Reply_.
|
||||
|
||||
Тем самым **функция Reply имеет приоритет!**
|
||||
|
||||
Для "пересылки" сообшения написанного ВК в сам чат ВК, нужно написать `!` первым символом в сообщении (подписи к фото/документу) отправленного из Telegram.
|
||||
<p align="center"><img src ="/tgvkbot/assets/rsz_forwardtg.jpg" /></p>
|
||||
<p align="center"><img src ="/tgvkbot/assets/forwardvk.PNG" /></p>
|
||||
|
||||
По возможности бот будет отправлять вложения из ВК как документы, или давать ссылку с превью. Если же это невозможно, бот даст прямую ссылку на файл. <del>Исключение на получение <a href="https://vc.ru/n/vk-music-noapi" target="_blank">аудио</a>, для них будет даваться ссылка на раздел аудио VK.</del> Отправка музыкальных файлов из Telegram **поддерживается**, однако вся музыка конвертируется в расширение `.aac` GIF из Telegram отправляются как видео VK.
|
||||
|
||||
- [Канал в Telegram](https://t.me/tg_vk) - обновления, поддержа
|