Compare commits

...

46 Commits

Author SHA1 Message Date
Ceda EI 6116594eb4 [fix] Re-order dispatchers so that /s?q_\d* commands work 2021-04-24 12:43:22 +05:30
Ceda EI 1f0a05ca9d Allows CORS 2019-05-31 10:41:28 +05:30
Ceda EI d2b70dacd8 Update README. 2019-05-21 16:27:47 +05:30
Ceda EI c5b7eba052 Update README. 2019-05-21 16:24:41 +05:30
Ceda EI 1cc0743061 Add client list in tokens. 2019-05-21 16:04:06 +05:30
Ceda EI 514117a079 Convert qid to int 2019-05-21 13:28:59 +05:30
Ceda EI ac3e09fb19 Fix state being True even if it is false. 2019-05-21 13:15:25 +05:30
Ceda EI 3604248a83 Fix typo 2019-05-13 19:48:44 +05:30
Ceda EI 0ab2fa5b6b Add ls to list both quests and side quests 2019-05-11 12:29:21 +05:30
Ceda EI 0a41d3d868 Add initial shortcuts 2019-04-13 00:55:56 +05:30
Ceda EI a23f1499ec Merge branch 'v2' 2019-03-19 18:26:31 +05:30
Ceda EI 3b134ae5f2 Avoid sending excess messages and unnecessary state drops 2019-03-19 18:02:05 +05:30
Ceda EI 697738945f Add group support - names, /Q_, /SQ_ 2019-03-19 17:47:36 +05:30
Ceda EI 021467817b Move points calculation outside loop. 2019-03-19 16:53:04 +05:30
Ceda EI 99c1795b93 Fix player.get_side_quest 2019-03-16 15:15:01 +05:30
Ceda EI 05d22a0ce8 Add "bad values" error 2019-03-16 14:46:47 +05:30
Ceda EI 4d610c5bff Fix /get_quest, /get_side_quest 2019-03-16 14:39:44 +05:30
Ceda EI cf05499e43 Fix /player. 2019-03-16 14:24:39 +05:30
Ceda EI 85c7add252 Redirect / to docs 2019-03-16 10:18:09 +05:30
Ceda EI 333b772d41 Move schema to schema.sql 2019-03-15 12:57:07 +05:30
Ceda EI 5b19524876 Add /delete_* 2019-03-15 11:16:24 +05:30
Ceda EI 4609711624 Send 404 if quest/side_quest not found 2019-03-14 20:12:42 +05:30
Ceda EI 28c33ee976 Add /update_*. Add checks. 2019-03-14 00:49:42 +05:30
Ceda EI 7c99523cf1 Add /add_quest, /add_side_quest 2019-03-14 00:03:49 +05:30
Ceda EI aa0b388126 Add /get_quest, /get_side_quest 2019-03-13 19:49:55 +05:30
Ceda EI 2a5ae51d71 Add get_quest, get_side_quest to player. 2019-03-13 19:49:15 +05:30
Ceda EI b3093fa1a4 Add /get_quests, /get_side_quests 2019-03-13 19:12:45 +05:30
Ceda EI 2315b05f2c Add errors.py, get_player, /player. 2019-03-13 18:52:44 +05:30
Ceda EI 1239cd08cc Add /auth 2019-03-13 18:30:35 +05:30
Ceda EI 6d67f05093 Add get_player_from_token 2019-03-13 17:50:53 +05:30
Ceda EI b04ca65270 Update help link 2019-03-12 11:12:51 +05:30
Ceda EI 4737f023e9 Add tokens UI 2019-03-11 21:11:09 +05:30
Ceda EI f5f81f5a93 Add delete_token() 2019-03-11 18:17:01 +05:30
Ceda EI 9261247115 Add token table, get_tokens(), add_token() 2019-03-11 18:09:48 +05:30
Ceda EI 6e55bae42c Move buttons to button_groups.py 2019-03-11 17:26:25 +05:30
Ceda EI 095e7791d3 Fix escaping html 2019-02-26 18:07:14 +05:30
Ceda EI 030276a23f Disable web page previews in List Quests/Side Quests 2018-12-26 21:12:50 +05:30
Ceda EI 0fdabeabda Show priority in list quests/side quests 2018-12-21 21:30:24 +05:30
Ceda EI cef59c494e Add /rate. Remove vars for handlers. 2018-12-03 11:41:44 +05:30
Ceda EI 38cffcf8b7 Add player status to main menu. 2018-11-26 20:04:17 +05:30
Ceda EI e78fa9caa1 Fix unable to delete side quests. 2018-11-17 12:54:52 +05:30
Ceda EI a80a7fbcc5 Add emojis. 2018-11-11 11:21:51 +05:30
Ceda EI 7b0ea8bedc Add more emojis. Some minor changes to UI. 2018-11-11 00:52:08 +05:30
Ceda EI 93aaebf655 Add emojis to main menu. 2018-11-10 23:40:31 +05:30
Ceda EI 2fd5804e87 Update /help 2018-11-10 21:36:31 +05:30
Ceda EI 0156419aed Fix typo 2018-11-10 16:00:52 +05:30
7 changed files with 752 additions and 140 deletions

View File

