mirror of
https://github.com/Steffo99/greed.git
synced 2024-11-29 00:54:18 +00:00
Fix #27 by removing the character limit on regexes
This commit is contained in:
parent
16e6b16fb0
commit
2b23bd5661
2 changed files with 52 additions and 51 deletions
29
utils.py
29
utils.py
|
@ -28,7 +28,8 @@ else:
|
||||||
class Price:
|
class Price:
|
||||||
"""The base class for the prices in greed.
|
"""The base class for the prices in greed.
|
||||||
Its int value is in minimum units, while its float and str values are in decimal format.int("""
|
Its int value is in minimum units, while its float and str values are in decimal format.int("""
|
||||||
def __init__(self, value: typing.Union[int, float, str, "Price"]=0):
|
|
||||||
|
def __init__(self, value: typing.Union[int, float, str, "Price"] = 0):
|
||||||
if isinstance(value, int):
|
if isinstance(value, int):
|
||||||
# Keep the value as it is
|
# Keep the value as it is
|
||||||
self.value = int(value)
|
self.value = int(value)
|
||||||
|
@ -47,8 +48,8 @@ class Price:
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return strings.currency_format_string.format(symbol=strings.currency_symbol,
|
return strings.currency_format_string.format(symbol=strings.currency_symbol,
|
||||||
value="{0:.2f}".format(
|
value="{0:.2f}".format(
|
||||||
self.value / (10 ** int(config["Payments"]["currency_exp"]))))
|
self.value / (10 ** int(config["Payments"]["currency_exp"]))))
|
||||||
|
|
||||||
def __int__(self):
|
def __int__(self):
|
||||||
return self.value
|
return self.value
|
||||||
|
@ -112,14 +113,15 @@ class Price:
|
||||||
|
|
||||||
|
|
||||||
def telegram_html_escape(string: str):
|
def telegram_html_escape(string: str):
|
||||||
return string.replace("<", "<")\
|
return string.replace("<", "<") \
|
||||||
.replace(">", ">")\
|
.replace(">", ">") \
|
||||||
.replace("&", "&")\
|
.replace("&", "&") \
|
||||||
.replace('"', """)
|
.replace('"', """)
|
||||||
|
|
||||||
|
|
||||||
def catch_telegram_errors(func):
|
def catch_telegram_errors(func):
|
||||||
"""Decorator, can be applied to any function to retry in case of Telegram errors."""
|
"""Decorator, can be applied to any function to retry in case of Telegram errors."""
|
||||||
|
|
||||||
def result_func(*args, **kwargs):
|
def result_func(*args, **kwargs):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
@ -137,9 +139,8 @@ def catch_telegram_errors(func):
|
||||||
except telegram.error.NetworkError as error:
|
except telegram.error.NetworkError as error:
|
||||||
print(f"Network error while calling {func.__name__}(),"
|
print(f"Network error while calling {func.__name__}(),"
|
||||||
f" retrying in {config['Telegram']['error_pause']} secs...")
|
f" retrying in {config['Telegram']['error_pause']} secs...")
|
||||||
# Display the full NetworkError if in debug mode
|
# Display the full NetworkError
|
||||||
if __debug__:
|
print(f"Full error: {error.message}")
|
||||||
print(f"Full error: {error.message}")
|
|
||||||
time.sleep(int(config["Telegram"]["error_pause"]))
|
time.sleep(int(config["Telegram"]["error_pause"]))
|
||||||
# Unknown error
|
# Unknown error
|
||||||
except telegram.error.TelegramError as error:
|
except telegram.error.TelegramError as error:
|
||||||
|
@ -154,13 +155,13 @@ def catch_telegram_errors(func):
|
||||||
else:
|
else:
|
||||||
print(f"Telegram error while calling {func.__name__}(),"
|
print(f"Telegram error while calling {func.__name__}(),"
|
||||||
f" retrying in {config['Telegram']['error_pause']} secs...")
|
f" retrying in {config['Telegram']['error_pause']} secs...")
|
||||||
# Display the full TelegramError if in debug mode
|
# Display the full TelegramError
|
||||||
if __debug__:
|
print(f"Full error: {error.message}")
|
||||||
print(f"Full error: {error.message}")
|
|
||||||
# Send the error to the Sentry server
|
# Send the error to the Sentry server
|
||||||
elif sentry_client is not None:
|
if sentry_client is not None:
|
||||||
sentry_client.captureException(exc_info=sys.exc_info())
|
sentry_client.captureException(exc_info=sys.exc_info())
|
||||||
time.sleep(int(config["Telegram"]["error_pause"]))
|
time.sleep(int(config["Telegram"]["error_pause"]))
|
||||||
|
|
||||||
return result_func
|
return result_func
|
||||||
|
|
||||||
|
|
||||||
|
|
74
worker.py
74
worker.py
|
@ -1,5 +1,5 @@
|
||||||
import threading
|
import threading
|
||||||
import typing
|
from typing import *
|
||||||
import uuid
|
import uuid
|
||||||
import datetime
|
import datetime
|
||||||
import telegram
|
import telegram
|
||||||
|
@ -17,10 +17,11 @@ import importlib
|
||||||
language = configloader.config["Config"]["language"]
|
language = configloader.config["Config"]["language"]
|
||||||
strings = importlib.import_module("strings." + language)
|
strings = importlib.import_module("strings." + language)
|
||||||
|
|
||||||
|
|
||||||
class StopSignal:
|
class StopSignal:
|
||||||
"""A data class that should be sent to the worker when the conversation has to be stopped abnormally."""
|
"""A data class that should be sent to the worker when the conversation has to be stopped abnormally."""
|
||||||
|
|
||||||
def __init__(self, reason: str=""):
|
def __init__(self, reason: str = ""):
|
||||||
self.reason = reason
|
self.reason = reason
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,15 +42,15 @@ class ChatWorker(threading.Thread):
|
||||||
# Open a new database session
|
# Open a new database session
|
||||||
self.session = db.Session()
|
self.session = db.Session()
|
||||||
# Get the user db data from the users and admin tables
|
# Get the user db data from the users and admin tables
|
||||||
self.user: typing.Optional[db.User] = None
|
self.user: Optional[db.User] = None
|
||||||
self.admin: typing.Optional[db.Admin] = None
|
self.admin: Optional[db.Admin] = None
|
||||||
# The sending pipe is stored in the ChatWorker class, allowing the forwarding of messages to the chat process
|
# The sending pipe is stored in the ChatWorker class, allowing the forwarding of messages to the chat process
|
||||||
self.queue = queuem.Queue()
|
self.queue = queuem.Queue()
|
||||||
# The current active invoice payload; reject all invoices with a different payload
|
# The current active invoice payload; reject all invoices with a different payload
|
||||||
self.invoice_payload = None
|
self.invoice_payload = None
|
||||||
# The Sentry client for reporting errors encountered by the user
|
# The Sentry client for reporting errors encountered by the user
|
||||||
if configloader.config["Error Reporting"]["sentry_token"] != \
|
if configloader.config["Error Reporting"]["sentry_token"] != \
|
||||||
"https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000":
|
"https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000":
|
||||||
import raven
|
import raven
|
||||||
self.sentry_client = raven.Client(configloader.config["Error Reporting"]["sentry_token"],
|
self.sentry_client = raven.Client(configloader.config["Error Reporting"]["sentry_token"],
|
||||||
release=raven.fetch_git_sha(os.path.dirname(__file__)),
|
release=raven.fetch_git_sha(os.path.dirname(__file__)),
|
||||||
|
@ -93,7 +94,7 @@ class ChatWorker(threading.Thread):
|
||||||
try:
|
try:
|
||||||
# If the user is not an admin, send him to the user menu
|
# If the user is not an admin, send him to the user menu
|
||||||
if self.admin is None:
|
if self.admin is None:
|
||||||
self.__user_menu()
|
self.__user_menu()
|
||||||
# If the user is an admin, send him to the admin menu
|
# If the user is an admin, send him to the admin menu
|
||||||
else:
|
else:
|
||||||
# Clear the live orders flag
|
# Clear the live orders flag
|
||||||
|
@ -112,7 +113,7 @@ class ChatWorker(threading.Thread):
|
||||||
if self.sentry_client is not None:
|
if self.sentry_client is not None:
|
||||||
self.sentry_client.captureException()
|
self.sentry_client.captureException()
|
||||||
|
|
||||||
def stop(self, reason: str=""):
|
def stop(self, reason: str = ""):
|
||||||
"""Gracefully stop the worker process"""
|
"""Gracefully stop the worker process"""
|
||||||
# Send a stop message to the thread
|
# Send a stop message to the thread
|
||||||
self.queue.put(StopSignal(reason))
|
self.queue.put(StopSignal(reason))
|
||||||
|
@ -144,8 +145,8 @@ class ChatWorker(threading.Thread):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def __wait_for_specific_message(self,
|
def __wait_for_specific_message(self,
|
||||||
items: typing.List[str],
|
items: List[str],
|
||||||
cancellable: bool=False) -> typing.Union[str, CancelSignal]:
|
cancellable: bool = False) -> Union[str, CancelSignal]:
|
||||||
"""Continue getting updates until until one of the strings contained in the list is received as a message."""
|
"""Continue getting updates until until one of the strings contained in the list is received as a message."""
|
||||||
while True:
|
while True:
|
||||||
# Get the next update
|
# Get the next update
|
||||||
|
@ -170,7 +171,7 @@ class ChatWorker(threading.Thread):
|
||||||
# Return the message text
|
# Return the message text
|
||||||
return update.message.text
|
return update.message.text
|
||||||
|
|
||||||
def __wait_for_regex(self, regex: str, cancellable: bool=False) -> typing.Union[str, CancelSignal]:
|
def __wait_for_regex(self, regex: str, cancellable: bool = False) -> Union[str, CancelSignal]:
|
||||||
"""Continue getting updates until the regex finds a match in a message, then return the first capture group."""
|
"""Continue getting updates until the regex finds a match in a message, then return the first capture group."""
|
||||||
while True:
|
while True:
|
||||||
# Get the next update
|
# Get the next update
|
||||||
|
@ -194,7 +195,7 @@ class ChatWorker(threading.Thread):
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
|
|
||||||
def __wait_for_precheckoutquery(self,
|
def __wait_for_precheckoutquery(self,
|
||||||
cancellable: bool=False) -> typing.Union[telegram.PreCheckoutQuery, CancelSignal]:
|
cancellable: bool = False) -> Union[telegram.PreCheckoutQuery, CancelSignal]:
|
||||||
"""Continue getting updates until a precheckoutquery is received.
|
"""Continue getting updates until a precheckoutquery is received.
|
||||||
The payload is checked by the core before forwarding the message."""
|
The payload is checked by the core before forwarding the message."""
|
||||||
while True:
|
while True:
|
||||||
|
@ -224,7 +225,7 @@ class ChatWorker(threading.Thread):
|
||||||
# Return the successfulpayment
|
# Return the successfulpayment
|
||||||
return update.message.successful_payment
|
return update.message.successful_payment
|
||||||
|
|
||||||
def __wait_for_photo(self, cancellable: bool=False) -> typing.Union[typing.List[telegram.PhotoSize], CancelSignal]:
|
def __wait_for_photo(self, cancellable: bool = False) -> Union[List[telegram.PhotoSize], CancelSignal]:
|
||||||
"""Continue getting updates until a photo is received, then return it."""
|
"""Continue getting updates until a photo is received, then return it."""
|
||||||
while True:
|
while True:
|
||||||
# Get the next update
|
# Get the next update
|
||||||
|
@ -242,8 +243,8 @@ class ChatWorker(threading.Thread):
|
||||||
# Return the photo array
|
# Return the photo array
|
||||||
return update.message.photo
|
return update.message.photo
|
||||||
|
|
||||||
def __wait_for_inlinekeyboard_callback(self, cancellable: bool=True) \
|
def __wait_for_inlinekeyboard_callback(self, cancellable: bool = True) \
|
||||||
-> typing.Union[telegram.CallbackQuery, CancelSignal]:
|
-> Union[telegram.CallbackQuery, CancelSignal]:
|
||||||
"""Continue getting updates until an inline keyboard callback is received, then return it."""
|
"""Continue getting updates until an inline keyboard callback is received, then return it."""
|
||||||
while True:
|
while True:
|
||||||
# Get the next update
|
# Get the next update
|
||||||
|
@ -260,7 +261,7 @@ class ChatWorker(threading.Thread):
|
||||||
# Return the callbackquery
|
# Return the callbackquery
|
||||||
return update.callback_query
|
return update.callback_query
|
||||||
|
|
||||||
def __user_select(self) -> typing.Union[db.User, CancelSignal]:
|
def __user_select(self) -> Union[db.User, CancelSignal]:
|
||||||
"""Select an user from the ones in the database."""
|
"""Select an user from the ones in the database."""
|
||||||
# Find all the users in the database
|
# Find all the users in the database
|
||||||
users = self.session.query(db.User).order_by(db.User.user_id).all()
|
users = self.session.query(db.User).order_by(db.User.user_id).all()
|
||||||
|
@ -335,7 +336,7 @@ class ChatWorker(threading.Thread):
|
||||||
products = self.session.query(db.Product).filter_by(deleted=False).all()
|
products = self.session.query(db.Product).filter_by(deleted=False).all()
|
||||||
# Create a dict to be used as 'cart'
|
# Create a dict to be used as 'cart'
|
||||||
# The key is the message id of the product list
|
# The key is the message id of the product list
|
||||||
cart: typing.Dict[typing.List[db.Product, int]] = {}
|
cart: Dict[List[db.Product, int]] = {}
|
||||||
# Initialize the products list
|
# Initialize the products list
|
||||||
for product in products:
|
for product in products:
|
||||||
# If the product is not for sale, don't display it
|
# If the product is not for sale, don't display it
|
||||||
|
@ -363,7 +364,7 @@ class ChatWorker(threading.Thread):
|
||||||
inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel,
|
inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel,
|
||||||
callback_data="cart_cancel")]])
|
callback_data="cart_cancel")]])
|
||||||
# Send a message containing the button to cancel or pay
|
# Send a message containing the button to cancel or pay
|
||||||
final = self.bot.send_message(self.chat.id, strings.conversation_cart_actions, reply_markup=inline_keyboard)
|
final_msg = self.bot.send_message(self.chat.id, strings.conversation_cart_actions, reply_markup=inline_keyboard)
|
||||||
# Wait for user input
|
# Wait for user input
|
||||||
while True:
|
while True:
|
||||||
callback = self.__wait_for_inlinekeyboard_callback()
|
callback = self.__wait_for_inlinekeyboard_callback()
|
||||||
|
@ -411,7 +412,7 @@ class ChatWorker(threading.Thread):
|
||||||
if cart[product_id][1] > 0:
|
if cart[product_id][1] > 0:
|
||||||
product_list += cart[product_id][0].text(style="short", cart_qty=cart[product_id][1]) + "\n"
|
product_list += cart[product_id][0].text(style="short", cart_qty=cart[product_id][1]) + "\n"
|
||||||
total_cost += cart[product_id][0].price * cart[product_id][1]
|
total_cost += cart[product_id][0].price * cart[product_id][1]
|
||||||
self.bot.edit_message_text(chat_id=self.chat.id, message_id=final.message_id,
|
self.bot.edit_message_text(chat_id=self.chat.id, message_id=final_msg.message_id,
|
||||||
text=strings.conversation_confirm_cart.format(product_list=product_list,
|
text=strings.conversation_confirm_cart.format(product_list=product_list,
|
||||||
total_cost=str(total_cost)),
|
total_cost=str(total_cost)),
|
||||||
reply_markup=final_inline_keyboard)
|
reply_markup=final_inline_keyboard)
|
||||||
|
@ -459,7 +460,7 @@ class ChatWorker(threading.Thread):
|
||||||
if cart[product_id][1] > 0:
|
if cart[product_id][1] > 0:
|
||||||
product_list += cart[product_id][0].text(style="short", cart_qty=cart[product_id][1]) + "\n"
|
product_list += cart[product_id][0].text(style="short", cart_qty=cart[product_id][1]) + "\n"
|
||||||
total_cost += cart[product_id][0].price * cart[product_id][1]
|
total_cost += cart[product_id][0].price * cart[product_id][1]
|
||||||
self.bot.edit_message_text(chat_id=self.chat.id, message_id=final.message_id,
|
self.bot.edit_message_text(chat_id=self.chat.id, message_id=final_msg.message_id,
|
||||||
text=strings.conversation_confirm_cart.format(product_list=product_list,
|
text=strings.conversation_confirm_cart.format(product_list=product_list,
|
||||||
total_cost=str(total_cost)),
|
total_cost=str(total_cost)),
|
||||||
reply_markup=final_inline_keyboard)
|
reply_markup=final_inline_keyboard)
|
||||||
|
@ -526,10 +527,10 @@ class ChatWorker(threading.Thread):
|
||||||
def __order_status(self):
|
def __order_status(self):
|
||||||
"""Display the status of the sent orders."""
|
"""Display the status of the sent orders."""
|
||||||
# Find the latest orders
|
# Find the latest orders
|
||||||
orders = self.session.query(db.Order)\
|
orders = self.session.query(db.Order) \
|
||||||
.filter(db.Order.user == self.user)\
|
.filter(db.Order.user == self.user) \
|
||||||
.order_by(db.Order.creation_date.desc())\
|
.order_by(db.Order.creation_date.desc()) \
|
||||||
.limit(20)\
|
.limit(20) \
|
||||||
.all()
|
.all()
|
||||||
# Ensure there is at least one order to display
|
# Ensure there is at least one order to display
|
||||||
if len(orders) == 0:
|
if len(orders) == 0:
|
||||||
|
@ -586,8 +587,7 @@ class ChatWorker(threading.Thread):
|
||||||
self.bot.send_message(self.chat.id, strings.payment_cc_amount,
|
self.bot.send_message(self.chat.id, strings.payment_cc_amount,
|
||||||
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
|
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
|
||||||
# Wait until a valid amount is sent
|
# Wait until a valid amount is sent
|
||||||
# TODO: check and debug the regex
|
selection = self.__wait_for_regex(r"([0-9]+(?:[.,][0-9]+)?|" + strings.menu_cancel + r")")
|
||||||
selection = self.__wait_for_regex(r"([0-9]{1,3}(?:[.,][0-9]+)?|" + strings.menu_cancel + r")")
|
|
||||||
# If the user cancelled the action
|
# If the user cancelled the action
|
||||||
if selection == strings.menu_cancel:
|
if selection == strings.menu_cancel:
|
||||||
# Exit the loop
|
# Exit the loop
|
||||||
|
@ -764,7 +764,7 @@ class ChatWorker(threading.Thread):
|
||||||
# Open the edit menu for that specific product
|
# Open the edit menu for that specific product
|
||||||
self.__edit_product_menu(product=product)
|
self.__edit_product_menu(product=product)
|
||||||
|
|
||||||
def __edit_product_menu(self, product: typing.Optional[db.Product]=None):
|
def __edit_product_menu(self, product: Optional[db.Product] = None):
|
||||||
"""Add a product to the database or edit an existing one."""
|
"""Add a product to the database or edit an existing one."""
|
||||||
# Create an inline keyboard with a single skip button
|
# Create an inline keyboard with a single skip button
|
||||||
cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_skip,
|
cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_skip,
|
||||||
|
@ -780,7 +780,7 @@ class ChatWorker(threading.Thread):
|
||||||
# Wait for an answer
|
# Wait for an answer
|
||||||
name = self.__wait_for_regex(r"(.*)", cancellable=bool(product))
|
name = self.__wait_for_regex(r"(.*)", cancellable=bool(product))
|
||||||
# Ensure a product with that name doesn't already exist
|
# Ensure a product with that name doesn't already exist
|
||||||
if (product and isinstance(name, CancelSignal)) or\
|
if (product and isinstance(name, CancelSignal)) or \
|
||||||
self.session.query(db.Product).filter_by(name=name, deleted=False).one_or_none() in [None, product]:
|
self.session.query(db.Product).filter_by(name=name, deleted=False).one_or_none() in [None, product]:
|
||||||
# Exit the loop
|
# Exit the loop
|
||||||
break
|
break
|
||||||
|
@ -805,7 +805,7 @@ class ChatWorker(threading.Thread):
|
||||||
if product.price is not None else 'Non in vendita')),
|
if product.price is not None else 'Non in vendita')),
|
||||||
reply_markup=cancel)
|
reply_markup=cancel)
|
||||||
# Wait for an answer
|
# Wait for an answer
|
||||||
price = self.__wait_for_regex(r"([0-9]{1,3}(?:[.,][0-9]{1,2})?|[Xx])",
|
price = self.__wait_for_regex(r"([0-9]+(?:[.,][0-9]{1,2})?|[Xx])",
|
||||||
cancellable=True)
|
cancellable=True)
|
||||||
# If the price is skipped
|
# If the price is skipped
|
||||||
if isinstance(price, CancelSignal):
|
if isinstance(price, CancelSignal):
|
||||||
|
@ -894,10 +894,10 @@ class ChatWorker(threading.Thread):
|
||||||
[telegram.InlineKeyboardButton(strings.menu_refund,
|
[telegram.InlineKeyboardButton(strings.menu_refund,
|
||||||
callback_data="order_refund")]])
|
callback_data="order_refund")]])
|
||||||
# Display the past pending orders
|
# Display the past pending orders
|
||||||
orders = self.session.query(db.Order)\
|
orders = self.session.query(db.Order) \
|
||||||
.filter_by(delivery_date=None, refund_date=None)\
|
.filter_by(delivery_date=None, refund_date=None) \
|
||||||
.join(db.Transaction)\
|
.join(db.Transaction) \
|
||||||
.join(db.User)\
|
.join(db.User) \
|
||||||
.all()
|
.all()
|
||||||
# Create a message for every one of them
|
# Create a message for every one of them
|
||||||
for order in orders:
|
for order in orders:
|
||||||
|
@ -1045,10 +1045,10 @@ class ChatWorker(threading.Thread):
|
||||||
# Loop used to move between pages
|
# Loop used to move between pages
|
||||||
while True:
|
while True:
|
||||||
# Retrieve the 10 transactions in that page
|
# Retrieve the 10 transactions in that page
|
||||||
transactions = self.session.query(db.Transaction)\
|
transactions = self.session.query(db.Transaction) \
|
||||||
.order_by(db.Transaction.transaction_id.desc())\
|
.order_by(db.Transaction.transaction_id.desc()) \
|
||||||
.limit(10)\
|
.limit(10) \
|
||||||
.offset(10 * page)\
|
.offset(10 * page) \
|
||||||
.all()
|
.all()
|
||||||
# Create a list to be converted in inline keyboard markup
|
# Create a list to be converted in inline keyboard markup
|
||||||
inline_keyboard_list = [[]]
|
inline_keyboard_list = [[]]
|
||||||
|
@ -1070,7 +1070,7 @@ class ChatWorker(threading.Thread):
|
||||||
inline_keyboard = telegram.InlineKeyboardMarkup(inline_keyboard_list)
|
inline_keyboard = telegram.InlineKeyboardMarkup(inline_keyboard_list)
|
||||||
# Create the message text
|
# Create the message text
|
||||||
transactions_string = "\n".join([str(transaction) for transaction in transactions])
|
transactions_string = "\n".join([str(transaction) for transaction in transactions])
|
||||||
text = strings.transactions_page.format(page=page+1,
|
text = strings.transactions_page.format(page=page + 1,
|
||||||
transactions=transactions_string)
|
transactions=transactions_string)
|
||||||
# Update the previously sent message
|
# Update the previously sent message
|
||||||
self.bot.edit_message_text(chat_id=self.chat.id, message_id=message.message_id, text=text,
|
self.bot.edit_message_text(chat_id=self.chat.id, message_id=message.message_id, text=text,
|
||||||
|
|
Loading…
Reference in a new issue