Welcome to lark3ri.com

Discord slot machine bot (using Twitch points as coins)

07.06.2020

I had an idea to test how to make a Discord bot and here is what it became. Still i was out of ideas that what kind of Discord bot i could create. Thanks for Janne(Jenkkemi) for the idea to make this kind of bot. It was very fun to create and program this.

It uses the Discord API to handle all the Discord side stuff, and the WebSockets to handle all the Twitch side stuff. It uses the Asyncio to processing all that together. You can download the full project from this link. It is licensed with GPL v2 license only. It is free to use under the license agreements. Here are all the codes that it includes.



Main file that runs the whole program.

bot.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa

	Version: 1.3
"""

from overload_builtins import print
import discord
import asyncio
import secret
import link_accounts
import get_channel_redemption
import commands.casino

link_accounts = link_accounts.LinkAccounts()
casino = commands.casino.Casino()


admins = {} # Admin Discord user IDs


class Client(discord.Client):
	async def on_ready(self):
		print(f'Logged on as {self.user}')
		asyncio.create_task(link_accounts.init(client))

	async def on_message(self, message):
		if message.author == self.user:
			return

		# Private messages
		if isinstance(message.channel, discord.channel.DMChannel):
			# !link
			if message.content == link_accounts.command[0]:
				if await link_accounts.link(message):
					await link_accounts.wait_user_response(message)

			# !unlink
			elif message.content == link_accounts.command[1]:
				await link_accounts.unlink(message)

			if int(message.author.id) not in admins:
				return

		# !casino
		if message.content.startswith(casino.command[0]):
			cut_casino = len(casino.command[0]) + 1
			# !casino roll
			if message.content[cut_casino:].startswith(casino.command[1]):
				await casino.identify_author_id(message)
				money_added = await link_accounts.check_cash_waiting(message, casino)
				await casino.roll(message.content[cut_casino:].split(), message, money_added)

			# !casino
			elif message.content == casino.command[0]:
				s = casino.print_rules(1)
				await message.channel.send(s)
				print(f'{message.author.id} {message.author.name}: {message.content}: print rules')

			# !casino rules
			elif message.content[cut_casino:] == casino.command[2]:
				await casino.identify_author_id(message)
				money_added = await link_accounts.check_cash_waiting(message, casino)
				await casino.print_own_rules(message, money_added)

			# !casino cash
			elif message.content[cut_casino:] == casino.command[3]:
				await casino.identify_author_id(message)
				money_added = await link_accounts.check_cash_waiting(message, casino)
				await casino.print_nice_balance(message, money_added)

			# !casino set
			elif message.content[cut_casino:].startswith(casino.command[4]):
				await casino.identify_author_id(message)
				await casino.set(message.content[cut_casino:].split(), message)

			# !casino add
			"""
			elif message.content[cut_casino:].startswith(casino.command[5]):
				await casino.identify_author_id(message)
				await casino.add(message.content[cut_casino:].split(), message)
			"""


if __name__ == '__main__':
	casino.init()
	loop = asyncio.get_event_loop()
	loop.create_task(casino.save_database_interval())
	loop.create_task(get_channel_redemption.connect(link_accounts.gcr_callback))

	client = Client()
	client.run(secret.discord_token)
	casino.save_database()

The actual game logic that makes all the game actions, like rolling the reels and get the next reel positions and so on.

casino.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa
"""

from overload_builtins import print
from random import randint
import asyncio
import time
import math
import json
import copy

bell = ':bell:'
heart = ':crown:'
diamond = ':diamond_shape_with_a_dot_inside:'
spade = ':hearts:'
horseshoe = ':dragon:'
star = ':star:'
wild = ':moneybag:'