@ -1,3 +1,30 @@
# Questable
A game-like To-Do List Telegram Bot
A game-like To-Do List Telegram Bot.
Source code for [Questable Bot](https://t.me/questable_bot) and the relevant
[API](https://api.questable.webionite.com/)
# Self Hosting
+ Clone the repository.
+ `git clone https://gitlab.com/questable/questable_bot.git`
+ `cd questable`
## Telegram Bot
+ Install the dependencies
+ `pip3 install python-telegram-bot`
+ Copy `sample.config.py` to `config.py` and edit it accordingly.
+ Run the bot
+ `python3 bot.py`
## Questable API Server
+ Install the dependencies
+ `pip3 install Flask flask_cors`
+ Install `gunicorn`
+ `pip3 install gunicorn`
+ Run `gunicorn3 -b 127.0.0.1:5000 server:app`. Change port if you want to run
gunicorn on a different port.
+ Set up a reverse proxy from your webserver to `localhost:5000`.

360
bot.py Normal file → Executable file
View File

@ -9,6 +9,8 @@ from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, \
RegexHandler
import signal
import sys
import re
import button_groups
try:
import config
@ -20,17 +22,22 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - \
%(message)s', level=logging.INFO)
def escape_html(message):
return re.sub("<", "&lt;",
re.sub("&", "&amp;", message))
def start(bot, update):
chat_id = update.message.chat_id
name = str(update.message.from_user.first_name)
if update.message.from_user.last_name:
name += " " + str(update.message.from_user.last_name)
if update.message.chat.type == "private":
name = str(update.message.from_user.first_name)
if update.message.from_user.last_name:
name += " " + str(update.message.from_user.last_name)
else:
name = update.message.chat.title
text = f"Hello {name}!\n" + \
"Welcome to Questable. To get started, check /help."
custom_keyboard = [
['Add Quest', 'Add Side Quest'],
['List Quests', 'List Side Quests']
]
custom_keyboard = button_groups.main
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)
@ -61,7 +68,10 @@ def add_quest(bot, update, player, type="quest"):
chat_id = update.message.chat_id
text = ("What shall the name of " +
{"quest": "Quest", "side_quest": "Side Quest"}[type] + " be?")
reply_markup = telegram.ReplyKeyboardRemove()
if update.message.chat.type == "private":
reply_markup = telegram.ReplyKeyboardRemove()
else:
reply_markup = telegram.ForceReply()
bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)
@ -80,7 +90,7 @@ def add_name(bot, update, player, type, qid):
chat_id = update.message.chat_id
text = "How difficult is it?"
custom_keyboard = [["Low", "Medium", "High"]]
custom_keyboard = button_groups.difficulty
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)
@ -88,11 +98,11 @@ def add_name(bot, update, player, type, qid):
def add_diff(bot, update, player, type, qid):
message = update.message.text.lower()
chat_id = update.message.chat_id
if message == "low":
if message in ["low", "📙 low", "l"]:
diff = 1
elif message == "medium":
elif message in ["medium", "📘 medium", "m"]:
diff = 2
elif message == "high":
elif message in ["high", "📗 high", "h"]:
diff = 3
else:
bot.send_message(chat_id=chat_id, text="Invalid Option")
@ -111,7 +121,7 @@ def add_diff(bot, update, player, type, qid):
x.update_db()
text = "How important is it?"
custom_keyboard = [["Low", "Medium", "High"]]
custom_keyboard = button_groups.importance
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)
@ -119,11 +129,11 @@ def add_diff(bot, update, player, type, qid):
def add_imp(bot, update, player, type, qid):
message = update.message.text.lower()
chat_id = update.message.chat_id
if message == "low":
if message in ["low", "🔹 low", "l"]:
imp = 1
elif message == "medium":
elif message in ["medium", "🔸 medium", "m"]:
imp = 2
elif message == "high":
elif message in ["high", "🔺 high", "h"]:
imp = 3
else:
bot.send_message(chat_id=chat_id, text="Invalid Option")
@ -141,31 +151,32 @@ def add_imp(bot, update, player, type, qid):
x.imp = imp
x.update_db()
text = "Quest Added!"
text = {"quest": "Quest", "side_quest": "Side Quest"}[type] + " Added!"
bot.send_message(chat_id=chat_id, text=text)
send_status(bot, update, player)
def send_status(bot, update, player, prefix=""):
name = str(update.message.from_user.first_name)
if update.message.from_user.last_name:
name += " " + str(update.message.from_user.last_name)
if update.message.chat.type == "private":
name = str(update.message.from_user.first_name)
if update.message.from_user.last_name:
name += " " + str(update.message.from_user.last_name)
else:
name = update.message.chat.title
name = escape_html(name)
points = player.get_points()
total_quests = len(player.get_quests(None))
completed_quests = len(player.get_quests(1))
total_side_quests = len(player.get_side_quests(None))
completed_side_quests = len(player.get_side_quests(1))
text = (f'<b>{name}</b>\n\n' + prefix +
text = (prefix + f'<b>👤 {name}</b>\n'
f'<b>🔥 XP:</b> {points}\n'
f'<b>⭐️ Quests:</b> {completed_quests}/{total_quests}\n'
f'<b>💠 Side Quests:</b> {completed_side_quests}/'
f'{total_side_quests}\n')
chat_id = update.message.chat_id
custom_keyboard = [
['Add Quest', 'Add Side Quest'],
['List Quests', 'List Side Quests']
]
custom_keyboard = button_groups.main
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup,
parse_mode="HTML")
@ -187,21 +198,27 @@ def list_quests(bot, update, player, type):
raise ValueError('Not quest or side_quest')
if len(x) == 0:
text = ("<b>You have completed every " +
{"quest": "Quests", "side_quest": "Side Quests"}[type] +
{"quest": "quest", "side_quest": "side quest"}[type] +
" ever known to me.</b>")
else:
text = "<b>List of " + {"quest": "Quests", "side_quest":
"Side Quests"}[type] + "</b>\n"
text = ("<b>" + {"quest": "📖", "side_quest": "📒"}[type] +
" List of " + {"quest": "Quests", "side_quest":
"Side Quests"}[type] + "</b>")
x.sort(key=lambda i: (i.imp, -i.QID), reverse=True)
if type == "quest":
for i in x:
imp = 3
for i in x:
if i.imp <= imp:
text += "\n\n<b>📌 " + ["Low", "Medium", "High"][i.imp-1]
text += "</b>"
imp = i.imp - 1
if type == "quest":
text += f"\n/Q_{i.QID} {i.name}"
else:
for i in x:
else:
text += f"\n/SQ_{i.QID} {i.name}"
chat_id = update.message.chat_id
bot.send_message(chat_id=chat_id, text=text, parse_mode="HTML")
bot.send_message(chat_id=chat_id, text=text, parse_mode="HTML",
disable_web_page_preview=True)
def quest(bot, update, player, qid, type):
@ -220,7 +237,8 @@ def quest(bot, update, player, qid, type):
text = ("<b>🗺 " + {"quest": "Quest", "side_quest": "Side Quest"}[type]
+ f":</b> {x.name}"
"\n<b>📌 Priority:</b> " + ["Low", "Medium", "High"][x.imp-1]
+ "\n<b>📘 Difficulty:</b> " + ["Low", "Medium", "High"][x.diff-1]
+ "\n<b>" + ["📙", "📘", "📗"][x.diff-1] + " Difficulty:</b> "
+ ["Low", "Medium", "High"][x.diff-1]
+ "\n<b>" + ["", ""][x.state] + " Status:</b> "
+ ["Incomplete", "Complete"][x.state])
@ -230,13 +248,7 @@ def quest(bot, update, player, qid, type):
elif x.state == 0:
state = {"quest": "eq", "side_quest": "esq"}[type]
player.set_state(state, qid)
custom_keyboard = [
["Mark as done"],
["Edit Name", "Change Priority"],
["Change Difficulty", "Delete " +
{"quest": "Quest", "side_quest": "Side Quest"}[type]],
["Back"]]
custom_keyboard = button_groups.quests(type)
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
chat_id = update.message.chat_id
bot.send_message(chat_id=chat_id, text=text, parse_mode="HTML",
@ -244,7 +256,7 @@ def quest(bot, update, player, qid, type):
def quest_handling(bot, update, db):
text = update.message.text.lower().split("_")
text = update.message.text.lower().split("@")[0].split("_")
chat_id = update.message.chat_id
player = questable.player(db, chat_id)
drop_state(bot, update, player)
@ -271,10 +283,7 @@ def mark_as_done(bot, update, player, qid, type):
player.set_state('none', 0)
send_status(bot, update, player, f"<b>🌟 Earned {points} XP</b>\n\n")
chat_id = update.message.chat_id
custom_keyboard = [
['Add Quest', 'Add Side Quest'],
['List Quests', 'List Side Quests']
]
custom_keyboard = button_groups.main
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
bot.send_animation(chat_id=chat_id, animation=random.choice(config.gifs),
reply_markup=reply_markup)
@ -293,20 +302,31 @@ def edit_quest(bot, update, player, qid, target, type):
if target == "name":
x.name = message
text = "<b>Updated Name</b>"
elif target == "imp" or target == "diff":
text = "<b>☑️ Updated Name</b>"
elif target == "imp":
message = message.lower()
if message != "low" and message != "medium" and message != "high":
if message in ["low", "🔹 low", "l"]:
x.imp = 1
elif message in ["medium", "🔸 medium", "m"]:
x.imp = 2
elif message in ["high", "🔺 high", "h"]:
x.imp = 3
else:
bot.send_message(chat_id=chat_id, text="Invalid Option")
return
text = "<b>☑️ Updated Priority</b>"
elif target == "diff":
message = message.lower()
if message in ["low", "📙 low", "l"]:
x.diff = 1
elif message in ["medium", "📘 medium", "m"]:
x.diff = 2
elif message in ["high", "📗 high", "h"]:
x.diff = 3
else:
num = {"low": 1, "medium": 2, "high": 3}[message]
if target == "imp":
x.imp = num
text = "<b>Updated Priority</b>"
elif target == "diff":
x.diff = num
text = "<b>Updated Difficulty</b>"
bot.send_message(chat_id=chat_id, text="Invalid Option")
return
text = "<b>☑️ Updated Difficulty</b>"
x.update_db()
if type == "quest":
@ -314,11 +334,11 @@ def edit_quest(bot, update, player, qid, target, type):
elif type == "side_quest":
player.set_state('esq', qid)
custom_keyboard = [
["Mark as done"],
["Edit Name", "Change Priority"],
["Change Difficulty", "Delete " +
["Mark as done"],
["📝 Edit Name", "⚠️ Change Priority"],
["📚 Change Difficulty", "🗑 Delete " +
{"quest": "Quest", "side_quest": "Side Quest"}[type]],
["Back"]]
["⬅️ Back"]]
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup,
parse_mode="HTML")
@ -337,16 +357,15 @@ def drop_state(bot, update, player):
x = questable.get_side_quest(player.DB, player.CHAT_ID, state["extra"])
x.delete_from_db()
player.set_state('none', 0)
else:
player.set_state('none', 0)
def help_command(bot, update, db):
player = questable.player(db, update.message.chat_id)
drop_state(bot, update, player)
chat_id = update.message.chat_id
custom_keyboard = [
['Add Quest', 'Add Side Quest'],
['List Quests', 'List Side Quests']
]
custom_keyboard = button_groups.main
text = ("*Questable Bot*\n\nQuestable is an RPG-like bot for maintaining "
"events in real life. _Main Tasks_ are _Quests_ while _other "
"tasks_ are _Side Quests._ You can use the bot to maintain a "
@ -354,7 +373,8 @@ def help_command(bot, update, db):
"Quests you get XP based on how difficult and important the "
"Quest/Side Quest was. Quests/Side Quests can be added and "
"modified later.\n\n To get more help check "
"[Extended Help](https://webionite.com/questable/). In case, of "
"[Extended Help](https://questable.webionite.com/help/) or "
"[this video](https://t.me/quadnite/25). In case, of "
"bugs/feedback/more help, contact @ceda\\_ei or join the "
"[group](https://t.me/questable).")
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
@ -362,6 +382,74 @@ def help_command(bot, update, db):
reply_markup=reply_markup)
def rate_command(bot, update, db):
player = questable.player(db, update.message.chat_id)
drop_state(bot, update, player)
chat_id = update.message.chat_id
custom_keyboard = button_groups.main
text = ("[Vote for us on Telegram Directory!](https://t.me/tgdrbot?"
"start=questable_bot)\n\n"
"Telegram Directory is a website that helps you discover top "
"telegram channels, bots and groups.")
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
bot.send_message(chat_id=chat_id, text=text, parse_mode="Markdown",
reply_markup=reply_markup)
def tokens(bot, update):
custom_keyboard = button_groups.tokens
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
reply_text = ("Tokens are used to authenticate external "
"applications. This only provides access to "
"Questable data.\n"
"\nOfficial clients are:\n"
"[Questable CLI](https://gitlab.com/questable/questable-cli)"
)
bot.send_message(chat_id=update.message.chat_id, text=reply_text,
reply_markup=reply_markup, parse_mode="markdown",
disable_web_page_preview=True)
def add_token(bot, update, player):
custom_keyboard = button_groups.main
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
if len(player.get_tokens()) < 3:
reply_text = player.add_token()
else:
reply_text = "Maximum number of tokens reached."
bot.send_message(chat_id=player.CHAT_ID, text=reply_text,
reply_markup=reply_markup)
def delete_token(bot, update, player):
tokens = player.get_tokens()
custom_keyboard = list(map(lambda x: [x], tokens))
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
reply_text = "Choose token to remove."
player.set_state("rt")
bot.send_message(chat_id=update.message.chat_id, text=reply_text,
reply_markup=reply_markup)
def delete_token_rt(bot, update, player):
player.delete_token(update.message.text.lower())
custom_keyboard = button_groups.main
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
reply_text = "Token has been removed."
player.set_state("none")
bot.send_message(chat_id=player.CHAT_ID, text=reply_text,
reply_markup=reply_markup)
def list_tokens(bot, update, player):
custom_keyboard = button_groups.main
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
reply_text = "📋 List of tokens\n\n"
reply_text += "\n".join(player.get_tokens())
bot.send_message(chat_id=update.message.chat_id, text=reply_text,
reply_markup=reply_markup)
def message_handling(bot, update, db):
text = update.message.text.lower()
player = questable.player(db, update.message.chat_id)
@ -379,19 +467,32 @@ def message_handling(bot, update, db):
# eqn / esqn: Edit Quest / Side Quest Name
# eqi / esqi: Edit Quest / Side Quest Importance
# eqd / esqd: Edit Quest / Side Quest Difficulty
# rt: Remove token
if state["state"] == "none":
if text == "add quest":
if text in ["add quest", "❇️ add quest", "aq"]:
add_quest(bot, update, player)
elif text == "add side quest":
elif text in ["add side quest", "📯 add side quest", "asq"]:
add_quest(bot, update, player, "side_quest")
elif text == "list quests":
elif text in ["list quests", "📜 list quests", "lq"]:
list_quests(bot, update, player, "quest")
elif text == "list side quests":
elif text in ["list side quests", "📃 list side quests", "lsq"]:
list_quests(bot, update, player, "side_quest")
elif text in ["tokens", "🔑 tokens", "t"]:
tokens(bot, update)
elif text in ["list tokens", "📋 list tokens", "lt"]:
list_tokens(bot, update, player)
elif text in ["generate token", "🔑 generate token", "gt"]:
add_token(bot, update, player)
elif text in ["delete token", "🧹 delete token", "dt"]:
delete_token(bot, update, player)
elif text == "ls":
list_quests(bot, update, player, "side_quest")
list_quests(bot, update, player, "quest")
else:
drop_state(bot, update, player)
send_status(bot, update, player)
if update.message.chat.type == "private":
send_status(bot, update, player)
elif state["state"] == "aq":
add_name(bot, update, player, "quest", state["extra"])
@ -412,80 +513,83 @@ def message_handling(bot, update, db):
add_imp(bot, update, player, "side_quest", state["extra"])
elif state["state"] == "eq":
if text == "back":
if text in ["back", "⬅️ back", "b"]:
player.set_state('none', 0)
send_status(bot, update, player)
elif text == "mark as done":
elif text in ["mark as done", "✅ mark as done", "mad"]:
mark_as_done(bot, update, player, state["extra"], "quest")
elif text == "edit name":
elif text in ["edit name", "📝 edit name", "en"]:
player.set_state('eqn', state["extra"])
text = "What shall the new name of the Quest be?"
reply_markup = telegram.ReplyKeyboardRemove()
bot.send_message(chat_id=player.CHAT_ID, text=text,
reply_markup=reply_markup)
elif text == "change priority":
elif text in ["change priority", "⚠️ change priority", "cp"]:
player.set_state('eqi', state["extra"])
text = "How important is it?"
custom_keyboard = [["Low", "Medium", "High"]]
custom_keyboard = button_groups.importance
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
bot.send_message(chat_id=player.CHAT_ID, text=text,
reply_markup=reply_markup)
elif text == "change difficulty":
elif text in ["change difficulty", "📚 change difficulty", "cd"]:
player.set_state('eqd', state["extra"])
text = "How difficult is it?"
custom_keyboard = [["Low", "Medium", "High"]]
custom_keyboard = button_groups.difficulty
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
bot.send_message(chat_id=player.CHAT_ID, text=text,
reply_markup=reply_markup)
elif text == "delete quest":
elif text in ["delete quest", "🗑 delete quest", "dq"]:
quest = questable.get_quest(db, player.CHAT_ID, state["extra"])
quest.delete_from_db()
drop_state(bot, update, player)
prefix = f"<b>Quest {quest.name} has been deleted</b>\n\n"
send_status(bot, update, player, prefix=prefix)
else:
drop_state(bot, update, player)
send_status(bot, update, player)
if update.message.chat.type == "private":
drop_state(bot, update, player)
send_status(bot, update, player)
elif state["state"] == "esq":
if text == "back":
if text in ["back", "⬅️ back", "b"]:
player.set_state('none', 0)
send_status(bot, update, player)
elif text == "mark as done":
elif text in ["mark as done", "✅ mark as done", "mad"]:
mark_as_done(bot, update, player, state["extra"], "side_quest")
elif text == "edit name":
elif text in ["edit name", "📝 edit name", "en"]:
player.set_state('esqn', state["extra"])
text = "What shall the new name of the Side Quest be?"
reply_markup = telegram.ReplyKeyboardRemove()
bot.send_message(chat_id=player.CHAT_ID, text=text,
reply_markup=reply_markup)
elif text == "change priority":
elif text in ["change priority", "⚠️ change priority", "cp"]:
player.set_state('esqi', state["extra"])
text = "How important is it?"
custom_keyboard = [["Low", "Medium", "High"]]
custom_keyboard = button_groups.importance
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
bot.send_message(chat_id=player.CHAT_ID, text=text,
reply_markup=reply_markup)
elif text == "change difficulty":
elif text in ["change difficulty", "📚 change difficulty", "cd"]:
player.set_state('esqd', state["extra"])
text = "How difficult is it?"
custom_keyboard = [["Low", "Medium", "High"]]
custom_keyboard = button_groups.difficulty
reply_markup = telegram.ReplyKeyboardMarkup(custom_keyboard)
bot.send_message(chat_id=player.CHAT_ID, text=text,
reply_markup=reply_markup)
elif text == "delete side quest":
elif text in ["delete side quest", "🗑 delete side quest", "dsq"]:
sq = questable.get_side_quest(db, player.CHAT_ID, state["extra"])
sq.delete_from_db()
drop_state(bot, update, player)
prefix = f"<b>Side Quest {sq.name} has been deleted</b>\n\n"
send_status(bot, update, player, prefix=prefix)
else:
drop_state(bot, update, player)
send_status(bot, update, player)
if update.message.chat.type == "private":
drop_state(bot, update, player)
send_status(bot, update, player)
elif state["state"] == "bo":
player.set_state('none', 0)
send_status(bot, update, player)
if text == "back" or update.message.chat.type == "private":
player.set_state('none', 0)
send_status(bot, update, player)
elif state["state"] == "eqn":
edit_quest(bot, update, player, state["extra"], "name", "quest")
@ -505,9 +609,12 @@ def message_handling(bot, update, db):
elif state["state"] == "esqd":
edit_quest(bot, update, player, state["extra"], "diff", "side_quest")
elif state["state"] == "rt":
delete_token_rt(bot, update, player)
else:
drop_state(bot, update, player)
send_status(bot, update, player)
if update.message.chat.type == "private":
drop_state(bot, update, player)
send_status(bot, update, player)
def sigterm_handler(signal, frame, db):
@ -515,55 +622,32 @@ def sigterm_handler(signal, frame, db):
sys.exit(0)
db = sqlite3.connect("questable.db", check_same_thread=False)
cursor = db.cursor()
signal.signal(signal.SIGTERM, lambda x, y: sigterm_handler(x, y, db))
# Set up tables
queries = [
("CREATE TABLE IF NOT EXISTS quests(chat_id int NOT NULL, qid int NOT"
" NULL, name varchar(255), difficulty int, importance int, "
"state int DEFAULT 0, UNIQUE(chat_id, qid));"),
("CREATE TABLE IF NOT EXISTS side_quests(chat_id int NOT NULL, qid int "
"NOT NULL, name varchar(255), difficulty int, importance int, "
"state int DEFAULT 0, UNIQUE(chat_id, qid));"),
("CREATE TABLE IF NOT EXISTS points(chat_id int PRIMARY KEY, points "
"int);"),
("CREATE TABLE IF NOT EXISTS state(chat_id int PRIMARY KEY, state "
"varchar(10), extra varchar(10));"),
]
for query in queries:
cursor.execute(query)
# Set up database and tables
db = sqlite3.connect("questable.db", check_same_thread=False)
cursor = db.cursor()
with open('schema.sql') as f:
cursor.executescript(f.read())
db.commit()
updater = Updater(token=config.api_key)
updater = Updater(token=config.api_key, use_context=False)
dispatcher = updater.dispatcher
start_handler = CommandHandler('start', start)
dispatcher.add_handler(start_handler)
me = CommandHandler('me', lambda x, y: me_handler(x, y, db))
dispatcher.add_handler(me)
cancel = CommandHandler('cancel', lambda x, y: me_handler(x, y, db))
dispatcher.add_handler(cancel)
help_h = CommandHandler('help', lambda x, y: help_command(x, y, db))
dispatcher.add_handler(help_h)
handler = MessageHandler(Filters.text, lambda x, y: message_handling(x, y, db))
dispatcher.add_handler(handler)
quest_handler = RegexHandler(r"/[Ss]?[Qq]_\d+", lambda x, y:
quest_handling(x, y, db))
dispatcher.add_handler(quest_handler)
unknown = MessageHandler(Filters.command, lambda x, y: message_handling(x, y,
db))
dispatcher.add_handler(unknown)
dispatcher.add_handler(CommandHandler('start', start))
dispatcher.add_handler(CommandHandler('me', lambda x, y: me_handler(x, y, db)))
dispatcher.add_handler(CommandHandler('rate', lambda x, y:
rate_command(x, y, db)))
dispatcher.add_handler(CommandHandler('cancel', lambda x, y: me_handler(x, y,
db)))
dispatcher.add_handler(CommandHandler('help', lambda x, y: help_command(x, y,
db)))
dispatcher.add_handler(RegexHandler(r"/[Ss]?[Qq]_\d+", lambda x, y:
quest_handling(x, y, db)))
dispatcher.add_handler(MessageHandler(Filters.command, lambda x, y:
message_handling(x, y, db)))
dispatcher.add_handler(MessageHandler(Filters.text, lambda x, y:
message_handling(x, y, db)))
if config.update_method == "polling":
updater.start_polling()

