From fef9acc2e354552f27828646137311e5fe04a655 Mon Sep 17 00:00:00 2001 From: fabioo29 Date: Mon, 23 Aug 2021 02:01:28 +0100 Subject: [PATCH] branch refactor --- .env.example | 19 ++ .github/workflows/keep_alive.yml | 12 + .gitignore | 148 +++++++++++ README.md | 83 ++++++ discord-bot/assist.py | 80 ++++++ discord-bot/constants.py | 146 +++++++++++ discord-bot/keep_alive.py | 15 ++ discord-bot/main.py | 427 +++++++++++++++++++++++++++++++ requirements.txt | 21 ++ 9 files changed, 951 insertions(+) create mode 100644 .env.example create mode 100644 .github/workflows/keep_alive.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 discord-bot/assist.py create mode 100644 discord-bot/constants.py create mode 100644 discord-bot/keep_alive.py create mode 100644 discord-bot/main.py create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c23dd79 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +>> https://console.actions.google.com/u/0/ > select project > 3 vert. dots > project settings > Project ID +DEVICE_ID = "something-something" + +>> https://console.actions.google.com/u/0/project//deviceregistration/ +DEVICE_MODEL_ID = "-something-something" + +>> https://console.cloud.google.com/ >> select project >> search google assistant API >> add +>> google assistant >> Credentials >> OAuth 2.0 >> Create >> Download secrets.json + +>> OAuth permission Screen >> Switch to Test(email setup needed)/Production according to your needs + +>> exec cmd command >> google-oauthlib-tool --client-secrets path/to/secrets.json --scope https://www.googleapis.com/auth/assistant-sdk-prototype +>> paste output below +CREDENTIALS = '{'key1': 'value1', 'key2': 'value2'}' + +ASSISTANT_TOKEN = "Token from CREDENTIALS" + +>> Get a discord bot >> https://www.writebots.com/discord-bot-token/ +DISCORD_TOKEN = "discord Token" diff --git a/.github/workflows/keep_alive.yml b/.github/workflows/keep_alive.yml new file mode 100644 index 0000000..0eae8eb --- /dev/null +++ b/.github/workflows/keep_alive.yml @@ -0,0 +1,12 @@ +on: + schedule: + - cron: "*/30 * * * *" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: mikeesto/heroku-awake@1.0.0 + with: + URL: "https://LikelyCandidAlgorithms.fabioo29.repl.co" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..588152f --- /dev/null +++ b/.gitignore @@ -0,0 +1,148 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so +./ven/ +credentials.json +client_secret_*.json +*.mp3 +*.wav +test.py +.vscode/ +audio-chunks/ +ven/ + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +*.pcm + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..fda51d2 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +

Voice assistant discord bot

+

Music discord bot with built-in google assistant

+ +
+

Bot default prefixes: ['-', '!', 'google '] - The bot will recognize your voice if it starts with those prefixes or any chat command in the same way. You can see what the bot is catching in your console with the '-sp' argument. Check Input arguments below for more info.

+ +
+ +

-help command (known existing google commands + some usefull ones)

+ +``` +Voice commands + tell me a joke + temperature braga {in centigrades} + rain today lisbon + what's python language? + time in japan + day in 29/04/1998 + 423*12+23-1 + spin the wheel + cristiano ronaldo {born,married, age, play for} + who wrote rich dad poor dad + 10cm to inch + real madrid win + surprise me + +Chat commands (+ voice commands)\n + -translate hello {en, english} {pt, portuguese} + -convert 100 USD EUR + -quote AAPL + -play {youtube url/search terms} + -{pause, unpause, skip, queue} +``` + +