class Casino:
	command = ['!casino', 'roll', 'rules', 'cash', 'set', 'add']
	data_file_name = 'casino-data.json'
	roll_cost = 1
	registered_author_ids = {}
	reel = [0, 0, 0]
	interval_time_to_save = 60 * 10
	reel_ = [
		[
			bell,
			horseshoe,
			spade,
			horseshoe,
			diamond,
			horseshoe,
			spade,
			horseshoe,
			heart,
			horseshoe
		],
		[
			bell,
			horseshoe,
			spade,
			horseshoe,
			diamond,
			horseshoe,
			spade,
			horseshoe,
			heart,
			wild
		],
		[
			bell,
			diamond,
			star,
			spade,
			bell,
			diamond,
			heart,
			star,
			spade,
			diamond
		]
	]

	# Bell Heart Diamond Spade Horseshoe Star Wild
	table_to_win = [
		[[0], [8], [4], [2, 6], [1, 3, 5, 7, 9], [], []],  # Reel 1
		[[0], [8], [4], [2, 6], [1, 3, 5, 7], [], [9]],  # Reel 2
		[[0, 4], [6], [1, 5, 9], [3, 8], [], [2, 7], []]  # Reel 3
	]

	pay_table = [
		[20, table_to_win[0][0], table_to_win[1][0], [False, table_to_win[2][0]]],  # $20 = Bell Bell Bell
		[16, table_to_win[0][1], table_to_win[1][1], [False, table_to_win[2][1]]],  # $16 = Heart Heart Heart
		[12, table_to_win[0][2], table_to_win[1][2], [False, table_to_win[2][2]]],  # $12 = Diamond Diamond Diamond
		[8, table_to_win[0][3], table_to_win[1][3], [False, table_to_win[2][3]]],  # $8  = Spade Spade Spade
		[4, table_to_win[0][4], table_to_win[1][4], [False, table_to_win[2][5]]],  # $4  = Horseshoe Horseshoe Star
		[2, table_to_win[0][4], table_to_win[1][4], [True, table_to_win[2][5]]]
		# $2  = Horseshoe Horseshoe Any(not Star)
	]

	def init(self):
		try:
			with open(self.data_file_name, 'r', encoding='utf-8') as f:
				data = f.read()
				self.registered_author_ids = json.loads(data)
		except IOError:
			pass

	def save_database(self):
		with open(self.data_file_name, 'w', encoding='utf-8') as f:
			json.dump(self.registered_author_ids, f, ensure_ascii=False, indent=4)

	async def save_database_interval(self):
		registered_author_ids = copy.deepcopy(self.registered_author_ids)
		while True:
			await asyncio.sleep(self.interval_time_to_save)
			if self.registered_author_ids.items() != registered_author_ids.items():
				print('saving database')
				self.save_database()
				registered_author_ids = copy.deepcopy(self.registered_author_ids)
				print('database saved')

	async def roll(self, cut_msg, message, money_added):
		current_author_id = str(message.author.id)
		if len(cut_msg) > 4:
			return
		if len(cut_msg) > 1 and not str(cut_msg[1]).isdigit():
			return
		if len(cut_msg) > 2 and not str(cut_msg[2]).isdigit():
			return
		if len(cut_msg) > 3 and not str(cut_msg[3]).isdigit():
			return

		count = 1
		# Set auto roll count
		if len(cut_msg) > 1 and str(cut_msg[1]).isdigit() and int(cut_msg[1]) > 0:
			count = int(cut_msg[1])
			if count > 10:
				count = 10
		# Set multiplier: needs to be before set_payout_limit function
		if len(cut_msg) > 2 and str(cut_msg[2]).isdigit():
			self.set_multiplier(current_author_id, int(cut_msg[2]))
		# Set payout limit
		if len(cut_msg) > 3 and str(cut_msg[3]).isdigit():
			self.set_payout_limit(current_author_id, int(cut_msg[3]))

		# Do game
		game_str = ''
		for i in range(count):
			if self.roll_cost * self.get_multiplier(current_author_id) > self.get_balance(current_author_id):
				game_str += '\n :money_with_wings: `You don\'t have enough cash to roll`'
				break
			self.update_balance(current_author_id, -self.roll_cost * self.get_multiplier(current_author_id))
			self.reel[0] = randint(0, 9)
			self.reel[1] = randint(0, 9)
			self.reel[2] = randint(0, 9)
			game_str += f'\n{str(self.get_reels_position())}'
			current_winnings = self.check_reels_update_balance(current_author_id)
			game_str += str(self.get_nice_balance(current_author_id, current_winnings))
			if current_winnings >= int(self.get_payout_limit(current_author_id)):
				break

		str_ = f'<@{current_author_id}> '
		if money_added > 0:
			str_ += f'{self.twitch_money_added_message(message, money_added)}'
		str_ += f'{game_str}'
		await message.channel.send(str_)

	def get_reels_position(self):
		s0 = self.reel_[0][self.reel[0]]
		s1 = self.reel_[1][self.reel[1]]
		s2 = self.reel_[2][self.reel[2]]
		return s0 + s1 + s2

	def check_reels_update_balance(self, current_author_id):
		current_winnings = 0
		for i in range(len(self.pay_table)):
			# Reel 1 check
			if self.reel[0] in self.pay_table[i][1]:
				# Reel 2 check, and if WILD then just pass through
				if self.reel[1] in self.table_to_win[1][6] \
						or self.reel[1] in self.pay_table[i][2]:
					# Reel 3 check and handling Any(not Star) case
					if not self.pay_table[i][3][0]:
						if self.reel[2] in self.pay_table[i][3][1]:
							current_winnings = self.pay_table[i][0] * self.get_multiplier(current_author_id)
							self.update_balance(current_author_id, current_winnings)
					else:
						if self.reel[2] not in self.pay_table[i][3][1]:
							current_winnings = self.pay_table[i][0] * self.get_multiplier(current_author_id)
							self.update_balance(current_author_id, current_winnings)
			# Stop point handling
			if current_winnings >= int(self.get_payout_limit(current_author_id)):
				break

		return current_winnings

	def update_balance(self, current_author_id, i: int):
		self.registered_author_ids[current_author_id][0] += i

	def get_nice_balance(self, current_author_id, current_winnings=0):
		s = f'`${self.registered_author_ids[current_author_id][0]}`'
		s += f' `M:{self.get_multiplier(current_author_id)}`'
		s += f' `L:{self.get_payout_limit(current_author_id)}`'
		if current_winnings > 0:
			s += f' `You won! ${current_winnings}`'
		return s

	async def print_nice_balance(self, message, money_added):
		current_author_id = str(message.author.id)
		str_ = f'{self.get_nice_balance(current_author_id)}'
		if money_added > 0:
			str_ = f'{self.twitch_money_added_message(message, money_added)}\n{str_}'
		await message.channel.send(f'<@{current_author_id}> {str_}')

	def get_balance(self, current_author_id):
		return self.registered_author_ids[current_author_id][0]

	def get_multiplier(self, current_author_id):
		return self.registered_author_ids[current_author_id][1]

	def set_multiplier(self, current_author_id, i):
		if i < 1:
			i = 1
		elif i > 4:
			i = 4
		self.registered_author_ids[current_author_id][1] = i

	def get_payout_limit(self, current_author_id):
		return self.registered_author_ids[current_author_id][2]

	def set_payout_limit(self, current_author_id, i):
		maximum_payout = self.pay_table[0][0] * self.get_multiplier(current_author_id)
		smallest_payout = self.pay_table[len(self.pay_table) - 1][0] * self.get_multiplier(current_author_id)
		if i < smallest_payout:
			i = smallest_payout  # Smallest possible payout
		elif i > maximum_payout:
			i = maximum_payout  # Maximum possible payout
		self.registered_author_ids[current_author_id][2] = i

	async def identify_author_id(self, message):
		current_author_id = str(message.author.id)
		# # Add new user
		if current_author_id not in self.registered_author_ids:
			# [balance, multiplier, stop if this or greater payout reached, current time]
			self.registered_author_ids[current_author_id] = [20, 1, 20, time.time()]
			self.save_database()

		str_ = f'{message.author.id} {message.author.name}: {message.content}: '
		str_ += self.get_nice_balance(current_author_id).replace('`', '')
		print(str_)

	async def print_own_rules(self, message, money_added):
		print(f'{message.author.id} {message.author.name}: {message.content}: print own rules')
		current_author_id = str(message.author.id)
		str_ = f'{self.get_nice_balance(current_author_id)}\n'
		str_ += f'{self.print_rules(self.get_multiplier(current_author_id))}'
		if money_added > 0:
			str_ = f'{self.twitch_money_added_message(message, money_added)}\n{str_}'
		await message.channel.send(f'<@{current_author_id}> {str_}')

	def print_rules(self, multiplier=1):
		s = ''
		for i in range(len(self.pay_table) - 1):
			s += self.reel_[0][self.pay_table[i][1][0]]
			s += self.reel_[1][self.pay_table[i][2][0]]
			s += self.reel_[2][self.pay_table[i][3][1][0]]
			s += f' `${self.pay_table[i][0] * multiplier}`\n'
		s += f'{horseshoe}{horseshoe} Any(Not {star}) `${2 * multiplier}`\n\n'

		s += f'{wild} **WILD** symbol on the center reel.'
		s += ' Wild can substitute for any other symbol, with no exceptions.\n\n'

		s += '**Get more money!** Redeem specific Twitch reward from'
        s += ' [twitch channel name here] Twitch channel using your channel points'
		s += ' to get more money. Link your Discord and Twitch account first by sending `!link`'
		s += ' private message for this bot. You can also unlink your Discord and Twitch connection by'
		s += ' sending `!unlink` private message for this bot.\n\n'

		s += '`!casino rules` or `!casino` Shows you the casino rules.\n'
		s += '`!casino roll` Roll the reels once.\n'
		s += '`!casino roll 10` Roll the reels multiple times at once. Number can be between 1 - 10.\n'
		s += '`!casino roll 10 4` Set the multiplier. Number can be between 1 - 4.\n'
		s += '`!casino roll 10 4 80` Set the payout limit. Stop if this or greater payout.\n'
		s += '`!casino cash` Show how much money you have at the moment.\n'
		s += '`!casino set M` Set the multiplier. M can also be m, mult, multp or multiplier.\n'
		s += '`!casino set L` Set the payout limit. L can also be l, limit or payoutlimit.\n'
		# s += '`!casino add M` Add $100 money. Every 10 minutes you can add money. M can also be m or money.'
		return s

	async def set(self, cut_msg, message):
		current_author_id = str(message.author.id)

		print(f'{message.author.id} {message.author.name}: {message.content}')

		if len(cut_msg) < 3 or len(cut_msg) > 3:
			return

		str_ = ''
		# Set Multiplier
		if cut_msg[1] in ['m', 'M', 'mult', 'multp', 'multiplier'] and str(cut_msg[2]).isdigit():
			self.set_multiplier(current_author_id, int(cut_msg[2]))
			str_ = f' `Multiplier set to {self.get_multiplier(current_author_id)}`'

		# Set stop point
		elif cut_msg[1] in ['l', 'L', 'limit', 'payoutlimit'] and str(cut_msg[2]).isdigit():
			self.set_payout_limit(current_author_id, int(cut_msg[2]))
			str_ = f' `Payout limit set to {self.get_payout_limit(current_author_id)}`'

		if str_ == '':
			return

		await message.channel.send(f'<@{current_author_id}>{str_}')
		print(f'{message.author.id} {message.author.name}: {message.content}:{str_.lower().replace("`", "")}')

	async def add(self, cut_msg, message):
		current_author_id = str(message.author.id)

		print(f'{message.author.id} {message.author.name}: {message.content}')

		if len(cut_msg) < 2:
			return
		if not cut_msg[1] in ['m', 'M', 'money']:
			return

		# Add money
		if self.permission_to_add_100(current_author_id):
			self.update_timer_time(current_author_id)
			await self.send_money_added_message(message)
			self.save_database()
			return

		p = time.time() - self.registered_author_ids[current_author_id][3]
		m = math.floor(((60.0 * 10.0) - p) / 60.0)
		s = math.floor(((60.0 * 10.0) - p) - (m * 60))
		str_ = f' :money_with_wings: `You have to wait {m} minutes and {s} seconds before you can add more money.`'

		await message.channel.send(f'<@{current_author_id}>{str_}')

	async def send_money_added_message(self, message):
		current_author_id = str(message.author.id)
		str_ = f' :money_mouth: `Money added! Your current balance: ${self.get_balance(current_author_id)}`'
		await message.channel.send(f'<@{current_author_id}>{str_}')
		str_ = f'{message.author.id} {message.author.name}: {message.content}: '
		str_ += f'money added, current balance {self.get_balance(current_author_id)}'
		print(str_)

	def twitch_money_added_message(self, message, cost):
		current_author_id = str(message.author.id)

		str_ = f'{message.author.id} {message.author.name}: {message.content}: '
		str_ += f'twitch money added {cost}, current balance {self.get_balance(current_author_id)}'
		print(str_)

		str_ = f':money_mouth: `You redeemed a Twitch reward and gained ${cost} of cash! Your current balance:'
		str_ += f' ${self.get_balance(current_author_id)}`'

		return str_

	def permission_to_add_100(self, current_author_id):
		if time.time() - self.registered_author_ids[current_author_id][3] > 60 * 10:
			self.update_balance(current_author_id, 100)
			return True
		return False

	def update_timer_time(self, current_author_id):
		self.registered_author_ids[current_author_id][3] = time.time()