23
button_groups.py Normal file
View File

@ -0,0 +1,23 @@
main = [
['❇️ Add Quest', '📯 Add Side Quest'],
['📜 List Quests', '📃 List Side Quests'],
['🏅 Player Status', '🔑 Tokens']
]
importance = [["🔹 Low", "🔸 Medium", "🔺 High"]]
difficulty = [["📙 Low", "📘 Medium", "📗 High"]]
def quests(cat):
return [
["✅ Mark as done"],
["📝 Edit Name", "⚠️ Change Priority"],
["📚 Change Difficulty", "🗑 Delete " +
{"quest": "Quest", "side_quest": "Side Quest"}[cat]],
["⬅️ Back"]]
tokens = [
["📋 List tokens"],
["🔑 Generate token", "🧹 Delete token"]
]

4
errors.py Normal file
View File

@ -0,0 +1,4 @@
_400 = {"error": "insufficient parameters"}
_400_bv = {"error": "bad values"}
_401 = {"error": "invalid token"}
_404 = {"error": "The quest/side quest doesn't exist"}

View File

@ -1,3 +1,6 @@
import uuid
class base_quest():
TABLE = None
@ -6,7 +9,7 @@ class base_quest():
self.DB = db
self.CHAT_ID = chat_id
self.name = name
self.QID = qid
self.QID = int(qid)
self.imp = imp
self.diff = diff
self.state = state
@ -41,6 +44,9 @@ class base_quest():
cursor.execute(query, (self.CHAT_ID, self.QID))
self.DB.commit()
def __str__(self):
return f"{self.QID}: {self.name}"
class quest(base_quest):
TABLE = "quests"
@ -150,3 +156,57 @@ class player():
q = side_quest(self.DB, *row)
quests.append(q)
return quests
def get_quest(self, qid):
cursor = self.DB.cursor()
query = ('SELECT chat_id, qid, name, importance, difficulty, '
'state FROM quests WHERE chat_id = ? AND qid = ?')
cursor.execute(query, (self.CHAT_ID, qid))
row = cursor.fetchone()
if row is None:
return False
else:
return quest(self.DB, *row)
def get_side_quest(self, qid):
cursor = self.DB.cursor()
query = ('SELECT chat_id, qid, name, importance, difficulty, '
'state FROM side_quests WHERE chat_id = ? AND qid = ?')
cursor.execute(query, (self.CHAT_ID, qid))
row = cursor.fetchone()
if row is None:
return False
else:
return side_quest(self.DB, *row)
def get_tokens(self):
cursor = self.DB.cursor()
query = ('SELECT token FROM tokens WHERE chat_id=?')
cursor.execute(query, (self.CHAT_ID,))
tokens = list(map(lambda x: x[0], cursor))
return tokens
def add_token(self):
cursor = self.DB.cursor()
token = str(uuid.uuid4())
query = ('INSERT INTO tokens(chat_id, token) values(?, ?)')
cursor.execute(query, (self.CHAT_ID, token))
self.DB.commit()
return token
def delete_token(self, token):
cursor = self.DB.cursor()
query = ('DELETE FROM tokens WHERE chat_id = ? AND token = ?')
cursor.execute(query, (self.CHAT_ID, token))
self.DB.commit()
def get_player_from_token(db, token):
cursor = db.cursor()
query = "SELECT chat_id FROM tokens WHERE token=?"
cursor.execute(query, (token,))
chat_id = cursor.fetchone()
if chat_id is None:
return False
else:
return player(db, chat_id[0])

