diff --git a/.gitignore b/.gitignore index 4d538a7..504c397 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *.pyc +*.swp pycache +tags myconfig.py virtualenv .idea diff --git a/Pipfile b/Pipfile index d03f378..16ec328 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ name = "pypi" [packages] python-telegram-bot = "==11.1.0" prettyconf = "==2.0.1" +pygithub = "*" [dev-packages] "flake8" = "*" diff --git a/bot.py b/bot.py index a7d1353..b17ccf8 100644 --- a/bot.py +++ b/bot.py @@ -8,23 +8,41 @@ import config import utils +from dba import connect +from dba import save_chat +from dba import load_all_chats + logger = logging.getLogger('bot') + def command_start(bot, update): logger.info('Received command /start') - bot.send_message(chat_id=update.message.chat_id, text=config.BOT_GREETING) + chat = update.message.chat + db = connect(config.DB_NAME) + is_new = save_chat(db, chat.id, chat.type, chat.title) + if is_new: + bot.send_message( + chat_id=update.message.chat_id, + text=config.BOT_GREETING, + parse_mode='Markdown', + ) def command_help(bot, update): logger.info('Received command /help') bot.send_message( chat_id=update.message.chat_id, - text="Available commands:\n" - " - /start - start intereaction with the bot\n" - " - /help - Show commands\n" - " - /status - Show status and alive time\n" + text='\n'.join([ + "Available commands:", + " - `/start` - start intereaction with the bot", + " - `/help` - Show commands", + " - `/status` - Show status and alive time", + " - `/settings` - Show current settings", + " - `/debug` - Show debug information for developers", + ]), + parse_mode='Markdown', ) @@ -32,8 +50,51 @@ def command_status(bot, update): logger.info('bot asked to execute /status commamd') bot.send_message( chat_id=update.message.chat_id, - text=f'Status is OK, running since {utils.since()}', - ) + text=f'Status is **OK**, running since {utils.since()}', + parse_mode='Markdown', + ) + + +def command_settings(bot, update): + logger.info('Received command /settings') + chat = update.message.chat + if config.VERBOSITY < 1.0: + verbosity_level = f'{round(config.VERBOSITY*100.0, 2)}%' + else: + verbosity_level = 'Palicoso' + bot.send_message( + chat_id=update.message.chat_id, + text="\n".join([ + f"Settings for {chat.title}:", + f" - Database: `{config.DB_NAME}`", + f" - Verbosity level: `{verbosity_level}`", + f" - Log level: `{config.LOG_LEVEL}`", + f" - Poll interval time (in seconds): **{config.POLL_INTERVAL}**", + f" - Repos (**0**)", + ]), + parse_mode='Markdown', + ) + + +def command_debug(bot, update): + logger.info('Received command /debug') + chat = update.message.chat + db = connect(config.DB_NAME) + all_chats = load_all_chats(db) + buff = [ + "Chat:", + f" - id: {chat.id}", + f" - type: {chat.type}", + f" - title: {chat.title}", + f" - Chats: ({len(all_chats)})", + ] + for c in all_chats: + buff.append(f' - {c.title}') + bot.send_message( + chat_id=update.message.chat_id, + text='\n'.join(buff), + parse_mode='Markdown', + ) def welcome(bot: Bot, update: Update): @@ -73,7 +134,8 @@ def reply(bot, update): logger.info(f'bot sends reply {reply_spec.reply}') bot.send_message( chat_id=update.message.chat_id, - text=reply_spec.reply + text=reply_spec.reply, + parse_mode='Markdown', ) @@ -90,6 +152,8 @@ def main(): dp.add_handler(CommandHandler('start', command_start)) dp.add_handler(CommandHandler('help', command_help)) dp.add_handler(CommandHandler('status', command_status)) + dp.add_handler(CommandHandler('settings', command_settings)) + dp.add_handler(CommandHandler('debug', command_debug)) dp.add_handler(MessageHandler(Filters.status_update.new_chat_members, welcome)) dp.add_handler(MessageHandler(Filters.group, reply)) diff --git a/config.py b/config.py index 89b7825..f9e8ded 100644 --- a/config.py +++ b/config.py @@ -47,7 +47,12 @@ def log(logger_method): POLL_INTERVAL = config('POLL_INTERVAL', int, default=3) # Bot message for start command -BOT_GREETING = config('BOT_GREETING', default="Hi! I'm a friendly, slightly psychopath robot") +BOT_GREETING = config('BOT_GREETING', default=( + "### Hi! I'm a friendly, slightly psychopath robot.\n" + "Now I can remember this chat. This means you " + "can configure some settings and can use some " + "new features like GitHub integration." + )) # A username longer than this will be considered non-human # - Allowed values: An integer larger than 1 @@ -55,6 +60,7 @@ def log(logger_method): int, default=100) +DB_NAME = config('DB_NAME', default='pydeckard_db') # We have found, through empiric evidence, that a large ration of Chinese # characters usually indicates the user is a spammer or bot. @@ -104,6 +110,11 @@ def bot_replies_enabled() -> bool: "la puerta de Tannhäuser. Todos esos momentos se perderán en el " "tiempo... como lágrimas en la lluvia. Es hora de morir. 🔫", ("python", "pitón", "piton"): THE_ZEN_OF_PYTHON, + ("phyton",): [ + "Se dice **Python**, simplón", + "s/phython/python/", + "_Phyton_ también decía mi abuela...", + ], } MAXLEN_FOR_USERNAME_TO_TREAT_AS_HUMAN = 100 diff --git a/dba/__init__.py b/dba/__init__.py new file mode 100644 index 0000000..6dca60e --- /dev/null +++ b/dba/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from dba.chat import save_chat +from dba.chat import load_all_chats +from dba.core import DBA + +def connect(name): + if connect.singleton is None: + connect.singleton = DBA(name) + return connect.singleton + + +connect.singleton = None + diff --git a/dba/chat.py b/dba/chat.py new file mode 100644 index 0000000..719d16c --- /dev/null +++ b/dba/chat.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + + +from .core import DBA + + +# Highlevel functions + + + +def save_chat(db, id_chat, chat_type, title): + exists = db.get_row('SELECT * FROM chat where id_chat=?', id_chat) + if not exists: + sql = 'INSERT INTO chat (id_chat, chat_type, title) VALUES (?, ?, ?)' + db.execute(sql, (id_chat, chat_type, title)) + return 1 + + +def load_all_chats(db): + return db.get_rows('SELECT * FROM chat') diff --git a/dba/core.py b/dba/core.py new file mode 100644 index 0000000..2b9b16d --- /dev/null +++ b/dba/core.py @@ -0,0 +1,100 @@ +import os +import logging + +import sqlite3 + + +logger = logging.getLogger(__name__) + + +def _exists_db(db_name): + return any([ + db_name == ':memory:', + not os.path.exists(db_name), + ]) + + +CREATE_TABLE_CHAT = ''' + CREATE TABLE IF NOT EXISTS chat ( + id_chat long, + chat_type text, + title text + ); +''' + + +class ObjectRow(sqlite3.Row): + + def __getattr__(self, name): + if name in self.keys(): + return self[name] + else: + raise AttributeError(name) + + +class DBA: + + def __init__(self, db_name): + is_first_time = _exists_db(db_name) + self.conn = sqlite3.connect(db_name) + self.conn.row_factory = ObjectRow + if is_first_time: + self.initialize_db() + + def initialize_db(self): + logger.info('No database (first time running)') + self.conn.execute(CREATE_TABLE_CHAT) + logger.info('Database initialized') + + def execute(self, db, statement, *args): + cur = self.conn.cursor() + result = None + try: + result = cur.execute(statement, args) + finally: + cur.close() + return result + + def get_value(self, sql, *args, cast=None, default=None): + cur = self.conn.cursor() + result = default + try: + cur.execute(str(sql), tuple(args)) + row = cur.fetchone() + if row: + result = row[0] + if cast is not None: + result = cast(result) + finally: + cur.close() + return default + + def get_row(self, sql, *args, cast=None): + cur = self.conn.cursor() + row = {} + try: + cur.execute(str(sql), tuple(args)) + row = cur.fetchone() + if row and cast is not None: + row = cast(row) + finally: + cur.close() + return row + + def get_rows(self, sql, *args, cast=None, limit=0): + cur = self.conn.cursor() + rows = [] + try: + cur.execute(str(sql), tuple(args)) + if limit > 0: + rows = list(cur.fetchmany(limit)) + else: + rows = list(cur.fetchall()) + if cast is not None: + rows = [cast(row) for row in rows] + finally: + cur.close() + return rows + + + diff --git a/vcs.py b/vcs.py new file mode 100644 index 0000000..0581a2a --- /dev/null +++ b/vcs.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +from collections import namedtuple + + +Issue = namedtuple('Issue', ['issue_id', 'title']) + + +class GitService: + + user = None + + def __init__(self): + from github import Github + if GitService.user is None: + conn = Github() + GitService.user = conn.get_user('pythoncanarias') + + def list_repos(self) -> list: + """List the names of all the public repositories. + """ + return [r.name for r in self.user.get_repos()] + + def list_issues(self, repo_name: str) -> list: + repo = GitService.user.get_repo(repo_name) + result = [ + Issue(issue_id=i.number, title=i.title) + for i in repo.get_issues() + ] + return result