This is the key file between Discord and Twitch. It handels the Discord and Twitch accounts linking process. If the user asks to link the Discord and Twitch accounts, the bot asks the user to send a generated unique identifier ID through Twitch, back to the bot, that it can know that this user have this Twitch account, and so link those together. Partly it also handles the action of users to redeem a specific reward from Twitch account to add more money to the bot cash for that specific user that asked it.

It also handles the situation if someone redeems the specific reward in Twitch to add more money, but the user doesn't have lineked the Discord and Twitch accounts yet, and the bot does not know yet to from which user this is from. It saves the reward redeem action and before every user gaming action it checks the user from remeeded rewards, if the user had linked the accounts and if it have some rewards waiting. It handles also the timing how users can whisper at the same time to the bot, while keeping their own time limits active.

link_accounts.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa
"""

from overload_builtins import print
import time
import asyncio
import json

import nonce
import get_whispers


class LinkAccounts:
	command = ['!link', '!unlink']
	linked_accounts_fn = 'linked-account.json'
	cash_waiting_fn = 'cash-waiting-requests.json'
	linked_accounts = {}
	expect_response = {}
	cash_waiting = {}
	dc_client = None
	get_whispers = None
	init_done = False

	async def init(self, dc_client):
		if self.init_done:
			print('init already done')
			return
		self.init_done = True
		self.dc_client = dc_client
		await self.load_linked_accounts()
		await self.load_cash_waiting()
		self.get_whispers = get_whispers.WebSocketClient(self.gw_callback)

	async def load_linked_accounts(self):
		try:
			with open(self.linked_accounts_fn, 'r', encoding='utf-8') as f:
				data = f.read()
				self.linked_accounts = json.loads(data)
		except IOError:
			pass

	def save_linked_accounts(self):
		with open(self.linked_accounts_fn, 'w', encoding='utf-8') as f:
			json.dump(self.linked_accounts, f, ensure_ascii=False, indent=4)

	async def load_cash_waiting(self):
		try:
			with open(self.cash_waiting_fn, 'r', encoding='utf-8') as f:
				data = f.read()
				self.cash_waiting = json.loads(data)
		except IOError:
			pass

	def save_cash_waiting(self):
		with open(self.cash_waiting_fn, 'w', encoding='utf-8') as f:
			json.dump(self.cash_waiting, f, ensure_ascii=False, indent=4)

	async def gcr_callback(self, display_name: str, tw_uid: str, cost: int):
		print(f'{display_name} {tw_uid} {cost}')

		if tw_uid in self.cash_waiting:
			self.cash_waiting[tw_uid] += cost
		else:
			self.cash_waiting[tw_uid] = cost
		self.save_cash_waiting()

		for dc_uid, tw_uid_ in self.linked_accounts.items():
			if tw_uid_ == tw_uid:
				dc_user = self.dc_client.get_user(int(dc_uid))
				await dc_user.send(f'Successfully transferred {cost} Twitch channel points to your '
								   f'slot machine cash.')
				return

	async def gw_callback(self, tw_uid: str, received_token: str):
		print(f'{tw_uid} {received_token}')
		if received_token in self.expect_response:
			print('expected token received. user added to the linked accounts')
			self.linked_accounts[self.expect_response[received_token]] = str(tw_uid)
			del self.expect_response[received_token]
			if not self.expect_response and self.get_whispers.connection.open:
				await self.get_whispers.connection.close()
				await self.get_whispers.connection.wait_closed()
			self.save_linked_accounts()
		else:
			print('expected token NOT received')

	async def check_cash_waiting(self, message, casino):
		message_author_id = str(message.author.id)

		if message_author_id not in self.linked_accounts:
			return 0

		for tw_uid, cost in self.cash_waiting.items():
			if tw_uid == self.linked_accounts[message_author_id]:

				print(f'casino add {cost}')

				# Add new twitch reward money to user current balance
				casino.update_balance(message_author_id, int(cost))
				casino.save_database()

				del self.cash_waiting[tw_uid]
				self.save_cash_waiting()

				return int(cost)
		return 0

	async def unlink(self, message):
		str_ = f'{message.author.id} {message.author.name}: {message.content}:'
		current_author_id = str(message.author.id)
		if current_author_id in self.linked_accounts:
			del self.linked_accounts[current_author_id]
			print(f'{str_} account successfully unlinked')
			await message.channel.send('Account successfully unlinked.')
			self.save_linked_accounts()
			return
		print(f'{str_} your account is not linked')
		await message.channel.send('Your account is not linked.')

	async def link(self, message):
		str_ = f'{message.author.id} {message.author.name}: {message.content}:'
		current_author_id = str(message.author.id)
		if current_author_id in self.linked_accounts:
			print(f'{str_} account already linked')
			await message.channel.send('Account already linked.')
			return 0
		if current_author_id in self.expect_response.values():
			print(f'{str_} the link process is already running for this user')
			return 0
		else:
			print(f'{str_} link')

		generated_token = nonce.get(16)
		while generated_token in self.expect_response:
			generated_token = nonce.get(16)

		self.expect_response[generated_token] = current_author_id

		await self.get_whispers.update_time()
		if self.get_whispers.connection is None or not self.get_whispers.connection.open:
			await self.get_whispers.connect()
			asyncio.create_task(self.get_whispers.heartbeat())
			asyncio.create_task(self.get_whispers.on_message())
			asyncio.create_task(self.get_whispers.timer())

		s = 'In order to complete your linking process please whisper '
		s += 'in Twitch within 1 minute. Copy this: '
		s += f'`/w {get_whispers.bot_name} {generated_token}`'
		await message.channel.send(s)
		return 1

	async def wait_user_response(self, message):
		str_ = f'{message.author.id} {message.author.name}: {message.content}:'
		current_author_id = str(message.author.id)
		for key, value in self.expect_response.items():
			if value == current_author_id:
				print(f'{str_} waiting for user response: {key}')
				break
		time_ = self.get_whispers.time
		while True:
			await asyncio.sleep(1)
			# If response is expect response
			if current_author_id in self.linked_accounts:
				print(f'{str_} successfully linked account')
				await message.channel.send('You have successfully linked your Discord and Twitch accounts.')
				return

			# If waiting time ends
			if time.time() > time_:
				print(f'{str_} time ends')
				# Remove expect response from the expect_response dict
				for key, value in self.expect_response.items():
					if value == current_author_id:
						del self.expect_response[key]
						break
				break
		await message.channel.send('Time limit reached. Try again: `!link`')
		print(f'{str_} try again')

While user wants to connect the Discord and Twitch channels, this receives the user messages from Twitch and keeping the connection alive until all time limits are reached from all active users or when also the last active user completes the assignment of sending the identifier ID to the bot through Twitch successfully.

get_whispers.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa
"""