39
schema.sql Normal file
View File

@ -0,0 +1,39 @@
CREATE TABLE IF NOT EXISTS quests(
chat_id int NOT NULL,
qid int NOT NULL,
name varchar(255),
difficulty int,
importance int,
state int DEFAULT 0,
UNIQUE(chat_id, qid)
);
CREATE TABLE IF NOT EXISTS side_quests(
chat_id int NOT NULL,
qid int NOT NULL,
name varchar(255),
difficulty int,
importance int,
state int DEFAULT 0,
UNIQUE(chat_id, qid)
);
CREATE TABLE IF NOT EXISTS points(
chat_id int PRIMARY KEY,
points int
);
CREATE TABLE IF NOT EXISTS state(
chat_id int PRIMARY KEY,
state varchar(10),
extra varchar(10)
);
CREATE TABLE IF NOT EXISTS tokens(
chat_id int,
token varchar(36) PRIMARY KEY
);

375
server.py Normal file
View File

@ -0,0 +1,375 @@
#!/usr/bin/env python3
import questable
import sqlite3
import errors
from flask import Flask, jsonify, request, redirect
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
db = sqlite3.connect("questable.db", check_same_thread=False)
# Returns the player object if valid token
def get_player(db):
try:
token = request.values['token']
except (AttributeError):
return False
except (KeyError):
return False
return questable.get_player_from_token(db, token)
# /auth.
def auth(db):
if get_player(db) is False:
return jsonify({"success": False})
else:
return jsonify({"success": True})
app.add_url_rule('/auth', '/auth', lambda: auth(db), methods=['GET'])
# /player
def player(db):
player = get_player(db)
if player is False:
return jsonify(errors._401), 401
noq = len(player.get_quests(None))
nosq = len(player.get_side_quests(None))
return jsonify({
"xp": player.get_points(),
"quests_completed": noq - len(player.get_quests()),
"total_quests": noq,
"side_quests_completed": nosq - len(player.get_side_quests()),
"total_side_quests": nosq,
})
app.add_url_rule('/player', '/player', lambda: player(db), methods=['GET'])
def dictify_quest(quest):
return {
"id": quest.QID,
"name": quest.name,
"difficulty": quest.diff,
"priority": quest.imp,
"state": [False, True][quest.state]
}
# /get_quests
def get_quests(db):
player = get_player(db)
if player is False:
return jsonify(errors._401), 401
quests = list(map(dictify_quest, player.get_quests()))
return jsonify(quests)
app.add_url_rule('/get_quests', '/get_quests', lambda: get_quests(db),
methods=['GET'])
# /get_side_quests
def get_side_quests(db):
player = get_player(db)
if player is False:
return jsonify(errors._401), 401
side_quests = list(map(dictify_quest, player.get_side_quests()))
return jsonify(side_quests)
app.add_url_rule('/get_side_quests', '/get_side_quests',
lambda: get_side_quests(db), methods=['GET'])
# /get_quest
def get_quest(db):
player = get_player(db)
if player is False:
return jsonify(errors._401), 401
try:
qid = request.values['id']
except(KeyError):
return jsonify(errors._400), 400
quest = player.get_quest(qid)
if quest is False:
return jsonify(errors._404), 404
return jsonify(dictify_quest(quest))
app.add_url_rule('/get_quest', '/get_quest', lambda: get_quest(db),
methods=['GET'])
# /get_side_quest
def get_side_quest(db):
player = get_player(db)
if player is False:
return jsonify(errors._401), 401
try:
qid = request.values['id']
except(KeyError):
return jsonify(errors._400), 400
side_quest = player.get_side_quest(qid)
if side_quest is False:
return jsonify(errors._404), 404
return jsonify(dictify_quest(side_quest))
app.add_url_rule('/get_side_quest', '/get_side_quest',
lambda: get_side_quest(db), methods=['GET'])
# /add_quest
def add_quest(db):
player = get_player(db)
if player is False:
return jsonify(errors._401), 401
x = player.get_quests(None)
if len(x) == 0:
qid = 1
else:
x.sort(key=lambda i: i.QID, reverse=True)
qid = x[0].QID + 1
try:
name = request.values['name']
imp = int(request.values['priority'])
diff = int(request.values['difficulty'])
except (KeyError):
return jsonify(errors._400), 400
except (ValueError):
return jsonify(errors._400_bv), 400
if imp not in [1, 2, 3] or diff not in [1, 2, 3]:
return jsonify(errors._400_bv), 400
quest = questable.add_quest(db, player.CHAT_ID, qid, name, imp, diff, 0)
return jsonify(dictify_quest(quest))
app.add_url_rule('/add_quest', '/add_quest', lambda: add_quest(db),
methods=['POST'])
# /add_side_quest
def add_side_quest(db):
player = get_player(db)
if player is False:
return jsonify(errors._401), 401
x = player.get_side_quests(None)
if len(x) == 0:
qid = 1
else:
x.sort(key=lambda i: i.QID, reverse=True)
qid = x[0].QID + 1
try:
name = request.values['name']
imp = int(request.values['priority'])
diff = int(request.values['difficulty'])
except (KeyError):
return jsonify(errors._400), 400
except (ValueError):
return jsonify(errors._400_bv), 400
if imp not in [1, 2, 3] or diff not in [1, 2, 3]:
return jsonify(errors._400_bv), 400
quest = questable.add_side_quest(db, player.CHAT_ID, qid, name, imp,
diff, 0)
return jsonify(dictify_quest(quest))
app.add_url_rule('/add_side_quest', '/add_side_quest',
lambda: add_side_quest(db), methods=['POST'])
# /update_quest
def update_quest(db):
player = get_player(db)
if player is False:
return jsonify(errors._401), 401
try:
qid = request.values['id']
except(KeyError):
return jsonify(errors._400), 400
available_keys = [i for i in ['name', 'difficulty', 'priority', 'state']
if i in request.values.keys()]
if len(available_keys) == 0:
return jsonify(errors._400), 400
quest = questable.get_quest(db, player.CHAT_ID, qid)
if quest is False:
return jsonify(errors._404), 404
if quest.state == 1:
return jsonify(dictify_quest(quest))
for i in available_keys:
try:
if i == "name":
quest.name = request.values["name"]
elif i == "difficulty":
diff = int(request.values["difficulty"])
if diff in [1, 2, 3]:
quest.diff = diff
else:
return jsonify(errors._400_bv), 400
elif i == "priority":
imp = int(request.values["priority"])
if imp in [1, 2, 3]:
quest.imp = imp
else:
return jsonify(errors._400_bv), 400
elif i == "state":
state = (True if str(request.values['state']).lower()
in ["1", "true"] else False)
if state is True:
quest.state = 1
else:
return jsonify(errors._400_bv), 400
except (ValueError):
return jsonify(errors._400_bv), 400
if quest.state == 1:
points = 55 + 10*quest.imp + 15*quest.diff
player.add_points(points)
quest.update_db()
return jsonify(dictify_quest(quest))
app.add_url_rule('/update_quest', '/update_quest', lambda: update_quest(db),
methods=['POST'])
# /update_side_quest
def update_side_quest(db):
player = get_player(db)
if player is False:
return jsonify(errors._401), 401
try:
qid = request.values['id']
except(KeyError):
return jsonify(errors._400), 400
available_keys = [i for i in ['name', 'difficulty', 'priority', 'state']
if i in request.values.keys()]
if len(available_keys) == 0:
return jsonify(errors._400), 400
quest = questable.get_side_quest(db, player.CHAT_ID, qid)
if quest is False:
return jsonify(errors._404), 404
if quest.state == 1:
return jsonify(dictify_quest(quest))
for i in available_keys:
try:
if i == "name":
quest.name = request.values["name"]
elif i == "difficulty":
diff = int(request.values["difficulty"])
if diff in [1, 2, 3]:
quest.diff = diff
else:
return jsonify(errors._400_bv), 400
elif i == "priority":
imp = int(request.values["priority"])
if imp in [1, 2, 3]:
quest.imp = imp
else:
return jsonify(errors._400_bv), 400
elif i == "state":
state = (True if str(request.values['state']).lower()
in ["1", "true"] else False)
if state is True:
quest.state = 1
else:
return jsonify(errors._400_bv), 400
except (ValueError):
return jsonify(errors._400_bv), 400
if quest.state == 1:
points = 10*quest.imp + 15*quest.diff
player.add_points(points)
quest.update_db()
return jsonify(dictify_quest(quest))
app.add_url_rule('/update_side_quest', '/update_side_quest',
lambda: update_side_quest(db), methods=['POST'])
# /delete_quest
def delete_quest(db):
player = get_player(db)
if player is False:
return jsonify(errors._401), 401
try:
qid = request.values['id']
except(KeyError):
return jsonify(errors._400), 400
try:
quest = questable.get_quest(db, player.CHAT_ID, qid)
except Exception:
return jsonify(errors._404), 404
if quest.state == 1:
return jsonify({"success": False})
quest.delete_from_db()
return jsonify({"success": True})
app.add_url_rule('/delete_quest', '/delete_quest',
lambda: delete_quest(db), methods=['DELETE'])
# /delete_side_quest
def delete_side_quest(db):
player = get_player(db)
if player is False:
return jsonify(errors._401), 401
try:
qid = request.values['id']
except(KeyError):
return jsonify(errors._400), 400
try:
side_quest = questable.get_side_quest(db, player.CHAT_ID, qid)
except Exception:
return jsonify(errors._404), 404
if side_quest.state == 1:
return jsonify({"success": False})
side_quest.delete_from_db()
return jsonify({"success": True})
app.add_url_rule('/delete_side_quest', '/delete_side_quest',
lambda: delete_side_quest(db), methods=['DELETE'])
@app.route('/')
def redirect_to_docs():
return redirect("https://questable.webionite.com/api", code=301)