python-telegram-bot v7 compatability + more (#35)

* Remove import of telegram.Emoji

* Using a list of filters in MessageHandler is getting deprecated

* Update requirements

Proudly upgrades to latest python-telegram-bot

* Refine readme

* Test kill command

* Another implement

* Add /kill command

* initial config support

* json config

* Add token into json

* Typo

* Add Admin list

* Refine admin & starter

* Fix an exception

* Fix typo

* refine readme

* Update help
This commit is contained in:
Simon Shi 2017-08-19 05:36:30 +08:00 committed by Jannes Höke
parent 8be4bc688d
commit 783e010956
12 changed files with 113 additions and 84 deletions

3
.gitignore vendored
View file

@ -1,3 +1,6 @@
# Config file
config.json
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]

View file

@ -2,18 +2,19 @@
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](./LICENSE) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](./LICENSE)
Telegram Bot that allows you to play the popular card game UNO via inline queries. The bot currently runs as [@unobot](http://telegram.me/unobot) Telegram Bot that allows you to play the popular card game UNO via inline queries. The bot currently runs as [@unobot](http://telegram.me/unobot).
To run the bot yourself, you will need: To run the bot yourself, you will need:
- Python (tested with 3.4 and 3.5) - Python (tested with 3.4+)
- The [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) module version 5.0.0 - The [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) module
- [Pony ORM](https://ponyorm.com/) - [Pony ORM](https://ponyorm.com/)
## Setup ## Setup
- Get a bot token from [@BotFather](http://telegram.me/BotFather) and place it in `credentials.py` - Get a bot token from [@BotFather](http://telegram.me/BotFather) and change configurations in `config.json`.
- Convert all language files from .po to .mo using `msgfmt unobot.po -o unobot.mo` - Convert all language files from `.po` files to `.mo` files using `msgfmt unobot.po -o unobot.mo`.
- Use `/setinline` and `/setinlinefeedback` with BotFather for your bot Also try `find . -maxdepth 2 -type d -name 'LC_MESSAGES' -exec bash -c 'msgfmt {}/unobot.po -o {}/unobot.mo' \;`.
- Use `/setinline` and `/setinlinefeedback` with BotFather for your bot.
Then run the bot with `python3 bot.py` Then run the bot with `python3 bot.py`.
Code documentation is minimal but there Code documentation is minimal but there.

71
bot.py
View file

@ -17,7 +17,6 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging import logging
from datetime import datetime from datetime import datetime
from random import randint from random import randint
@ -45,13 +44,16 @@ import settings
from simple_commands import help from simple_commands import help
#import json
#with open("config.json","r") as f:
# config = json.loads(f.read())
#forbidden = config.get("black_list", None)
logging.basicConfig( logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO) level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@user_locale @user_locale
def notify_me(bot, update): def notify_me(bot, update):
"""Handler for /notify_me command, pm people for next game""" """Handler for /notify_me command, pm people for next game"""
@ -88,7 +90,8 @@ def new_game(bot, update):
del gm.remind_dict[update.message.chat_id] del gm.remind_dict[update.message.chat_id]
game = gm.new_game(update.message.chat) game = gm.new_game(update.message.chat)
game.owner = update.message.from_user game.starter = update.message.from_user
game.owner.append(update.message.from_user.id)
send_async(bot, chat_id, send_async(bot, chat_id,
text=_("Created a new game! Join the game with /join " text=_("Created a new game! Join the game with /join "
"and start the game with /start")) "and start the game with /start"))
@ -96,6 +99,41 @@ def new_game(bot, update):
if botan: if botan:
botan.track(update.message, 'New games') botan.track(update.message, 'New games')
@user_locale
def kill_game(bot, update):
"""Handler for the /kill command"""
chat = update.message.chat
user = update.message.from_user
games = gm.chatid_games.get(chat.id)
if update.message.chat.type == 'private':
help(bot, update)
return
if not games:
send_async(bot, chat.id,
text=_("There is no running game in this chat."))
return
game = games[-1]
if user.id in game.owner:
try:
gm.end_game(chat, user)
send_async(bot, chat.id, text=__("Game ended!", multi=game.translate))
except NoGameInChatError:
send_async(bot, chat.id,
text=_("The game is not started yet. "
"Join the game with /join and start the game with /start"),
reply_to_message_id=update.message.message_id)
else:
send_async(bot, chat.id,
text=_("Only the game creator ({name}) and admin can do that.")
.format(name=game.starter.first_name),
reply_to_message_id=update.message.message_id)
@user_locale @user_locale
def join_game(bot, update): def join_game(bot, update):
@ -321,7 +359,7 @@ def close_game(bot, update):
game = games[-1] game = games[-1]
if game.owner.id == user.id: if user.id in game.owner:
game.open = False game.open = False
send_async(bot, chat.id, text=_("Closed the lobby. " send_async(bot, chat.id, text=_("Closed the lobby. "
"No more players can join this game.")) "No more players can join this game."))
@ -329,8 +367,8 @@ def close_game(bot, update):
else: else:
send_async(bot, chat.id, send_async(bot, chat.id,
text=_("Only the game creator ({name}) can do that.") text=_("Only the game creator ({name}) and admin can do that.")
.format(name=game.owner.first_name), .format(name=game.starter.first_name),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
return return
@ -349,15 +387,15 @@ def open_game(bot, update):
game = games[-1] game = games[-1]
if game.owner.id == user.id: if user.id in game.owner:
game.open = True game.open = True
send_async(bot, chat.id, text=_("Opened the lobby. " send_async(bot, chat.id, text=_("Opened the lobby. "
"New players may /join the game.")) "New players may /join the game."))
return return
else: else:
send_async(bot, chat.id, send_async(bot, chat.id,
text=_("Only the game creator ({name}) can do that") text=_("Only the game creator ({name}) and admin can do that.")
.format(name=game.owner.first_name), .format(name=game.starter.first_name),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
return return
@ -376,7 +414,7 @@ def enable_translations(bot, update):
game = games[-1] game = games[-1]
if game.owner.id == user.id: if user.id in game.owner:
game.translate = True game.translate = True
send_async(bot, chat.id, text=_("Enabled multi-translations. " send_async(bot, chat.id, text=_("Enabled multi-translations. "
"Disable with /disable_translations")) "Disable with /disable_translations"))
@ -384,8 +422,8 @@ def enable_translations(bot, update):
else: else:
send_async(bot, chat.id, send_async(bot, chat.id,
text=_("Only the game creator ({name}) can do that") text=_("Only the game creator ({name}) and admin can do that.")
.format(name=game.owner.first_name), .format(name=game.starter.first_name),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
return return
@ -404,7 +442,7 @@ def disable_translations(bot, update):
game = games[-1] game = games[-1]
if game.owner.id == user.id: if user.id in game.owner:
game.translate = False game.translate = False
send_async(bot, chat.id, text=_("Disabled multi-translations. " send_async(bot, chat.id, text=_("Disabled multi-translations. "
"Enable them again with " "Enable them again with "
@ -413,8 +451,8 @@ def disable_translations(bot, update):
else: else:
send_async(bot, chat.id, send_async(bot, chat.id,
text=_("Only the game creator ({name}) can do that") text=_("Only the game creator ({name}) and admin can do that.")
.format(name=game.owner.first_name), .format(name=game.starter.first_name),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
return return
@ -728,6 +766,7 @@ dispatcher.add_handler(ChosenInlineResultHandler(process_result))
dispatcher.add_handler(CallbackQueryHandler(select_game)) dispatcher.add_handler(CallbackQueryHandler(select_game))
dispatcher.add_handler(CommandHandler('start', start_game, pass_args=True)) dispatcher.add_handler(CommandHandler('start', start_game, pass_args=True))
dispatcher.add_handler(CommandHandler('new', new_game)) dispatcher.add_handler(CommandHandler('new', new_game))
dispatcher.add_handler(CommandHandler('kill', kill_game))
dispatcher.add_handler(CommandHandler('join', join_game)) dispatcher.add_handler(CommandHandler('join', join_game))
dispatcher.add_handler(CommandHandler('leave', leave_game)) dispatcher.add_handler(CommandHandler('leave', leave_game))
dispatcher.add_handler(CommandHandler('open', open_game)) dispatcher.add_handler(CommandHandler('open', open_game))
@ -740,7 +779,7 @@ dispatcher.add_handler(CommandHandler('skip', skip_player))
dispatcher.add_handler(CommandHandler('notify_me', notify_me)) dispatcher.add_handler(CommandHandler('notify_me', notify_me))
simple_commands.register() simple_commands.register()
settings.register() settings.register()
dispatcher.add_handler(MessageHandler([Filters.status_update], status_update)) dispatcher.add_handler(MessageHandler(Filters.status_update, status_update))
dispatcher.add_error_handler(error) dispatcher.add_error_handler(error)
start_bot(updater) start_bot(updater)

10
card.py
View file

@ -18,8 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from telegram.emoji import Emoji
# Colors # Colors
RED = 'r' RED = 'r'
BLUE = 'b' BLUE = 'b'
@ -30,10 +28,10 @@ BLACK = 'x'
COLORS = (RED, BLUE, GREEN, YELLOW) COLORS = (RED, BLUE, GREEN, YELLOW)
COLOR_ICONS = { COLOR_ICONS = {
RED: Emoji.HEAVY_BLACK_HEART, RED: '❤️',
BLUE: Emoji.BLUE_HEART, BLUE: '💙',
GREEN: Emoji.GREEN_HEART, GREEN: '💚',
YELLOW: Emoji.YELLOW_HEART, YELLOW: '💛',
BLACK: '⬛️' BLACK: '⬛️'
} }

8
config.json.example Normal file
View file

@ -0,0 +1,8 @@
{
"token": "token_here",
"botan_token": null,
"admin_list": [0],
"open_lobby": true,
"enable_translations": false,
"workers": 32
}

View file

@ -1,21 +0,0 @@
#!/usr/bin/env python3
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
TOKEN = 'TOKEN'
BOTAN_TOKEN = '' # Optional: Add a botan.io token if you want bot statistics

14
game.py
View file

@ -19,23 +19,25 @@
import logging import logging
import json
from datetime import datetime from datetime import datetime
from deck import Deck from deck import Deck
import card as c import card as c
class Game(object): class Game(object):
""" This class represents a game of UNO """ """ This class represents a game of UNO """
current_player = None current_player = None
reversed = False reversed = False
draw_counter = 0
choosing_color = False choosing_color = False
started = False started = False
owner = None draw_counter = 0
open = True
translate = False
players_won = 0 players_won = 0
starter = None
with open("config.json","r") as f:
config = json.loads(f.read())
owner = config.get("admin_list", None)
open = config.get("open_lobby", True)
translate = config.get("enable_translations", False)
def __init__(self, chat): def __init__(self, chat):
self.chat = chat self.chat = chat

View file

@ -1,2 +1,2 @@
python-telegram-bot==5 python-telegram-bot
pony pony

View file

@ -18,7 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from telegram import ReplyKeyboardMarkup, Emoji from telegram import ReplyKeyboardMarkup
from telegram.ext import CommandHandler, RegexHandler from telegram.ext import CommandHandler, RegexHandler
from utils import send_async from utils import send_async
@ -44,12 +44,12 @@ def show_settings(bot, update):
us = UserSetting(id=update.message.from_user.id) us = UserSetting(id=update.message.from_user.id)
if not us.stats: if not us.stats:
stats = Emoji.BAR_CHART + ' ' + _("Enable statistics") stats = '📊' + ' ' + _("Enable statistics")
else: else:
stats = Emoji.CROSS_MARK + ' ' + _("Delete all statistics") stats = '' + ' ' + _("Delete all statistics")
kb = [[stats], [Emoji.EARTH_GLOBE_EUROPE_AFRICA + ' ' + _("Language")]] kb = [[stats], ['🌍' + ' ' + _("Language")]]
send_async(bot, chat.id, text=Emoji.WRENCH + ' ' + _("Settings"), send_async(bot, chat.id, text='🔧' + ' ' + _("Settings"),
reply_markup=ReplyKeyboardMarkup(keyboard=kb, reply_markup=ReplyKeyboardMarkup(keyboard=kb,
one_time_keyboard=True)) one_time_keyboard=True))
@ -60,12 +60,12 @@ def kb_select(bot, update, groups):
user = update.message.from_user user = update.message.from_user
option = groups[0] option = groups[0]
if option == Emoji.BAR_CHART: if option == '📊':
us = UserSetting.get(id=user.id) us = UserSetting.get(id=user.id)
us.stats = True us.stats = True
send_async(bot, chat.id, text=_("Enabled statistics!")) send_async(bot, chat.id, text=_("Enabled statistics!"))
elif option == Emoji.EARTH_GLOBE_EUROPE_AFRICA: elif option == '🌍':
kb = [[locale + ' - ' + descr] kb = [[locale + ' - ' + descr]
for locale, descr for locale, descr
in sorted(available_locales.items())] in sorted(available_locales.items())]
@ -73,7 +73,7 @@ def kb_select(bot, update, groups):
reply_markup=ReplyKeyboardMarkup(keyboard=kb, reply_markup=ReplyKeyboardMarkup(keyboard=kb,
one_time_keyboard=True)) one_time_keyboard=True))
elif option == Emoji.CROSS_MARK: elif option == '':
us = UserSetting.get(id=user.id) us = UserSetting.get(id=user.id)
us.stats = False us.stats = False
us.first_places = 0 us.first_places = 0
@ -98,9 +98,9 @@ def locale_select(bot, update, groups):
def register(): def register():
dispatcher.add_handler(CommandHandler('settings', show_settings)) dispatcher.add_handler(CommandHandler('settings', show_settings))
dispatcher.add_handler(RegexHandler('^([' + Emoji.BAR_CHART + dispatcher.add_handler(RegexHandler('^([' + '📊' +
Emoji.EARTH_GLOBE_EUROPE_AFRICA + '🌍' +
Emoji.CROSS_MARK + ']) .+$', '' + ']) .+$',
kb_select, pass_groups=True)) kb_select, pass_groups=True))
dispatcher.add_handler(RegexHandler(r'^(\w\w_\w\w) - .*', dispatcher.add_handler(RegexHandler(r'^(\w\w_\w\w) - .*',
locale_select, pass_groups=True)) locale_select, pass_groups=True))

View file

@ -17,21 +17,20 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
from telegram.ext import Updater from telegram.ext import Updater
from telegram.contrib.botan import Botan from telegram.contrib.botan import Botan
from game_manager import GameManager from game_manager import GameManager
from database import db from database import db
from credentials import TOKEN, BOTAN_TOKEN
db.bind('sqlite', 'uno.sqlite3', create_db=True) db.bind('sqlite', 'uno.sqlite3', create_db=True)
db.generate_mapping(create_tables=True) db.generate_mapping(create_tables=True)
gm = GameManager() gm = GameManager()
updater = Updater(token=TOKEN, workers=32) with open("config.json","r") as f:
config = json.loads(f.read())
updater = Updater(token=config.get("token"), workers=config.get("workers", 32))
dispatcher = updater.dispatcher dispatcher = updater.dispatcher
botan = False botan = Botan(config.get("botan_token", None))
if BOTAN_TOKEN:
botan = Botan(BOTAN_TOKEN)

View file

@ -46,6 +46,7 @@ help_text = ("Follow these steps:\n\n"
"Other commands (only game creator):\n" "Other commands (only game creator):\n"
"/close - Close lobby\n" "/close - Close lobby\n"
"/open - Open lobby\n" "/open - Open lobby\n"
"/kill - Terminate the game\n"
"/enable_translations - Translate relevant texts into all " "/enable_translations - Translate relevant texts into all "
"languages spoken in a game\n" "languages spoken in a game\n"
"/disable_translations - Use English for those texts\n\n" "/disable_translations - Use English for those texts\n\n"

View file

@ -20,7 +20,6 @@
import logging import logging
from telegram import Emoji
from telegram.ext.dispatcher import run_async from telegram.ext.dispatcher import run_async
from internationalization import _, __ from internationalization import _, __
@ -51,29 +50,29 @@ def display_name(user):
def display_color(color): def display_color(color):
""" Convert a color code to actual color name """ """ Convert a color code to actual color name """
if color == "r": if color == "r":
return _("{emoji} Red").format(emoji=Emoji.HEAVY_BLACK_HEART) return _("{emoji} Red").format(emoji='❤️')
if color == "b": if color == "b":
return _("{emoji} Blue").format(emoji=Emoji.BLUE_HEART) return _("{emoji} Blue").format(emoji='💙')
if color == "g": if color == "g":
return _("{emoji} Green").format(emoji=Emoji.GREEN_HEART) return _("{emoji} Green").format(emoji='💚')
if color == "y": if color == "y":
return _("{emoji} Yellow").format(emoji=Emoji.YELLOW_HEART) return _("{emoji} Yellow").format(emoji='💛')
def display_color_group(color, game): def display_color_group(color, game):
""" Convert a color code to actual color name """ """ Convert a color code to actual color name """
if color == "r": if color == "r":
return __("{emoji} Red", game.translate).format( return __("{emoji} Red", game.translate).format(
emoji=Emoji.HEAVY_BLACK_HEART) emoji='❤️')
if color == "b": if color == "b":
return __("{emoji} Blue", game.translate).format( return __("{emoji} Blue", game.translate).format(
emoji=Emoji.BLUE_HEART) emoji='💙')
if color == "g": if color == "g":
return __("{emoji} Green", game.translate).format( return __("{emoji} Green", game.translate).format(
emoji=Emoji.GREEN_HEART) emoji='💚')
if color == "y": if color == "y":
return __("{emoji} Yellow", game.translate).format( return __("{emoji} Yellow", game.translate).format(
emoji=Emoji.YELLOW_HEART) emoji='💛')
def error(bot, update, error): def error(bot, update, error):