from overload_builtins import print
import asyncio
import websockets
import random
import json
import time

import nonce
import secret

bot_name = 'bot_name'


class WebSocketClient:
	time = 0
	connection = None

	def __init__(self, callback_function):
		self.topics = ['whispers.[CHANNEL ID]']
		self.uri = 'wss://pubsub-edge.twitch.tv'
		self.callback_function = callback_function
		self.connection = None

	async def connect(self):
		print('connect')
		self.connection = await websockets.client.connect(self.uri)
		data = {
			'type': 'LISTEN',
			'nonce': nonce.get(15),
			'data': {
					'topics': self.topics,
				'auth_token': secret.twitch_client_oauth
			}
		}
		await self.connection.send(str(data).replace('\'', '"'))

	async def heartbeat(self):
		print('starts')
		while True:
			try:
				if self.connection.open:
					await self.connection.send('{"type":"PING"}')
				else:
					print(f'self.connection.open: False')
			except Exception as e:
				print(f'Exception: {e}')
				return

			i = 120 + random.randint(0, 180)
			while i > 0:
				if not self.connection.open:
					print(f'ended')
					return
				await asyncio.sleep(1)
				i -= 1

	async def on_message(self):
		print('starts')
		while True:
			try:
				r = await self.connection.recv()
			except Exception:
				print(f'ended')
				return
			data = json.loads(r)

			if 'type' in data and data['type'] == 'PONG':
				continue

			print(r)

			if 'type' in data and data['type'] != 'MESSAGE':
				continue

			data['data']['message'] = json.loads(str(data['data']['message']))

			if data['data']['message']['type'] != 'whisper_received':
				continue

			# Get whisper sender twitch id and message
			from_id = str(data['data']['message']['data_object']['from_id'])
			body = str(data['data']['message']['data_object']['body'])
			await self.callback_function(from_id, body)

	async def get_time(self):
		return self.time

	async def update_time(self):
		print('time update')
		self.time = time.time() + 60.0  # + is how long one session can last

	async def timer(self):
		print('starts')
		while True:
			if time.time() > self.time:
				print('time ended')
				await self.connection.close()
				await self.connection.wait_closed()
				return
			if not self.connection.open:
				print('ended')
				return
			await asyncio.sleep(1)

