1
0
mirror of https://gitlab.com/simple-stock-bots/simple-stock-bot.git synced 2025-06-16 07:16:40 +00:00

Merge branch '99-inline-functionality-is-broken' into 'master'

Resolve "inline functionality is broken"

Closes #99

See merge request simple-stock-bots/simple-telegram-stock-bot!48
This commit is contained in:
Anson Biggs 2023-09-07 04:51:51 +00:00
commit ad4262777d
No known key found for this signature in database
12 changed files with 184 additions and 178 deletions

View File

@ -1,20 +1,3 @@
FROM python:3.11-buster AS builder FROM python:3.11
COPY requirements.txt /requirements.txt
RUN pip install --user -r requirements.txt
FROM python:3.11-slim
ENV MPLBACKEND=Agg
COPY --from=builder /root/.local /root/.local
RUN pip install --no-cache-dir black
ENV TELEGRAM=TOKEN
ENV MARKETDATA=TOKEN
COPY . . COPY . .
# CMD [ "python", "./bot.py" ]

View File

@ -1,26 +1,39 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // For format details, see https://aka.ms/devcontainer.json. For config options, see the
// https://github.com/microsoft/vscode-dev-containers/tree/v0.191.0/containers/docker-existing-dockerfile // README at: https://github.com/devcontainers/templates/tree/main/src/python
{ {
"name": "DockerDev", "name": "Python 3",
// Sets the run context to one level up instead of the .devcontainer folder. // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"context": "..", // "image": "mcr.microsoft.com/devcontainers/python:1-3-bookworm",
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. "build": {
"dockerFile": "Dockerfile", "dockerfile": "Dockerfile"
// Set *default* container specific settings.json values on container create. },
"settings": {}, "features": {
// Add the IDs of extensions you want installed when the container is created. "ghcr.io/devcontainers-contrib/features/black:2": {},
"extensions": [ "ghcr.io/devcontainers-contrib/features/mypy:2": {},
"ghcr.io/devcontainers-contrib/features/pylint:2": {},
"ghcr.io/devcontainers/features/docker-in-docker": {}
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python", "ms-python.python",
"ms-azuretools.vscode-docker" "ms-python.black-formatter",
"ms-python.flake8",
"ms-python.vscode-pylance",
"ms-python.isort",
"charliermarsh.ruff"
] ]
// Use 'forwardPorts' to make a list of ports inside the container available locally. }
// "forwardPorts": [], },
// Uncomment the next line to run commands after the container is created - for example installing curl. "postCreateCommand": "pip3 install --user -r dev-reqs.txt"
// "postCreateCommand": "apt-get update && apt-get install -y curl", // Features to add to the dev container. More info: https://containers.dev/features.
// Uncomment when using a ptrace-based debugger like C++, Go, and Rust // "features": {},
// "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], // Use 'forwardPorts' to make a list of ports inside the container available locally.
// Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker. // "forwardPorts": [],
// "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ], // Use 'postCreateCommand' to run commands after the container is created.
// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. // "postCreateCommand": "pip3 install --user -r requirements.txt",
// "remoteUser": "vscode" // Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
} }

12
.vscode/launch.json vendored
View File

@ -1,12 +0,0 @@
{
"configurations": [
{
"name": "Telegram Bot",
"type": "python",
"request": "launch",
"program": "bot.py",
"console": "integratedTerminal",
"python": "python3.11"
}
]
}

View File

@ -1,5 +1,8 @@
{ {
"python.formatting.provider": "black", "editor.formatOnSave": true,
"python.linting.mypyEnabled": true, "editor.formatOnPaste": true,
"python.linting.flake8Enabled": true, "editor.formatOnSaveMode": "modificationsIfAvailable",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
}
} }

View File

