Compare commits

...

No commits in common. "master" and "gh-pages" have entirely different histories.

44 changed files with 808 additions and 2826 deletions

View File

@ -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

View File

@ -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
View File

@ -1,11 +0,0 @@
.idea/
__pycache__/
data/migrations/*
!data/migrations/__init__.py
config.py
*.pem
env_file
openssl_config
venv/
commands/
.venv

View File

@ -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
View File

@ -0,0 +1,16 @@
Copyright (c) 2017 А. Сергей
Данная лицензия разрешает лицам, получившим копию данного программного обеспечения и сопутствующей документации
(в дальнейшем именуемыми «Программное Обеспечение»), безвозмездно использовать Программное Обеспечение без ограничений,
включая неограниченное право на использование, копирование, изменение, слияние, публикацию,
а также лицам, которым предоставляется данное Программное Обеспечение, при соблюдении следующих условий:
Указанное выше уведомление об авторском праве и данные условия должны быть включены во все копии или значимые части
данного Программного Обеспечения.
ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ,
ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ,
СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ, НО НЕ ОГРАНИЧИВАЯСЬ ИМИ.
НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО КАКИМ-ЛИБО ИСКАМ,
ЗА УЩЕРБ ИЛИ ПО ИНЫМ ТРЕБОВАНИЯМ, В ТОМ ЧИСЛЕ, ПРИ ДЕЙСТВИИ КОНТРАКТА, ДЕЛИКТЕ ИЛИ ИНОЙ СИТУАЦИИ,
ВОЗНИКШИМ ИЗ-ЗА ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫХ ДЕЙСТВИЙ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.

View File

@ -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
View File

@ -0,0 +1,4 @@
theme: jekyll-theme-slate
title: tgvkbot
description: 'Общайтесь ВКонтакте с помощью Telegram бота'
google_analytics: UA-101930895-1

View 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>

View 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
View 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
View File

@ -0,0 +1,5 @@
---
---
@import "jekyll-theme-slate";
@import "style.css";

BIN
assets/dialogs.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
assets/documents.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
assets/forwardvk.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/groupmessage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/privatemessage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
assets/rsz_dialogs (1).jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
assets/rsz_forwardtg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/rsz_groupmessage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
assets/rsz_reply.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
assets/rsz_search (1).jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
assets/rsz_sunset (1).jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

1
assets/sample.txt Normal file
View File

@ -0,0 +1 @@

BIN
assets/sticker.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
assets/sunset.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
assets/vk_music.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

116
bot.py
View File

@ -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

View File

@ -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)

View File

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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
View 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.
Нажав на кнопку ниже вы попадёте на страницу развёртывания, где будут поля:
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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)

View File

@ -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

View File

@ -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⚠️ Настройка переменных окружуения была прервана!')

View File

@ -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',
)

View File

@ -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
View 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) - обновления, поддержа

File diff suppressed because it is too large Load Diff