When a user redeems the specific reward from the Twitch channel, this receive and parses the message from Twitch. This should always be running when the bot is on.

get_channel_redemption.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa
"""

from overload_builtins import print
import asyncio
import websockets
import random
import json
import requests
import time

import nonce
import secret

custom_reward_names = ['custom_reward_names']


class WebSocketClient:
	def __init__(self, callback_function):
		self.callback_function = callback_function
		# self.channel = '[CHANNEL NAME]'
		self.topics = ['channel-points-channel-v1.[CHANNEL ID]']
		self.uri = 'wss://pubsub-edge.twitch.tv'
		self.error = False
		self.data_file_name = 'secret.json'
		self.connection = None

	async def connect(self):
		if self.connection is not None:
			if self.connection.open:
				return
		database_data = await self.update_database()
		"""
		for i in range(len(self.topics)):
			self.topics[i] = f'{self.topics[i]}.{database_data["channel_id"]}'
		"""
		self.connection = await websockets.client.connect(self.uri)
		data = {
			'type': 'LISTEN',
			'nonce': nonce.get(15),
			'data': {
				'topics': self.topics,
				'auth_token': database_data['access_token']
			}
		}
		await self.connection.send(str(data).replace('\'', '"'))

	async def heartbeat(self):
		while True:
			try:
				if self.connection.open:
					await self.connection.send('{"type":"PING"}')
				else:
					print(f'self.connection.open: False')
			except Exception as e:
				print(e)
				pass
			await asyncio.sleep(180 + random.randint(0, 120))

	async def on_message(self):
		while True:
			try:
				r = await self.connection.recv()
			except Exception:
				print('r = await self.connection.recv() FAILED')
				await self.connect()
				continue
			data = json.loads(r)

			if 'type' in data and data['type'] == 'PONG':
				continue

			if 'type' in data and data['type'] == 'RECONNECT':
				print('RECONNECT')
				await self.connection.close()
				await self.connection.wait_closed()
				await asyncio.sleep(5)
				await self.connect()
				continue

			if 'error' in data and data['error'] == 'ERR_BADAUTH':
				print('ERR_BADAUTH')
				await asyncio.sleep(5)
				self.error = True
				await self.connect()
				continue

			if 'type' in data and data['type'] != 'MESSAGE':
				continue

			data['data']['message'] = json.loads(str(data['data']['message']))

			if data['data']['message']['type'] != 'reward-redeemed':
				continue

			if not data['data']['message']['data']['redemption']['reward']['title'] in custom_reward_names:
				continue

			print(r)

			display_name = data['data']['message']['data']['redemption']['user']['display_name']
			user_id = data['data']['message']['data']['redemption']['user']['id']
			cost = data['data']['message']['data']['redemption']['reward']['cost']
			await self.callback_function(str(display_name), str(user_id), int(cost))

	async def update_database(self):
		# Load current database as data
		with open(self.data_file_name, 'r') as file:
			s = file.read()
		data = json.loads(s)

		# If token is not expired
		time_stamp = float(data['time_stamp'])
		expires_in = float(data['expires_in'])
		if self.error is False and time.time() < time_stamp + expires_in:
			return data

		self.error = False

		await asyncio.sleep(1)

		# Get new tokens
		params = {
			'grant_type': 'refresh_token',
			'refresh_token': bytes.fromhex(str(data['refresh_token'])).decode(encoding='utf_8'),
			'client_id': secret.twitch_client_id,
			'client_secret': secret.twitch_client_secret,
			'scope': data['scope'],
			'state': nonce.get(32)
		}

		url = 'https://id.twitch.tv/oauth2/token'
		r = requests.post(url, params=params)
		data = json.loads(r.text)

		# Add other data
		data['refresh_token'] = str(data['refresh_token']).encode(encoding='utf_8').hex()
		data['time_stamp'] = time.time()

		# Get channel id
		"""
		await asyncio.sleep(1)
		params = {
			'login': self.channel
		}
		headers = {
			'Authorization': f'Bearer {secret.twitch_client_oauth}',
			'Client-ID': secret.twitch_client_oauth_id,
			'Accept': 'application/vnd.twitchtv.v5+json'
		}
		url = 'https://api.twitch.tv/helix/users'
		r = requests.get(url, headers=headers, params=params)
		channel_data = json.loads(r.text)
		data['channel_id'] = channel_data['data'][0]['id']
		"""

		# Save new updated database
		with open(self.data_file_name, 'w', encoding='utf-8') as file:
			json.dump(data, file, ensure_ascii=False, indent=4)

		return data


async def connect(callback_function):
	client = WebSocketClient(callback_function)
	await client.connect()
	asyncio.create_task(client.heartbeat())
	asyncio.create_task(client.on_message())

Get nice random string of characters. This generates the identifier IDs.

nonce.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa
"""

import random


def get(length):
	letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
	return ''.join(random.choice(letters) for i in range(length))

Only for getting a nice readable and understandable log files. If some errors happen, it is easy to see where it is coming from when all print functions is going through this.

overload_builtins.py
"""
	Copyright (C) 2020 Lari Varjonen <[email protected]>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	version 2 as published by the Free Software Foundation.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

	Thanks for Janne(Jenkkemi) for the idea to make this kind of bot: https://twitter.com/JanneKuoksa
"""

import builtins as __builtin__
import datetime
import inspect
import os


def print(msg):
	frame = inspect.stack()[1]
	module = inspect.getmodule(frame[0])
	caller_file_name = os.path.basename(module.__file__)
	caller_function_name = inspect.stack()[1][3]
	datetime_now = datetime.datetime.now()
	return __builtin__.print(f'{datetime_now}: {caller_file_name}: {caller_function_name}: {msg}')
« Back | ↑ Top