@ -103,7 +103,7 @@ class MarketData:
) )
status.raise_for_status() status.raise_for_status()
except r.HTTPError: except r.HTTPError:
return f"API returned an HTTP error code {status.status_code} in {status.elapsed.total_seconds()} Seconds." return f"API returned an HTTP error code {status.status_code} in {status.elapsed.total_seconds()} seconds."
except r.Timeout: except r.Timeout:
return "API timed out before it was able to give status. This is likely due to a surge in usage or a complete outage." return "API timed out before it was able to give status. This is likely due to a surge in usage or a complete outage."
@ -111,7 +111,7 @@ class MarketData:
if statusJSON["status"] == "ok": if statusJSON["status"] == "ok":
return ( return (
f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} Seconds." f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} seconds."
) )
else: else:
return f"MarketData.app is currently reporting the following status: {statusJSON['status']}" return f"MarketData.app is currently reporting the following status: {statusJSON['status']}"
@ -131,7 +131,11 @@ class MarketData:
if quoteResp := self.get(f"stocks/quotes/{symbol}/"): if quoteResp := self.get(f"stocks/quotes/{symbol}/"):
price = round(quoteResp["last"][0], 2) price = round(quoteResp["last"][0], 2)
changePercent = round(quoteResp["changepct"][0], 2)
try:
changePercent = round(quoteResp["changepct"][0], 2)
except TypeError:
return f"The price of {symbol} is {price}"
message = f"The current price of {symbol.name} is ${price} and " message = f"The current price of {symbol.name} is ${price} and "
@ -148,11 +152,13 @@ class MarketData:
def spark_reply(self, symbol: Stock) -> str: def spark_reply(self, symbol: Stock) -> str:
if quoteResp := self.get(f"stocks/quotes/{symbol}/"): if quoteResp := self.get(f"stocks/quotes/{symbol}/"):
changePercent = round(quoteResp["changepct"][0], 2) try:
return f"`{symbol.tag}`: {changePercent}%" changePercent = round(quoteResp["changepct"][0], 2)
else: return f"`{symbol.tag}`: {changePercent}%"
logging.warning(f"{symbol} did not have 'changepct' field.") except TypeError:
return f"`{symbol.tag}`" pass
return f"`{symbol.tag}`"
def intra_reply(self, symbol: Stock) -> pd.DataFrame: def intra_reply(self, symbol: Stock) -> pd.DataFrame:
"""Returns price data for a symbol of the past month up until the previous trading days close. """Returns price data for a symbol of the past month up until the previous trading days close.

View File

@ -77,10 +77,10 @@ class cg_Crypto:
try: try:
status.raise_for_status() status.raise_for_status()
return ( return (
f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} Seconds." f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} seconds."
) )
except r.HTTPError: except r.HTTPError:
return f"CoinGecko API returned an error code {status.status_code} in {status.elapsed.total_seconds()} Seconds." return f"CoinGecko API returned an error code {status.status_code} in {status.elapsed.total_seconds()} seconds."
def price_reply(self, coin: Coin) -> str: def price_reply(self, coin: Coin) -> str:
"""Returns current market price or after hours if its available for a given coin symbol. """Returns current market price or after hours if its available for a given coin symbol.

View File