+ +Input arguments (optional features) +```shell +python discord-bot/main.py --help +usage: main.py [-h] [-sp] [-cd] [-rt RESPONSETIME] + +optional arguments: + -h, --help show this help message and exit + -sp, --speechdetection + show speech detection print in console + -cd, --channeldialog hide dialog User/BOT in channel chat + -rt RESPONSETIME, --responsetime RESPONSETIME + time for user to ask something to BOT in voice channel +``` + +
+ +SETUP: You need to change the `.env` file according to your data: +```python +# .env file you need to change +# check .env.example for instructions + +DEVICE_ID = "something-something" + +DEVICE_MODEL_ID = "-something-something" + +CREDENTIALS = '{'key1': 'value1', 'key2': 'value2'}' + +ASSISTANT_TOKEN = "Token from credentials.json" + +DISCORD_TOKEN = "discord Token" +``` +``` +virtualenv venv -p python3.9 # create virtualenv +source venv/bin/activate + +# install dependencies +pip install -r requirements +sudo apt update; sudo apt-install ffmpeg + +python discord-bot/main.py # start discord app +``` +To add: : (1) Top.gg integrationn + +
+ +

Tested in Ubuntu 18.04, Python3.9

+ diff --git a/discord-bot/assist.py b/discord-bot/assist.py new file mode 100644 index 0000000..2731f42 --- /dev/null +++ b/discord-bot/assist.py @@ -0,0 +1,80 @@ + +import json +import logging +import os + +from google.assistant.embedded.v1alpha2 import (embedded_assistant_pb2, embedded_assistant_pb2_grpc) +from google.assistant.embedded.v1alpha2.embedded_assistant_pb2 import ( + AssistConfig, AssistRequest, AudioOutConfig, DeviceConfig, DialogStateIn, + ScreenOutConfig) +from google.assistant.embedded.v1alpha2.embedded_assistant_pb2_grpc import \ + EmbeddedAssistantStub +from google.auth.transport.grpc import secure_authorized_channel +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from html2text import html2text + +ASSISTANT_API_ENDPOINT = 'embeddedassistant.googleapis.com' +DEFAULT_GRPC_DEADLINE = 60 * 3 + 5 + + +class Assistant(object): + def __init__(self, language_code='en-US', device_model_id=None, device_id=None, credentials=None, token=None): + loaded_credentials, http_request = self.load_oath2_credentials( + credentials, token) + channel = secure_authorized_channel( + loaded_credentials, http_request, ASSISTANT_API_ENDPOINT) + logging.info('Connecting to %s', ASSISTANT_API_ENDPOINT) + + self.stub = EmbeddedAssistantStub(channel) + self.language_code = language_code + self.device_model_id = device_model_id + self.device_id = device_id + self.conversation_state = None + + def text_assist(self, query: str, is_new_conversation=True) -> str: + result: str = None + better_result: str = None + for response in self.stub.Assist(self.iter_text_assist_requests(query, is_new_conversation=is_new_conversation), DEFAULT_GRPC_DEADLINE): + if response.dialog_state_out.conversation_state: + self.conversation_state = response.dialog_state_out.conversation_state + if response.dialog_state_out.supplemental_display_text: + result = response.dialog_state_out.supplemental_display_text + if response.screen_out.data: + better_result = html2text(response.screen_out.data) + + if result == None: + return "I don't have an answer for what you are looking for!" + else: + return result + + def iter_text_assist_requests(self, query, is_new_conversation): + config = AssistConfig( + audio_out_config=AudioOutConfig( + encoding='LINEAR16', + sample_rate_hertz=16000, + volume_percentage=0, + ), + dialog_state_in=DialogStateIn( + language_code=self.language_code, + conversation_state=self.conversation_state, + is_new_conversation=is_new_conversation, + ), + device_config=DeviceConfig( + device_id=self.device_id, + device_model_id=self.device_model_id, + ), + text_query=query, + ) + req = AssistRequest(config=config) + yield req + + def load_oath2_credentials(self, credentials: str, token: str): + loaded_credentials = Credentials( + token=token, + **(credentials) + ) + + http_request = Request() + loaded_credentials.refresh(http_request) + return loaded_credentials, http_request \ No newline at end of file diff --git a/discord-bot/constants.py b/discord-bot/constants.py new file mode 100644 index 0000000..b247de0 --- /dev/null +++ b/discord-bot/constants.py @@ -0,0 +1,146 @@ +INVOCATION_PREFIXES = ['-', '!', 'google '] + +HELP_MESSAGE ="""```\nVoice commands\n + tell me a joke + temperature braga {in centigrades} + rain today lisbon + what's python language? + time in japan + day in 29/04/1998 + 423*12+23-1 + spin the wheel + cristiano ronaldo {born, married, age, play for} + who wrote rich dad poor dad + 10cm to inch + real madrid win + surprise me + \nChat commands (+ voice commands)\n + -translate hello {en, english} {pt, portuguese} + -convert 100 USD EUR + -quote AAPL + -play {youtube url/search terms} + -{pause, unpause, skip, queue}``` + """ + +CURRENCIES = [ + 'USD','JPY','BGN','CZK','DKK','GBP', + 'HUF','PLN','RON','SEK','CHF','NOK', + 'HRK','RUB','TRY','AUD','BRL','CAD', + 'CNY','HKD','IDR','INR','KRW','MXN', + 'MYR','NZD','PHP','SGD','THB','ZAR', + 'EUR', 'ILS' +] + +LANGUAGES = { + 'af': 'afrikaans', + 'sq': 'albanian', + 'am': 'amharic', + 'ar': 'arabic', + 'hy': 'armenian', + 'az': 'azerbaijani', + 'eu': 'basque', + 'be': 'belarusian', + 'bn': 'bengali', + 'bs': 'bosnian', + 'bg': 'bulgarian', + 'ca': 'catalan', + 'ceb': 'cebuano', + 'ny': 'chichewa', + 'zh-cn': 'chinese (simplified)', + 'zh-tw': 'chinese (traditional)', + 'co': 'corsican', + 'hr': 'croatian', + 'cs': 'czech', + 'da': 'danish', + 'nl': 'dutch', + 'en': 'english', + 'eo': 'esperanto', + 'et': 'estonian', + 'tl': 'filipino', + 'fi': 'finnish', + 'fr': 'french', + 'fy': 'frisian', + 'gl': 'galician', + 'ka': 'georgian', + 'de': 'german', + 'el': 'greek', + 'gu': 'gujarati', + 'ht': 'haitian creole', + 'ha': 'hausa', + 'haw': 'hawaiian', + 'iw': 'hebrew', + 'he': 'hebrew', + 'hi': 'hindi', + 'hmn': 'hmong', + 'hu': 'hungarian', + 'is': 'icelandic', + 'ig': 'igbo', + 'id': 'indonesian', + 'ga': 'irish', + 'it': 'italian', + 'ja': 'japanese', + 'jw': 'javanese', + 'kn': 'kannada', + 'kk': 'kazakh', + 'km': 'khmer', + 'ko': 'korean', + 'ku': 'kurdish (kurmanji)', + 'ky': 'kyrgyz', + 'lo': 'lao', + 'la': 'latin', + 'lv': 'latvian', + 'lt': 'lithuanian', + 'lb': 'luxembourgish', + 'mk': 'macedonian', + 'mg': 'malagasy', + 'ms': 'malay', + 'ml': 'malayalam', + 'mt': 'maltese', + 'mi': 'maori', + 'mr': 'marathi', + 'mn': 'mongolian', + 'my': 'myanmar (burmese)', + 'ne': 'nepali', + 'no': 'norwegian', + 'or': 'odia', + 'ps': 'pashto', + 'fa': 'persian', + 'pl': 'polish', + 'pt': 'portuguese', + 'pa': 'punjabi', + 'ro': 'romanian', + 'ru': 'russian', + 'sm': 'samoan', + 'gd': 'scots gaelic', + 'sr': 'serbian', + 'st': 'sesotho', + 'sn': 'shona', + 'sd': 'sindhi', + 'si': 'sinhala', + 'sk': 'slovak', + 'sl': 'slovenian', + 'so': 'somali', + 'es': 'spanish', + 'su': 'sundanese', + 'sw': 'swahili', + 'sv': 'swedish', + 'tg': 'tajik', + 'ta': 'tamil', + 'te': 'telugu', + 'th': 'thai', + 'tr': 'turkish', + 'uk': 'ukrainian', + 'ur': 'urdu', + 'ug': 'uyghur', + 'uz': 'uzbek', + 'vi': 'vietnamese', + 'cy': 'welsh', + 'xh': 'xhosa', + 'yi': 'yiddish', + 'yo': 'yoruba', + 'zu': 'zulu', +} + +FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'options': '-vn'} + +YDL_OPTIONS = {'format': 'bestaudio/best', 'noplaylist':'True'} \ No newline at end of file diff --git a/discord-bot/keep_alive.py b/discord-bot/keep_alive.py new file mode 100644 index 0000000..4fcd39e --- /dev/null +++ b/discord-bot/keep_alive.py @@ -0,0 +1,15 @@ +from flask import Flask +from threading import Thread + +app = Flask('') + +@app.route('/') +def home(): + return "Hello. I am alive!" + +def run(): + app.run(host='0.0.0.0',port=8080) + +def keep_alive(): + t = Thread(target=run) + t.start() \ No newline at end of file diff --git a/discord-bot/main.py b/discord-bot/main.py new file mode 100644 index 0000000..31ae179 --- /dev/null +++ b/discord-bot/main.py @@ -0,0 +1,427 @@ +import os +import re +import json +import time +import discord +import youtube_dl + +import yfinance as yf +import speech_recognition as sr + +from currency_converter import CurrencyConverter +from translate import Translator + +from pydub import AudioSegment +from pydub.silence import split_on_silence +from youtubesearchpython import VideosSearch +from discord.ext.commands import AutoShardedBot +from discord.utils import get +from discord.ext import tasks + +from youtubesearchpython import VideosSearch +from argparse import ArgumentParser +from decouple import config +from assist import Assistant +from gtts import gTTS + +from constants import * +from keep_alive import keep_alive + + +def combined_sounds(path1:str, path2:str): + """combine two wav files into one var""" + sound1 = AudioSegment.from_wav(path1) + sound2 = AudioSegment.from_wav(path2) + + return sound1 + sound2 + +def remove_accents(raw_text:str) -> str : + """Strip accents from input string""" + raw_text = re.sub(u"[àáâãäå]", 'a', raw_text) + raw_text = re.sub(u"[èéêë]", 'e', raw_text) + raw_text = re.sub(u"[ìíîï]", 'i', raw_text) + raw_text = re.sub(u"[òóôõö]", 'o', raw_text) + raw_text = re.sub(u"[ùúûü]", 'u', raw_text) + raw_text = re.sub(u"[ýÿ]", 'y', raw_text) + raw_text = re.sub(u"[ß]", 'ss', raw_text) + raw_text = re.sub(u"[ñ]", 'n', raw_text) + return raw_text + +def speech_to_tex(path:str): + """" + convert audio from .wav file to text using + google speech recognition API. + """ + sound = AudioSegment.from_wav(path) + chunks = split_on_silence(sound, + min_silence_len = 500, + silence_thresh = sound.dBFS-14, + keep_silence=500, + ) + folder_name = "audio-chunks" + if not os.path.isdir(folder_name): + os.mkdir(folder_name) + + # whole_text = "" + whole_text = [] + for i, audio_chunk in enumerate(chunks, start=1): + chunk_filename = os.path.join(folder_name, f"chunk{i}.wav") + audio_chunk.export(chunk_filename, format="wav") + + with sr.AudioFile(chunk_filename) as source: + try: + audio_listened = r.record(source) + try: + text = r.recognize_google(audio_listened, language = 'en-US', show_all=True) + if text['alternative'][0]['confidence'] < 0.7: + text['alternative'][0]['transcript'] = "*inaudible*" + text = text['alternative'][0]['transcript'] + except sr.UnknownValueError as e: + text = "*inaudible*" + else: + # whole_text += text + whole_text.append(text) + except: + whole_text.append('') + continue + + return whole_text + +def deEmojify(text:str) -> str: + """Remove special chars and emojis from string""" + regrex_pattern = re.compile(pattern = "[" + u"\U0001F600-\U0001F64F" # emoticons + u"\U0001F300-\U0001F5FF" # symbols & pictographs + u"\U0001F680-\U0001F6FF" # transport & map symbols + u"\U0001F1E0-\U0001F1FF" # flags (iOS) + "]+", flags = re.UNICODE) + return regrex_pattern.sub(r'',text) + +class AssistantDiscordBot(AutoShardedBot): + """Responds to Discord User Queries""" + + def __init__( + self, + device_model_id=None, + device_id=None, + credentials=None, + token=None): + super(AssistantDiscordBot, self).__init__( + command_prefix=INVOCATION_PREFIXES, + fetch_offline_members=False, + case_insensitive=True, + help_command=None + ) + self.assistant = Assistant( + device_model_id=device_model_id, + device_id=device_id, + credentials=credentials, + token=token + ) + self.waiting = False + self.timer = 0 + self.youtubePlaying = False + self.ytQueue = {} + self.ytvidTimer = 0 + self.MusicStatus = False + + +if __name__ == '__main__': + + parser = ArgumentParser() + parser.add_argument('-sp','--speechdetection', action="store_true", + help='show speech detection print in console') + parser.add_argument('-cd','--channeldialog', action="store_true", + help='hide dialog User/BOT in channel chat') + parser.add_argument('-rt','--responsetime', default="5", required=False, + help='time for user to ask something to BOT in voice channel') + + parser = parser.parse_args() + + device_model_id = config("DEVICE_MODEL_ID") + device_id = config("DEVICE_ID") + assistant_token = config("ASSISTANT_TOKEN") + credentials = config("CREDENTIALS") + + discord_token = config("DISCORD_TOKEN") + + credentials = json.loads(credentials) + credentials.pop('token', None) + + client = AssistantDiscordBot( + device_model_id=device_model_id, + device_id=device_id, + credentials=credentials, + token=assistant_token, + ) + + r = sr.Recognizer() + c = CurrencyConverter() + + @tasks.loop(seconds = 1) + async def check_voice(ctx): + """ + Check if Bot detected user voice and give user + x more seconds to talk and then cancels it to + run the callback function "voiceReceiver_callback" + """ + + vc = get(ctx.bot.voice_clients, guild=ctx.guild) + if [f for f in os.listdir() if f.endswith('.pcm')] and not client.waiting: + client.waiting = True + client.timer = time.time() + + if client.waiting and (time.time()-client.timer >= int(parser.responsetime)): + vc.stop_recording() + client.waiting = False + elif not client.waiting and not vc.recording: + vc.start_recording(discord.Sink(encoding='wav', filters={'time': 0}), voiceReceiver_callback, ctx) + + async def playAfterGoogle(ctx): + ''' Resume music after Google speaks ''' + voice = get(ctx.bot.voice_clients, guild=ctx.guild) + while voice.is_playing(): + pass + + await play(ctx, client.ytQueue[ctx.guild.id][0], client.ytvidTimer) + + async def voiceReceiver_callback(sink, channel, *args): + """auto-execute after Discord.Sink(record_voice) gets cancelled""" + + user_texts = [] + wavFiles = [f for f in os.listdir() if f.endswith('.wav')] + + if not len(wavFiles): return + + for file in wavFiles: + currText = speech_to_tex(file) + user_texts.append(currText[0]) + + [os.remove(f) for f in wavFiles] + + if parser.speechdetection: print(user_texts) + + newUser_texts = [] + for text in user_texts: + if list(filter(text.lower().startswith, INVOCATION_PREFIXES)) != []: + newText = text.lower().replace(list(filter(text.lower().startswith, INVOCATION_PREFIXES))[0], '') + if newText != '': newUser_texts.append(newText) + + if not len(newUser_texts): return + + for text in newUser_texts: + assistant_response = deEmojify(client.assistant.text_assist(remove_accents(text))) + + if not parser.channeldialog: + await channel.send(f'```\nUser:{text}\n\nAssistant: {assistant_response}\n```') + + gTTS(text=assistant_response, lang='en', slow=False).save('botPlay.mp3') + + client.ytvidTimer = time.time() - client.ytvidTimer + + try: + voice = get(channel.bot.voice_clients, guild=channel.guild) + voice.pause() + + voice.play(discord.FFmpegPCMAudio('botPlay.mp3', options = "-loglevel panic")) + voice.source.volume = 0.8 + + await playAfterGoogle(channel) + except: + pass + + @client.event + async def on_ready(): + """Bot-ready message""" + [os.remove(f) for f in os.listdir() if f.endswith(('.wav', '.pcm'))] + print(client.user.name, 'ready!') + + @client.command(pass_context=True) + async def help(ctx): + """-help to show helpfull Bot commands""" + await ctx.send(HELP_MESSAGE) + return + + @client.command(pass_context=True) + async def join(ctx): + """Bot to join voice room on "join" command""" + if ctx.author.voice: + channel = ctx.message.author.voice.channel + await channel.connect() + check_voice.start(ctx) + try: client.ytQueue[ctx.guild.id]; + except: client.ytQueue[ctx.guild.id] = []; + else: + await ctx.send("You're not in a voice room.") + return + + @client.command(pass_context=True) + async def leave(ctx): + """Bot to leave voice room on "leave" command""" + if ctx.voice_client: + await ctx.guild.voice_client.disconnect() + check_voice.stop() + check_music.stop() + try: del client.ytQueue[ctx.guild.id]; + except: pass; + else: + await ctx.send("I'm not in a voice room.") + return + + @tasks.loop(seconds = 2) + async def check_music(ctx): + """Play music in the queue if current music stops""" + voice = get(client.voice_clients, guild=ctx.guild) + + if not client.ytQueue[ctx.guild.id] or not client.MusicStatus: + return + + if not voice.is_playing(): + if client.youtubePlaying: + del client.ytQueue[ctx.guild.id][0] + if (client.ytQueue[ctx.guild.id]): + await play(ctx, client.ytQueue[ctx.guild.id][0]) + + @client.command(aliases=['p'], pass_context=True) + async def play(ctx, url: str, timestamp: int = 0): + """Bot to play youtube url on "play {url}" command""" + voice = get(client.voice_clients, guild=ctx.guild) + + if voice.is_playing(): + if not url in client.ytQueue[ctx.guild.id]: + client.ytQueue[ctx.guild.id].append(url) + with youtube_dl.YoutubeDL(YDL_OPTIONS) as ydl: + info = ydl.extract_info(url, download=False) + await ctx.send(info.get('title', None)[:30] + " added to the queue.") + return + + with youtube_dl.YoutubeDL(YDL_OPTIONS) as ydl: + if not url in client.ytQueue[ctx.guild.id]: client.ytQueue[ctx.guild.id].append(url) + client.ytvidTimer = time.time() - timestamp + client.youtubePlaying = True + + info = ydl.extract_info(url, download=False) + I_URL = info['formats'][0]['url'] + FFMPEG_OPTIONS['options'] = '-vn -ss ' + str(timestamp) + source = await discord.FFmpegOpusAudio.from_probe(I_URL, **FFMPEG_OPTIONS) + voice.play(source) + voice.source.volume = 0.8 + + await ctx.send(info.get('title', None)[:22] + " is now playing.") + + if not check_music.is_running(): + check_music.start(ctx) + + @client.command(pass_context=True) + async def pause(ctx): + """pause current music""" + client.MusicStatus = False + voice = get(client.voice_clients, guild=ctx.guild) + client.ytvidTimer = time.time() - client.ytvidTimer + voice.pause() + check_music.stop() + + @client.command(pass_context=True) + async def unpause(ctx): + """unpause current music""" + client.MusicStatus = True + await play(ctx, client.ytQueue[ctx.guild.id][0], client.ytvidTimer) + + @client.command(pass_context=True) + async def skip(ctx): + """skip current music""" + voice = get(client.voice_clients, guild=ctx.guild) + voice.pause() + if len(client.ytQueue[ctx.guild.id]) >= 1: + del client.ytQueue[ctx.guild.id][0] + if (client.ytQueue[ctx.guild.id]): + await play(ctx, client.ytQueue[ctx.guild.id][0]) + + @client.command(aliases=['queue', 'q'], pass_context=True) + async def playlist(ctx): + """list all queued music""" + voice = get(client.voice_clients, guild=ctx.guild) + if (client.ytQueue[ctx.guild.id]): + outStr = "Music Playlist```" + with youtube_dl.YoutubeDL(YDL_OPTIONS) as ydl: + for i, url in enumerate(client.ytQueue[ctx.guild.id]): + info = ydl.extract_info(url, download=False) + outStr += f'\n{i+1}. ' + info.get('title', None)[:35] + if not i and voice.is_playing(): outStr += " [Playing]" + await ctx.send(outStr + "```") + else: await ctx.send("The music queue is empty.") + + @client.command(pass_context=True) + async def quote(ctx, ticker: str): + """quote {AAPL, TSLA} to get company stock price""" + try: + await ctx.send(str(yf.Ticker(ticker).info['currentPrice'])+'$') + return + except: + await ctx.send("I can't get any data from this stock ticker.") + return + + @client.command(pass_context=True) + async def convert(ctx, value: float, orig: str, dest: str): + """convert {value} {USD} {EUR} to convert currencies""" + if orig.upper() in CURRENCIES and \ + dest.upper() in CURRENCIES: + await ctx.send('{:.2f} {}'.format(c.convert(value,orig,dest), dest.upper())) + return + else: + await ctx.send(f'''\nAvailable currencies\n```\n{CURRENCIES}\n```''') + return + + @client.event + async def on_message(message): + """Google assistant on messages starting with {INVOCATION_PREFIXES}""" + + if message.author.bot: + return + + lower_content = message.content.lower() + + # If message don't start with {INVOCATION_PREFIXES} then ignore it + if list(filter(lower_content.startswith, INVOCATION_PREFIXES)) == []: + return + + lower_content = lower_content.replace(list(filter(lower_content.startswith, INVOCATION_PREFIXES))[0], '') + + # If it is a known command without the prefix, BOT execute it + if list(filter(lower_content.startswith, list(client.all_commands.keys()))) != []: + if lower_content.startswith('play'): + if not 'www' in lower_content.split()[1:] and not 'http' in lower_content.split()[1:]: + ctx = await client.get_context(message) + await play(ctx, VideosSearch(' '.join(lower_content.split()[1:]), limit = 1).resultComponents[0]['link']) + return + await client.process_commands(message) + return + + # -translate {text} {en} {pt} to translate text + if 'translate' in lower_content.split()[0][1:]: + origLan, destLan = lower_content.split()[-2], lower_content.split()[-1] + if origLan in list(LANGUAGES.values())+list(LANGUAGES.keys()) and \ + destLan in list(LANGUAGES.values())+list(LANGUAGES.keys()): + if not destLan in list(LANGUAGES.keys()): + destLan = list(LANGUAGES.keys())[list(LANGUAGES.values()).index(destLan)] + if not origLan in list(LANGUAGES.keys()): + origLan = list(LANGUAGES.keys())[list(LANGUAGES.values()).index(origLan)] + await message.channel.send( + Translator( + from_lang=origLan, + to_lang=destLan + ).translate(lower_content[10:].replace( + lower_content[lower_content.index(lower_content.split()[-2]):],'')) + ) + return + else: + await message.channel.send(f'''\nAvailable languages\n```\n{list(LANGUAGES.values())}\n```''') + return + + assistant_response = client.assistant.text_assist(lower_content[1:]) + + if assistant_response: + await message.channel.send(assistant_response) + + keep_alive() + client.run(discord_token) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b0493ec --- /dev/null +++ b/requirements.txt @@ -0,0 +1,21 @@ +currencyconverter==0.16.3 +dblpy==0.4.0 +futures==3.1.1 +google-assistant-grpc==0.2.1 +google-assistant-sdk==0.6.0 +gtts==2.2.3 +html2text==2020.1.16 +httpx==0.13.3 +pathlib2==2.3.6 +pydub==0.25.1 +pynacl==1.4.0 +pyopenssl==20.0.1 +python-decouple==3.4 +sounddevice==0.3.15 +speechrecognition==3.8.1 +tenacity==4.12.0 +translate==3.6.1 +yfinance==0.1.63 +youtube-dl==2021.6.6 +youtube-search-python +git+https://github.com/Sheepposu/discord.py.git \ No newline at end of file