diff --git a/assets/cert.sh b/assets/cert.sh new file mode 100644 index 0000000..1842917 --- /dev/null +++ b/assets/cert.sh @@ -0,0 +1 @@ +openssl req -newkey rsa:2048 -sha256 -nodes -keyout cert.key -x509 -days 365 -out cert.pem -subj /C=US/ST=New York/L=Brooklyn/O=Example Brooklyn Company/CN=hexor.ru diff --git a/assets/main.db.sql b/assets/main.db.sql new file mode 100644 index 0000000..c2c75da --- /dev/null +++ b/assets/main.db.sql @@ -0,0 +1,34 @@ +BEGIN TRANSACTION; +-- DROP TABLE IF EXISTS `word`; +CREATE TABLE IF NOT EXISTS `word` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `word` TEXT UNIQUE +); +-- DROP TABLE IF EXISTS `user`; +CREATE TABLE IF NOT EXISTS `user` ( + `id` INTEGER NOT NULL UNIQUE, + `username` TEXT NOT NULL, + `first_name` INTEGER NOT NULL, + `last_name` INTEGER NOT NULL, + `date` INTEGER NOT NULL, + PRIMARY KEY(`id`) +); +-- DROP TABLE IF EXISTS `relations`; +CREATE TABLE IF NOT EXISTS `relations` ( + `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + `word_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `conf_id` INTEGER NOT NULL, + `date` INTEGER NOT NULL, + FOREIGN KEY(`conf_id`) REFERENCES `conf`(`id`), + FOREIGN KEY(`word_id`) REFERENCES `word`(`id`) ON DELETE CASCADE, + FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) +); +-- DROP TABLE IF EXISTS `conf`; +CREATE TABLE IF NOT EXISTS `conf` ( + `id` NUMERIC NOT NULL UNIQUE, + `title` TEXT, + `date` INTEGER NOT NULL, + PRIMARY KEY(`id`) +); +COMMIT; diff --git a/assets/settings.ini b/assets/settings.ini new file mode 100644 index 0000000..6256821 --- /dev/null +++ b/assets/settings.ini @@ -0,0 +1,3 @@ +[bot] +telegram_key = 530043167:AAHkasdasdGHvmNojcD2mUXUU_5z8lKs0 +telegram_api = https://api.telegram.org/ \ No newline at end of file diff --git a/assets/stop-word.ru b/assets/stop-word.ru new file mode 100644 index 0000000..0791455 --- /dev/null +++ b/assets/stop-word.ru @@ -0,0 +1,423 @@ +а +в +г +е +ж +и +к +м +о +с +т +у +я +бы +stat +вообще +/stat +во +вы +да +до +ее +ей +ею +её +же +за +из +им +их +ли +мы +на +не +ни +но +ну +нх +об +он +от +по +со +та +те +то +ту +ты +уж +без +был +вам +вас +ваш +вон +вот +все +всю +вся +всё +где +год +два +две +дел +для +его +ему +еще +ещё +или +ими +имя +как +кем +ком +кто +лет +мне +мог +мож +мои +мой +мор +моя +моё +над +нам +нас +наш +нее +ней +нем +нет +нею +неё +них +оба +она +они +оно +под +пор +при +про +раз +сам +сих +так +там +тем +тех +том +тот +тою +три +тут +уже +чем +что +эта +эти +это +эту +алло +буду +будь +бывь +была +были +было +быть +вами +ваша +ваше +ваши +ведь +весь +вниз +всем +всех +всею +года +году +даже +двух +день +если +есть +зато +кого +кому +куда +лишь +люди +мало +меля +меня +мимо +мира +мной +мною +мочь +надо +нами +наша +наше +наши +него +нему +ниже +ними +один +пока +пора +пять +рано +сама +сами +само +саму +свое +свои +свою +себе +себя +семь +стал +суть +твой +твоя +твоё +тебе +тебя +теми +того +тоже +тому +туда +хоть +хотя +чаще +чего +чему +чтоб +чуть +этим +этих +этой +этом +этот +более +будем +будет +будто +будут +вверх +вдали +вдруг +везде +внизу +время +всего +всеми +всему +всюду +давно +даром +долго +друго +занят +затем +зачем +здесь +иметь +какая +какой +когда +кроме +лучше +между +менее +много +могут +может +можно +можхо +назад +низко +нужно +одной +около +опять +очень +перед +позже +после +потом +почти +пятый +разве +рядом +самим +самих +самой +самом +своей +своих +сеаой +снова +собой +собою +такая +также +такие +такое +такой +тобой +тобою +тогда +тысяч +уметь +часто +через +чтобы +шесть +этими +этого +этому +близко +больше +будете +будешь +бывает +важная +важное +важные +важный +вокруг +восемь +всегда +второй +далеко +дальше +девять +десять +должно +другая +другие +других +другое +другой +занята +занято +заняты +значит +именно +иногда +каждая +каждое +каждые +каждый +кругом +меньше +начала +нельзя +нибудь +никуда +ничего +обычно +однако +одного +отсюда +первый +потому +почему +просто +против +раньше +самими +самого +самому +своего +сейчас +сказал +совсем +теперь +только +третий +хорошо +хотеть +хочешь +четыре +шестой +восьмой +впрочем +времени +говорил +говорит +девятый +десятый +кажется +конечно +которая +которой +которые +который +которых +наверху +наконец +недавно +немного +нередко +никогда +однажды +посреди +сегодня +седьмой +сказала +сказать +сколько +слишком +сначала +спасибо +двадцать +довольно +которого +наиболее +недалеко +особенно +отовсюду +двадцатый +миллионов +несколько +прекрасно +процентов +четвертый +двенадцать +непрерывно +пожалуйста +пятнадцать +семнадцать +тринадцать +двенадцатый +одиннадцать +пятнадцатый +семнадцатый +тринадцатый +шестнадцать +восемнадцать +девятнадцать +одиннадцатый +четырнадцать +шестнадцатый +восемнадцатый +девятнадцатый +действительно +четырнадцатый +многочисленная +многочисленное +многочисленные +многочисленный +ага \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..087a298 --- /dev/null +++ b/database.py @@ -0,0 +1,96 @@ +import datetime as dt + +class DataBase: + def __init__(self, basefile, scheme): + import sqlite3 + try: + self.conn = sqlite3.connect(basefile, check_same_thread=False) + except: + print('Could not connect to DataBase.') + return None + with open(scheme, 'r') as scheme_sql: + sql = scheme_sql.read() + if self.conn is not None: + try: + cursor = self.conn.cursor() + cursor.executescript(sql) + except: + print('Could not create scheme.') + else: + print("Error! cannot create the database connection.") + print('DB created.') + + def execute(self, sql): + cursor = self.conn.cursor() + cursor.execute(sql) + self.conn.commit() + return cursor.fetchall() + + def save_word(self, word): + sql = "INSERT OR IGNORE INTO word('word') VALUES ('%s')" % word + self.execute(sql) + sql = "SELECT id FROM word WHERE word = '%s'" % word + return(self.execute(sql)[0][0]) + + def add_user(self, + username, + user_id, + first_name, + last_name): + date = int(dt.datetime.now().strftime("%s")) + sql = """INSERT OR IGNORE INTO + user('id', 'username', 'first_name', 'last_name', 'date') + VALUES ('%s','%s','%s','%s','%s')""" % ( + user_id, + username, + first_name, + last_name, + date + ) + self.execute(sql) + + def add_conf(self, id, title): + date = int(dt.datetime.now().strftime("%s")) + sql = """INSERT OR IGNORE INTO + conf('id', 'title', 'date') + VALUES ('%s','%s','%s')""" % ( + id, + title, + date + ) + self.execute(sql) + + def add_relation(self, word, user_id, conf_id): + word_id = self.save_word(word) + date = int(dt.datetime.now().strftime("%s")) + sql = """INSERT OR IGNORE INTO + relations('word_id', 'user_id', 'conf_id', 'date') + VALUES ('%s','%s','%s','%s')""" % ( + word_id, + user_id, + conf_id, + date + ) + self.execute(sql) + + def get_top(self, user_id, conf_id, limit=10): + sql= """ + SELECT w.word, COUNT(*) as count FROM relations r + LEFT JOIN word w ON w.id = r.word_id + LEFT JOIN `user` u ON u.id = r.user_id + WHERE u.id = '%s' AND + r.conf_id = '%s' + GROUP BY w.word + ORDER BY count DESC + LIMIT %s + """ % ( + user_id, + conf_id, + limit + ) + result = self.execute(sql) + return(result) + + + def close(self): + self.conn.close() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..f7fc761 --- /dev/null +++ b/main.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from webhook import WebHook +import settings +import signal +import sys + +# catch ctrl+c +def signal_handler(signal, frame): + print('Exiting...') + settings.db.close() + sys.exit(0) +signal.signal(signal.SIGINT, signal_handler) + +wh = WebHook( + certfile = 'assets/cert.pem', + keyfile='assets/cert.key', + port=8080) + +wh.serve() \ No newline at end of file diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..d98992b --- /dev/null +++ b/settings.py @@ -0,0 +1,15 @@ +from worker import MessageWorker +from database import DataBase +from configparser import ConfigParser + +global parser +parser = ConfigParser() +parser.read('assets/settings.ini') + +global db +db = DataBase( + basefile='main.db', + scheme='assets/main.db.sql') + +global worker +worker = MessageWorker(db = db) diff --git a/webhook.py b/webhook.py new file mode 100644 index 0000000..02854a4 --- /dev/null +++ b/webhook.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from http.server import HTTPServer,SimpleHTTPRequestHandler,CGIHTTPRequestHandler +from socketserver import BaseServer +import ssl +import json +import settings + +# fuckin dirty hack. idk the best way to inherit return func into +# RequestHandler class + +class RequestHandler(SimpleHTTPRequestHandler): + def __init__(self, + request, + client_address, + server): + self.worker = settings.worker + super(RequestHandler, self).__init__( + request=request, + client_address=client_address, + server=server) + + def do_POST(self): + """Serve a POST request.""" + self.send_response(200) + self.send_header('Content-Type', 'text/html') + self.end_headers() + + length = self.headers.get('content-length') + post_body = self.rfile.read(int(length)) + msg = json.loads(post_body.decode("utf-8")) + msg2 = post_body.decode("utf-8") + fh = open("log.json","a+") + fh.write(msg2) + fh.close() + self.worker.handleUpdate(msg) + + def do_GET(self): + pass + + + +class WebHook: + def __init__(self, + certfile, + keyfile, + address = '0.0.0.0', + port=8443, + RequestHandler=RequestHandler): + + self.httpd = HTTPServer((address, port), RequestHandler) + self.httpd.socket = ssl.wrap_socket (self.httpd.socket, + certfile=certfile, + keyfile=keyfile, + server_side=True) + + def serve(self): + try: + self.httpd.serve_forever() + except KeyboardInterrupt: + pass + finally: + # Clean-up server (close socket, etc.) + self.httpd.server_close() diff --git a/worker.py b/worker.py new file mode 100644 index 0000000..51f192c --- /dev/null +++ b/worker.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from string import punctuation +import subprocess +from database import DataBase +import os +import urllib.request +from urllib.parse import urlencode +import settings + +class MessageWorker: + def __init__(self, db, stop_words = 'assets/stop-word.ru'): + self.stop_words = stop_words + self.db = db + + def handleUpdate(self, msg): + try: + if msg['message']['text'] == '/stat': + conf_id = msg['message']['chat']['id'] + user_id = msg['message']['from']['id'] + chat_title = msg['message']['chat']['title'] + self.db.add_conf(conf_id, chat_title) + + message = """Here is your top:\n""" + top = self.db.get_top( + user_id=user_id, + conf_id=conf_id + ) + for word in top: + message += '*%s*: %s\n' % (word[1], word[0]) + self.send(id=conf_id, msg=message) + return True + except: + return False + try: + text = msg['message']['text'] + username = msg['message']['from']['username'] + try: + last_name = msg['message']['from']['last_name'] + except: + last_name = '_null' + try: + first_name = msg['message']['from']['first_name'] + except: + first_name = '_null' + user_id = msg['message']['from']['id'] + chat_id = msg['message']['chat']['id'] + chat_title = msg['message']['chat']['title'] + #print(self.clean_text(text)) + except: + return False + + collection = self.clean_text(text) + + self.db.add_user(username, + user_id, + first_name, + last_name) + + self.db.add_conf(chat_id, chat_title) + + for word in collection: + self.db.add_relation(word=word, user_id=user_id, conf_id=chat_id) + + + def clean_text(self, s): + file = open(self.stop_words, 'rt') + sw = file.read().split('\n') + file.close() + # dirty hack with dat fucking file + fh = open("tmp.txt","w") + fh.write(s) + fh.close() + cmd = "./assets/mystem -nlwd < tmp.txt" + ps = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE) + output = ps.communicate()[0] + os.remove("tmp.txt") + # end of the fuckin' dirty hack + s = output.decode('utf8') + s = s.replace('?', '') + s = s.split('\n') + collection = [] + for word in s: + if len(word) >2: + if word not in sw: + collection.append(word) + else: + pass + else: + pass + return collection + + def send(self, id, msg): + url = settings.parser.get('bot', 'telegram_api') + \ + 'bot'+ settings.parser.get('bot', 'telegram_key') \ + + '/sendMessage' + post_fields = { + 'text': msg, + 'chat_id': id, + 'parse_mode': 'Markdown', + 'disable_web_page_preview': 1 + } + urllib.request.Request(url, urlencode(post_fields).encode()) + request = urllib.request.Request(url, urlencode(post_fields).encode()) + json = urllib.request.urlopen(request).read().decode() + return json