@ -123,7 +123,8 @@ class Router:
Each tuple contains: (Symbol, Issue Name). Each tuple contains: (Symbol, Issue Name).
""" """
df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list]) # df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list])
df = self.crypto.symbol_list
df = df[df["description"].str.contains(search, regex=False, case=False)].sort_values( df = df[df["description"].str.contains(search, regex=False, case=False)].sort_values(
by="type_id", key=lambda x: x.str.len() by="type_id", key=lambda x: x.str.len()
@ -338,8 +339,8 @@ class Router:
reply += self.spark_reply(self.find_symbols(t))[0] + "\n" reply += self.spark_reply(self.find_symbols(t))[0] + "\n"
if coins: if coins:
reply += "\n\n🦎Trending Crypto:\n`" reply += "\n\n🦎Trending on CoinGecko:\n`"
reply += "" * len("Trending Crypto:") + "`\n" reply += "" * len("Trending on CoinGecko:") + "`\n"
for coin in coins: for coin in coins:
reply += coin + "\n" reply += coin + "\n"
@ -353,7 +354,8 @@ class Router:
return "Trending data is not currently available." return "Trending data is not currently available."
def random_pick(self) -> str: def random_pick(self) -> str:
choice = random.choice(list(self.stock.symbol_list["description"]) + list(self.crypto.symbol_list["description"])) # choice = random.choice(list(self.stock.symbol_list["description"]) + list(self.crypto.symbol_list["description"]))
choice = random.choice(list(self.crypto.symbol_list["description"]))
hold = (datetime.date.today() + datetime.timedelta(random.randint(1, 365))).strftime("%b %d, %Y") hold = (datetime.date.today() + datetime.timedelta(random.randint(1, 365))).strftime("%b %d, %Y")
return f"{choice}\nBuy and hold until: {hold}" return f"{choice}\nBuy and hold until: {hold}"

View File

@ -1,8 +1,9 @@
-r telegram/requirements.txt -r telegram/requirements.txt
black==23.3.0 black==23.7.0
flake8==5.0.4 flake8==6.1.0
Flake8-pyproject==1.2.3 Flake8-pyproject==1.2.3
pylama==8.4.1 pylama==8.4.1
mypy==1.2.0 mypy==1.5.1
types-cachetools==5.3.0.5 types-cachetools==5.3.0.6
types-pytz==2023.3.0.0 types-pytz==2023.3.0.1
ruff==0.0.287

View File

@ -42,7 +42,7 @@ async def status(ctx: commands):
message = "" message = ""
try: try:
message = "Contact MisterBiggs#0465 if you need help.\n" message = "Contact MisterBiggs#0465 if you need help.\n"
message += s.status(f"Bot recieved your message in: {bot.latency*1000:.4f}ms") + "\n" message += s.status(f"Bot recieved your message in: {bot.latency*10:.4f} seconds") + "\n"
except Exception as ex: except Exception as ex:
logging.critical(ex) logging.critical(ex)

View File

@ -6,3 +6,6 @@ max-line-length = 130
[tool.pycodestyle] [tool.pycodestyle]
max_line_length = 130 max_line_length = 130
[tool.ruff]
line-length = 130

View File

@ -18,14 +18,15 @@ from telegram import (
LabeledPrice, LabeledPrice,
Update, Update,
) )
from telegram.ext import ( from telegram.ext import (
CallbackContext, Application,
CommandHandler, CommandHandler,
Filters,
InlineQueryHandler, InlineQueryHandler,
MessageHandler,
PreCheckoutQueryHandler, PreCheckoutQueryHandler,
Updater, MessageHandler,
filters,
ContextTypes,
) )
from common.symbol_router import Router from common.symbol_router import Router
@ -33,6 +34,10 @@ from T_info import T_info
# Enable logging # Enable logging
logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO) logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO)
# set higher logging level for httpx to avoid all GET and POST requests being logged
logging.getLogger("httpx").setLevel(logging.WARNING)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
TELEGRAM_TOKEN = os.environ["TELEGRAM"] TELEGRAM_TOKEN = os.environ["TELEGRAM"]
@ -50,58 +55,58 @@ t = T_info()
log.info("Bot script started.") log.info("Bot script started.")
def start(update: Update, context: CallbackContext): async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send help text when the command /start is issued.""" """Send help text when the command /start is issued."""
log.info(f"Start command ran by {update.message.chat.username}") log.info(f"Start command ran by {update.message.chat.username}")
update.message.reply_text( await update.message.reply_text(
text=t.help_text, text=t.help_text,
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
def help(update: Update, context: CallbackContext): async def help(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send help text when the command /help is issued.""" """Send help text when the command /help is issued."""
log.info(f"Help command ran by {update.message.chat.username}") log.info(f"Help command ran by {update.message.chat.username}")
update.message.reply_text( await update.message.reply_text(
text=t.help_text, text=t.help_text,
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
def license(update: Update, context: CallbackContext): async def license(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send bots license when the /license command is issued.""" """Send bots license when the /license command is issued."""
log.info(f"License command ran by {update.message.chat.username}") log.info(f"License command ran by {update.message.chat.username}")
update.message.reply_text( await update.message.reply_text(
text=t.license, text=t.license,
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
def status(update: Update, context: CallbackContext): async def status(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Gather status of bot and dependant services and return important status updates.""" """Gather status of bot and dependant services and return important status updates."""
log.warning(f"Status command ran by {update.message.chat.username}") log.warning(f"Status command ran by {update.message.chat.username}")
bot_resp_time = datetime.datetime.now(update.message.date.tzinfo) - update.message.date bot_resp_time = datetime.datetime.now(update.message.date.tzinfo) - update.message.date
bot_status = s.status(f"It took {bot_resp_time.total_seconds()} seconds for the bot to get your message.") bot_status = s.status(f"It took {bot_resp_time.total_seconds()} seconds for the bot to get your message.")
update.message.reply_text( await update.message.reply_text(
text=bot_status, text=bot_status,
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
) )
def donate(update: Update, context: CallbackContext): async def donate(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Sets up donation.""" """Sets up donation."""
log.info(f"Donate command ran by {update.message.chat.username}") log.info(f"Donate command ran by {update.message.chat.username}")
chat_id = update.message.chat_id chat_id = update.message.chat_id
if update.message.text.strip() == "/donate" or "/donate@" in update.message.text: if update.message.text.strip() == "/donate" or "/donate@" in update.message.text:
update.message.reply_text( await update.message.reply_text(
text=t.donate_text, text=t.donate_text,
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
amount = 1.0 amount = 1.0
@ -111,11 +116,11 @@ def donate(update: Update, context: CallbackContext):
try: try:
price = int(amount * 100) price = int(amount * 100)
except ValueError: except ValueError:
update.message.reply_text(f"{amount} is not a valid donation amount or number.") await update.message.reply_text(f"{amount} is not a valid donation amount or number.")
return return
log.info(f"Donation amount: {price} by {update.message.chat.username}") log.info(f"Donation amount: {price} by {update.message.chat.username}")
context.bot.send_invoice( await context.bot.send_invoice(
chat_id=chat_id, chat_id=chat_id,
title="Simple Stock Bot Donation", title="Simple Stock Bot Donation",
description=f"Simple Stock Bot Donation of ${amount} by {update.message.chat.username}", description=f"Simple Stock Bot Donation of ${amount} by {update.message.chat.username}",
@ -131,27 +136,27 @@ def donate(update: Update, context: CallbackContext):
) )
def precheckout_callback(update: Update, context: CallbackContext): async def precheckout_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Approves donation""" """Approves donation"""
log.info("precheckout_callback queried") log.info("precheckout_callback queried")
query = update.pre_checkout_query query = update.pre_checkout_query
query.answer(ok=True) await query.answer(ok=True)
# I dont think I need to check since its only donations. # I dont think I need to check since its only donations.
# if query.invoice_payload == "simple-stock-bot": # if query.invoice_payload == "simple-stock-bot":
# # answer False pre_checkout_query # # answer False pre_checkout_query
# query.answer(ok=True) # await query.answer(ok=True)
# else: # else:
# query.answer(ok=False, error_message="Something went wrong...") # await query.answer(ok=False, error_message="Something went wrong...")
def successful_payment_callback(update: Update, context: CallbackContext): async def successful_payment_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Thanks user for donation""" """Thanks user for donation"""
log.info("Successful payment!") log.info("Successful payment!")
update.message.reply_text("Thank you for your donation! It goes a long way to keeping the bot free!") await update.message.reply_text("Thank you for your donation! It goes a long way to keeping the bot free!")
def symbol_detect_image(update: Update, context: CallbackContext): async def symbol_detect_image(update: Update, context: ContextTypes.DEFAULT_TYPE):
""" """
Makes image captions into text then passes the `update` and `context` Makes image captions into text then passes the `update` and `context`
to symbol detect so that it can reply cashtags in image captions. to symbol detect so that it can reply cashtags in image captions.
@ -159,12 +164,12 @@ def symbol_detect_image(update: Update, context: CallbackContext):
try: try:
if update.message.caption: if update.message.caption:
update.message.text = update.message.caption update.message.text = update.message.caption
symbol_detect(update, context) await symbol_detect(update, context)
except AttributeError: except AttributeError:
return return
def symbol_detect(update: Update, context: CallbackContext): async def symbol_detect(update: Update, context: ContextTypes.DEFAULT_TYPE):
""" """
Runs on any message that doesn't have a command and searches for cashtags, Runs on any message that doesn't have a command and searches for cashtags,
then returns the prices of any symbols found. then returns the prices of any symbols found.
@ -182,18 +187,18 @@ def symbol_detect(update: Update, context: CallbackContext):
return return
if symbols: if symbols:
# Let user know bot is working # Let user know bot is working
context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) await context.bot.send_chat_action(chat_id=chat_id, action=telegram.constants.ChatAction.TYPING)
log.info(f"Symbols found: {symbols}") log.info(f"Symbols found: {symbols}")
for reply in s.price_reply(symbols): for reply in s.price_reply(symbols):
update.message.reply_text( await update.message.reply_text(
text=reply, text=reply,
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
def intra(update: Update, context: CallbackContext): async def intra(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""returns a chart of intraday data for a symbol""" """returns a chart of intraday data for a symbol"""
log.info(f"Intra command ran by {update.message.chat.username}") log.info(f"Intra command ran by {update.message.chat.username}")
@ -201,7 +206,7 @@ def intra(update: Update, context: CallbackContext):
chat_id = update.message.chat_id chat_id = update.message.chat_id
if message.strip().split("@")[0] == "/intra": if message.strip().split("@")[0] == "/intra":
update.message.reply_text( await update.message.reply_text(
"This command returns a chart of the stocks movement since the most recent market open.\nExample: /intra $tsla" "This command returns a chart of the stocks movement since the most recent market open.\nExample: /intra $tsla"
) )
return return
@ -212,19 +217,19 @@ def intra(update: Update, context: CallbackContext):
if len(symbols): if len(symbols):
symbol = symbols[0] symbol = symbols[0]
else: else:
update.message.reply_text("No symbols or coins found.") await update.message.reply_text("No symbols or coins found.")
return return
df = s.intra_reply(symbol) df = s.intra_reply(symbol)
if df.empty: if df.empty:
update.message.reply_text( await update.message.reply_text(
text="Invalid symbol please see `/help` for usage details.", text="Invalid symbol please see `/help` for usage details.",
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
return return
context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.UPLOAD_PHOTO) await context.bot.send_chat_action(chat_id=chat_id, action=telegram.constants.ChatAction.UPLOAD_PHOTO)
buf = io.BytesIO() buf = io.BytesIO()
mpf.plot( mpf.plot(
@ -237,17 +242,17 @@ def intra(update: Update, context: CallbackContext):
) )
buf.seek(0) buf.seek(0)
update.message.reply_photo( await update.message.reply_photo(
photo=buf, photo=buf,
caption=f"\nIntraday chart for {symbol.name} from {df.first_valid_index().strftime('%d %b at %H:%M')} to" caption=f"\nIntraday chart for {symbol.name} from {df.first_valid_index().strftime('%d %b at %H:%M')} to"
+ f" {df.last_valid_index().strftime('%d %b at %H:%M %Z')}" + f" {df.last_valid_index().strftime('%d %b at %H:%M %Z')}"
+ f"\n\n{s.price_reply([symbol])[0]}", + f"\n\n{s.price_reply([symbol])[0]}",
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
def chart(update: Update, context: CallbackContext): async def chart(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""returns a chart of the past month of data for a symbol""" """returns a chart of the past month of data for a symbol"""
log.info(f"Chart command ran by {update.message.chat.username}") log.info(f"Chart command ran by {update.message.chat.username}")
@ -255,7 +260,7 @@ def chart(update: Update, context: CallbackContext):
chat_id = update.message.chat_id chat_id = update.message.chat_id
if message.strip().split("@")[0] == "/chart": if message.strip().split("@")[0] == "/chart":
update.message.reply_text( await update.message.reply_text(
"This command returns a chart of the stocks movement for the past month.\nExample: /chart $tsla" "This command returns a chart of the stocks movement for the past month.\nExample: /chart $tsla"
) )
return return
@ -265,18 +270,18 @@ def chart(update: Update, context: CallbackContext):
if len(symbols): if len(symbols):
symbol = symbols[0] symbol = symbols[0]
else: else:
update.message.reply_text("No symbols or coins found.") await update.message.reply_text("No symbols or coins found.")
return return
df = s.chart_reply(symbol) df = s.chart_reply(symbol)
if df.empty: if df.empty:
update.message.reply_text( await update.message.reply_text(
text="Invalid symbol please see `/help` for usage details.", text="Invalid symbol please see `/help` for usage details.",
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
return return
context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.UPLOAD_PHOTO) await context.bot.send_chat_action(chat_id=chat_id, action=telegram.constants.ChatAction.UPLOAD_PHOTO)
buf = io.BytesIO() buf = io.BytesIO()
mpf.plot( mpf.plot(
@ -289,39 +294,42 @@ def chart(update: Update, context: CallbackContext):
) )
buf.seek(0) buf.seek(0)
update.message.reply_photo( await update.message.reply_photo(
photo=buf, photo=buf,
caption=f"\n1 Month chart for {symbol.name} from {df.first_valid_index().strftime('%d, %b %Y')}" caption=f"\n1 Month chart for {symbol.name} from {df.first_valid_index().strftime('%d, %b %Y')}"
+ f" to {df.last_valid_index().strftime('%d, %b %Y')}\n\n{s.price_reply([symbol])[0]}", + f" to {df.last_valid_index().strftime('%d, %b %Y')}\n\n{s.price_reply([symbol])[0]}",
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
def trending(update: Update, context: CallbackContext): async def trending(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""returns currently trending symbols and how much they've moved in the past trading day.""" """returns currently trending symbols and how much they've moved in the past trading day."""
log.info(f"Trending command ran by {update.message.chat.username}") log.info(f"Trending command ran by {update.message.chat.username}")
chat_id = update.message.chat_id chat_id = update.message.chat_id
context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) await context.bot.send_chat_action(chat_id=chat_id, action=telegram.constants.ChatAction.TYPING)
trending_list = s.trending() trending_list = s.trending()
update.message.reply_text( await update.message.reply_text(
text=trending_list, text=trending_list,
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
def inline_query(update: Update, context: CallbackContext): async def inline_query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
""" """
Handles inline query. Searches by looking if query is contained Handles inline query. Searches by looking if query is contained
in the symbol and returns matches in alphabetical order. in the symbol and returns matches in alphabetical order.
""" """
# info(f"Inline command ran by {update.message.chat.username}")
log.info(f"Query: {update.inline_query.query}") if not update.inline_query.query:
return
print(f"Query: {update.inline_query.query}")
ignored_queries = {"$", "$$", " ", ""} ignored_queries = {"$", "$$", " ", ""}
@ -331,12 +339,14 @@ def inline_query(update: Update, context: CallbackContext):
in any chat or direct message to search for the stock bots full list of stock and crypto symbols and return the price. in any chat or direct message to search for the stock bots full list of stock and crypto symbols and return the price.
""" """
update.inline_query.answer( await update.inline_query.answer(
[ [
InlineQueryResultArticle( InlineQueryResultArticle(
str(uuid4()), str(uuid4()),
title="Please enter a query. It can be a ticker or a name of a company.", title="Please enter a query. It can be a ticker or a name of a company.",
input_message_content=InputTextMessageContent(default_message, parse_mode=telegram.ParseMode.MARKDOWN), input_message_content=InputTextMessageContent(
default_message, parse_mode=telegram.constants.ParseMode.MARKDOWN
),
) )
] ]
) )
@ -349,29 +359,31 @@ def inline_query(update: Update, context: CallbackContext):
InlineQueryResultArticle( InlineQueryResultArticle(
str(uuid4()), str(uuid4()),
title=row["description"], title=row["description"],
input_message_content=InputTextMessageContent(row["price_reply"], parse_mode=telegram.ParseMode.MARKDOWN), input_message_content=InputTextMessageContent(
row["price_reply"], parse_mode=telegram.constants.ParseMode.MARKDOWN
),
) )
) )
if len(results) == 5: if len(results) == 5:
update.inline_query.answer(results, cache_time=60 * 60) await update.inline_query.answer(results, cache_time=60 * 60)
log.info("Inline Command was successful") log.info("Inline Command was successful")
return return
update.inline_query.answer(results) await update.inline_query.answer(results)
def rand_pick(update: Update, context: CallbackContext): async def rand_pick(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""For the gamblers. Returns a random symbol to buy and a sell date""" """For the gamblers. Returns a random symbol to buy and a sell date"""
log.info(f"Someone is gambling! Random_pick command ran by {update.message.chat.username}") log.info(f"Someone is gambling! Random_pick command ran by {update.message.chat.username}")
update.message.reply_text( await update.message.reply_text(
text=s.random_pick(), text=s.random_pick(),
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
def error(update: Update, context: CallbackContext): async def error(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Log Errors caused by Updates.""" """Log Errors caused by Updates."""
log.warning('Update "%s" caused error "%s"', update, error) log.warning('Update "%s" caused error "%s"', update, error)
@ -390,9 +402,9 @@ def error(update: Update, context: CallbackContext):
f"\t{html.escape(tb_string)}" f"\t{html.escape(tb_string)}"
) )
update.message.reply_text( await update.message.reply_text(
text=f"An error has occured. Please inform @MisterBiggs if the error persists. Error Code: `{err_code}`", text=f"An error has occured. Please inform @MisterBiggs if the error persists. Error Code: `{err_code}`",
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.constants.ParseMode.MARKDOWN,
) )
else: else:
log.warning("No message to send to user.") log.warning("No message to send to user.")
@ -402,48 +414,43 @@ def error(update: Update, context: CallbackContext):
def main(): def main():
"""Start the context.bot.""" """Start the context.bot."""
# Create the EventHandler and pass it your bot's token. # Create the EventHandler and pass it your bot's token.
updater = Updater(TELEGRAM_TOKEN) application = Application.builder().token(TELEGRAM_TOKEN).build()
# Get the dispatcher to register handlers
dp = updater.dispatcher
# on different commands - answer in Telegram # on different commands - answer in Telegram
dp.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("start", start))
dp.add_handler(CommandHandler("help", help)) application.add_handler(CommandHandler("help", help))
dp.add_handler(CommandHandler("license", license)) application.add_handler(CommandHandler("license", license))
dp.add_handler(CommandHandler("trending", trending)) application.add_handler(CommandHandler("trending", trending))
dp.add_handler(CommandHandler("random", rand_pick)) application.add_handler(CommandHandler("random", rand_pick))
dp.add_handler(CommandHandler("donate", donate)) application.add_handler(CommandHandler("donate", donate))
dp.add_handler(CommandHandler("status", status)) application.add_handler(CommandHandler("status", status))
dp.add_handler(CommandHandler("inline", inline_query)) application.add_handler(CommandHandler("inline", inline_query))
# Charting can be slow so they run async. # Charting can be slow so they run async.
dp.add_handler(CommandHandler("intra", intra, run_async=True)) application.add_handler(CommandHandler("intra", intra, block=False))
dp.add_handler(CommandHandler("intraday", intra, run_async=True)) application.add_handler(CommandHandler("intraday", intra, block=False))
dp.add_handler(CommandHandler("day", intra, run_async=True)) application.add_handler(CommandHandler("day", intra, block=False))
dp.add_handler(CommandHandler("chart", chart, run_async=True)) application.add_handler(CommandHandler("chart", chart, block=False))
dp.add_handler(CommandHandler("month", chart, run_async=True)) application.add_handler(CommandHandler("month", chart, block=False))
# on noncommand i.e message - echo the message on Telegram # on noncommand i.e message - echo the message on Telegram
dp.add_handler(MessageHandler(Filters.text, symbol_detect)) application.add_handler(MessageHandler(filters.TEXT, symbol_detect))
dp.add_handler(MessageHandler(Filters.photo, symbol_detect_image)) application.add_handler(MessageHandler(filters.PHOTO, symbol_detect_image))
# Inline Bot commands # Inline Bot commands
dp.add_handler(InlineQueryHandler(inline_query)) application.add_handler(InlineQueryHandler(inline_query))
# Pre-checkout handler to final check # Pre-checkout handler to final check
dp.add_handler(PreCheckoutQueryHandler(precheckout_callback)) application.add_handler(PreCheckoutQueryHandler(precheckout_callback))
# Payment success # Payment success
dp.add_handler(MessageHandler(Filters.successful_payment, successful_payment_callback)) application.add_handler(MessageHandler(filters.SUCCESSFUL_PAYMENT, successful_payment_callback))
# log all errors # log all errors
dp.add_error_handler(error) application.add_error_handler(error)
# Start the Bot # Start the Bot
updater.start_polling() application.run_polling(allowed_updates=Update.ALL_TYPES)
updater.idle()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,7 +1,7 @@
python-telegram-bot==13.5 python-telegram-bot==20.5
requests==2.25.1 requests==2.31.0
pandas==2.0.0 pandas==2.1.0
schedule==1.0.0 schedule==1.2.0
mplfinance==0.12.7a5 mplfinance==0.12.10b0
markdownify==0.6.5 markdownify==0.11.6
cachetools==4.2.2 cachetools==5.3.1