-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathTelegramBot.py
More file actions
332 lines (251 loc) · 13.2 KB
/
TelegramBot.py
File metadata and controls
332 lines (251 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
from __future__ import print_function
from utils.telegram_utils import UtilityMethods, ScheduleThread
from telegram.ext import Updater, CommandHandler, CallbackContext
from utils.coinbase_utils.PriceGraph import PriceGraph
import utils.coinbase_utils.CoinbaseAPI as cbapi
import utils.coinbase_utils.GlobalStatics as statics
import operator
import warnings
import spike
import schedule
import io
import logging
import json
import os
# Enable logging for Telegram bot
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)
# Silence annoying matplot lib warnings
warnings.filterwarnings("ignore", module="matplotlib")
warnings.filterwarnings("ignore", category=UserWarning, module="matplotlib")
# noinspection PyTypeChecker
class TelegramBot:
"""
This class is designed to launch a telegram bot and expose all functionality to the user
"""
def __init__(self):
"""
Initializes a CoinbaseAPI object and a Spike object using credentials stored on file, and authorizes users from
whitelist.
"""
# Create instance of CoinbaseAPI to facilitate communication between bot & coinbase
current_path = os.path.abspath(os.path.dirname(__file__))
api_file = str(current_path + "/credentials/API_key.json")
self.coinbase_api = cbapi.CoinbaseAPI(api_file)
self.spike = spike.Spike(currencies=statics.CURRENCIES, coinbase_api=self.coinbase_api,
notification_threshold=5, day_threshold=10, week_threshold=10)
self.price_graph = PriceGraph(self.coinbase_api)
self.notification_periodicity = 5 # in minutes
# Get whitelist
whitelist_file = str(current_path + "/credentials/whitelist.json")
file = open(whitelist_file)
whitelist_dict = json.load(file)
self.whitelist_users = whitelist_dict["whitelisted"]
# TODO: import password
# Get the Telegram API Token
dir_path = os.path.abspath(os.path.dirname(__file__))
file_path = os.path.join(dir_path, 'credentials/telegram-bot-api-key.txt')
telegram_api_token = UtilityMethods.get_telegram_api_token(file_path)
# Create updater (front end for Bot) to receive updates from telegram (messages, etc) & send them to dispatcher
self.updater = Updater(token=telegram_api_token, use_context=True)
# Create a dispatcher where we can register our handlers for commands & other behaviours
self.dispatcher = self.updater.dispatcher
self.context = None
self.id = None
# Add handlers which dictate how to respond to different commands
self.dispatcher.add_handler(CommandHandler("start", self.bot_command_start))
self.dispatcher.add_handler(CommandHandler("latest", self.bot_command_latest))
self.dispatcher.add_handler(CommandHandler("graph", self.bot_command_send_graph))
self.dispatcher.add_handler(CommandHandler("gimmemoney", self.bot_command_gimme_money))
self.dispatcher.add_handler(CommandHandler("portfolio", self.bot_command_portfolio))
self.dispatcher.add_handler(CommandHandler("current", self.bot_command_exchange_current))
self.dispatcher.add_handler(CommandHandler("profits", self.bot_command_profits))
self.dispatcher.add_handler(CommandHandler("balance", self.bot_command_balance))
# Register callback behaviour with dispatcher
# self.dispatcher.add_handler(CallbackQueryHandler(self.bot_helper_button_select_callback, pass_update_queue=True,
# pass_user_data=True))
def authenticate(self, update: Updater) -> bool:
"""
Checks if a user is on the whitelist.
:param update: Used to get the username of the user requesting to execute a command
:return: True if person is authorised, otherwise False
"""
username = update.message.from_user["username"]
if username not in self.whitelist_users:
print("Unauthorized user ", username)
return False
return True
def start_telegram_bot(self) -> None:
"""
Starts the bot by putting the updater into a polling mode, and making the bot wait for commands
"""
self.updater.start_polling()
self.updater.idle()
def bot_command_start(self, update: Updater, context: CallbackContext) -> None:
"""
Mandatory instance method because the bot needs the context initialized in order to send messages
:param update: An updater object used to receive data from the telegram chat
:param context: A context object which allows us to send data to the chat
"""
if not self.authenticate(update): # Verify that the user is allowed to access the bot
return
update.message.reply_text("Bot started. Let's make some money!")
self.context = context
self.id = update.effective_chat.id
self.bot_send_spike_alerts()
# Schedule a function to be executed at a given time interval until the bot is killed
schedule.every((self.notification_periodicity*60)).seconds.do(self.bot_send_spike_alerts)
ScheduleThread.ScheduleThread().start()
def bot_command_latest(self, update: Updater, context: CallbackContext) -> None:
"""
Retrieves latest spike notifications, independent of previous notifications via command "/latest".
:param update: The update context required to reply to the command.
:param context: Default CallbackContext.
"""
if not self.authenticate(update): # Verify that the user is allowed to access the bot
return
username = update.message.from_user["username"]
print(username, " requested the latest changes.")
messages = self.spike.get_spike_alerts(ignore_previous=True)
if len(messages) == 0:
update.message.reply_text("No updates to show.")
return
formatted_list = [str(message) + "\n" for message in messages]
formatted_message = "".join(formatted_list)
print(formatted_message)
update.message.reply_text(formatted_message)
def bot_command_send_graph(self, update: Updater, context: CallbackContext) -> None:
"""
Sends an image generated by PriceGraph object to user requesting the graph
:param update: Updater object
:param context: Context object used to send photo to user requesting graph
"""
if not self.authenticate(update): # Verify that the user is allowed to access the bot
return
username = update.message.from_user["username"]
print(username, " requested a graph.")
# Get PIL image from PriceGraph
pil_image = self.price_graph.normalised_price_graph(period="week", is_interactive=False, get_pil_image=True)
# pil_image.show()
# Convert PIL Image into raw bytes
buffer = io.BytesIO()
buffer.name = 'image.jpeg'
pil_image.save(buffer, 'JPEG')
buffer.seek(0)
context.bot.send_photo(update.effective_chat.id, photo=buffer)
def bot_command_gimme_money(self, update: Updater, context: CallbackContext) -> None:
if not self.authenticate(update): # Verify that the user is allowed to access the bot
return
username = update.message.from_user["username"]
print("A large sum of money was given to ", username, ".")
update.message.reply_text("💸💸💸💸💸💸💸💸💸💸\n")
def bot_command_portfolio(self, update: Updater, context: CallbackContext) -> None:
"""
Shows the user their portfolio balance in the form of a graph
:param update: Updater used to retrieve the username
:param context: Context used to send message if syntax is incorrect
"""
if not self.authenticate(update):
return
username = update.message.from_user["username"]
print(username, " requested portfolio")
if len(context.args) < 1:
update.message.reply_text(text="Use Syntax: \n`/portfolio coin (optional: period)`",
parse_mode="Markdownv2")
return
# Get period and coin from user
coin = context.args[0]
period = "month"
try:
period = context.args[1]
except IndexError:
pass
# Get PIL image
pil_image = self.price_graph.portfolio_price_graph(coin=coin, period=period)
buffer = io.BytesIO()
buffer.name = 'image.jpeg'
pil_image.save(buffer, 'JPEG')
buffer.seek(0)
context.bot.send_photo(update.effective_chat.id, photo=buffer)
def bot_command_exchange_current(self, update: Updater, context: CallbackContext) -> None:
"""
Sends the current exchange rate in CHF of a coin passed as an argument.
NOTE: The second argument must be a FIAT currency (CHF, USD, EUR, etc.)
:param update: Updater used to reply to a message
:param context: Context needed to fetch argument from user
"""
if not self.authenticate(update): # Verify that the user is allowed to access the bot
return
if len(context.args) != 1:
update.message.reply_text(text="Please enter a currency code: \n`/current coin`", parse_mode="Markdownv2")
return
coin = context.args[0].upper()
to_currency = "CHF"
if len(context.args) > 1:
to_currency = context.args[1].upper()
coin_currency_pair = coin + "-" + to_currency
current_exchange = float(self.coinbase_api.client.get_spot_price(currency_pair=coin_currency_pair).amount)
output = "1 {} is {:.2f} {}".format(coin.upper(), current_exchange, to_currency.upper())
update.message.reply_text(str(output))
def bot_command_profits(self, update: Updater, context: CallbackContext) -> None:
"""
Used to see how much profit could be gain from selling a certain currency
args[0] Currency which user wants to sell
args[1] Amount of currency user wants to sell
args[2] Currency in which profits are displayed (CHF, USD, etc.)
:param update: Updater used to respond to message
:param context: Context used to extract input arguments
"""
if not self.authenticate(update): # Verify that the user is allowed to access the bot
return
if len(context.args) < 3:
update.message.reply_text(text="Use Syntax: \n`/profits coin_to_sell num_coins profit_currency`",
parse_mode="Markdownv2")
return
sell_coin = context.args[0]
sell_amount = float(context.args[1])
profit_currency = context.args[2]
message = self.spike.get_sell_profitability(coin=sell_coin, amount=sell_amount, profit_currency=profit_currency)
update.message.reply_text(message)
def bot_command_balance(self, update: Updater, context: CallbackContext) -> None:
"""
Used to check the balance of a cryptocurrency account
:param update: Updater used to respond to message
:param context: Context used to extract input arguments
"""
if not self.authenticate(update): # Verify that the user is allowed to access the bot
return
messages = [] # list of messages to be displayed to the user
if len(context.args) <= 0: # No arguments implies that all currencies are fetched
currencies = statics.CURRENCIES
else: # otherwise, the currencies listed by the user are used
currencies = context.args
# get balances for requested coins from Coinbase API
balances = self.coinbase_api.get_account_balance(currencies)
# Extract items and sort by how many coins are held
sorted_balance = sorted(balances.items(), key=operator.itemgetter(1), reverse=True)
# generate balance message for each coin
for coin, coin_balance in sorted_balance:
# Need to escape . character for markdown to work
messages.append("Your have `" + str(coin_balance).replace(".", "\.") + "` *" + str(coin) + "*")
if len(messages) == 0: # explain syntax if no arguments are given
update.message.reply_text(text="Please enter one or more currency.\nSyntax: `/balance coin1 coin2 ...`",
parse_mode="Markdownv2")
return
update.message.reply_text(text="\n".join(messages), parse_mode="Markdownv2") # send message
def bot_send_spike_alerts(self) -> None:
"""
Sends a spike alert to the user in chat. This is not a callback hence why it doesn't take in a context
or updater as arguments.
"""
print("Checking for new alerts.")
messages = self.spike.get_spike_alerts()
if len(messages) == 0:
return
# formatted_list = [str(message) + "\n" for message in messages]
formatted_message = "\n".join(messages)
print(formatted_message)
self.context.bot.send_message(self.id, formatted_message)
if __name__ == '__main__':
bot = TelegramBot()
bot.start_telegram_bot()