From cd7358f94345f82ac92da3ced4f295973120a195 Mon Sep 17 00:00:00 2001 From: scisneromam Date: Sun, 21 Jan 2024 22:58:25 +0100 Subject: [PATCH 1/3] feat: MaxSongLength in seconds --- .gitignore | 185 ++++++++++++++++++++++++++++++++++++++++++++++++ src/bongocat.js | 36 ++++++++++ 2 files changed, 221 insertions(+) diff --git a/.gitignore b/.gitignore index 9193f81..ee0ad21 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,186 @@ +# -------------------------------------------------------------------------------- +# File created using 'AnGitIgnored' extensions for Visual Studio Code: +# https://marketplace.visualstudio.com/items?itemName=AnAppWiLos.angitignored +#------------------------------------------------------------------------------- +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# 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 + +# 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 + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__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/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + src/config.js + +**/.env \ No newline at end of file diff --git a/src/bongocat.js b/src/bongocat.js index dc63957..a87e2df 100644 --- a/src/bongocat.js +++ b/src/bongocat.js @@ -37,12 +37,14 @@ var minBpm = 50; var bpm = {}; bpm.user = {}; +var parts = {}; var queue = []; var bongoEnabled = true; var playing = false; setBPM(128); var githubUrl = "https://raw.githubusercontent.com/jvpeek/twitch-bongocat/master/songs/"; var stackMode = false; +var maxSongLength = 90_000; //90 secs var defaultNotation = "bongo"; var currentSong = null; @@ -69,6 +71,18 @@ function setVolume(volumeParam) volume = Math.min(1.0, Math.max(0, Number(volumeParam))); } +function setMaxSongLength(maxSongLengthParam) +{ + maxSongLength = Number(maxSongLengthParam); + if (maxSongLength > 0) + { + maxSongLength *= 1000; + } else + { + maxSongLength = -1; + } +} + function setBPM(targetBPM, username) { targetBPM = Number(targetBPM); @@ -289,6 +303,11 @@ function checkQueue() console.log(playbacks); for (let playback of playbacks) { + if (maxSongLength > 0 && playback.time > maxSongLength) + { + currentSong.timeoutIDs.push(setTimeout(outroAnimation, maxSongLength + 1000)); //1 sek + break; + } currentSong.timeoutIDs.push(setTimeout(playback.cmd, playback.time, ...playback.args)); } } @@ -399,6 +418,17 @@ function setVolumeCommand(args) commands["!bongovolume"] = setVolumeCommand; +function setMaxSongLengthCommand(args) +{ + if (!isSuperUser(args.tags)) + { + return; + } + setMaxSongLength(args.arg); +} + +commands["!bongomaxsonglength"] = setMaxSongLengthCommand; + // ====================================================== // // ==================== user commands =================== // // ====================================================== // @@ -530,6 +560,12 @@ if (params.get("stackMode")) stackMode = true; } +let maxSongLengthParam = params.get("minBpm"); +if (maxSongLengthParam && !isNaN(Number(maxSongLengthParam))) +{ + setMaxSongLength(maxSongLengthParam); +} + let volumeParam = params.get("volume"); if (volumeParam && !isNaN(Number(volumeParam))) { From 543aeda0e5e212f5ffb2d802c5e7b557edd7bb47 Mon Sep 17 00:00:00 2001 From: scisneromam Date: Wed, 24 Jan 2024 12:08:04 +0100 Subject: [PATCH 2/3] feat: Allow multiple github urls --- .gitignore | 4 +++- src/bongocat.js | 27 ++++++++++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index ee0ad21..67b9edd 100644 --- a/.gitignore +++ b/.gitignore @@ -183,4 +183,6 @@ pyrightconfig.json src/config.js -**/.env \ No newline at end of file +**/.env + +**/*.pem diff --git a/src/bongocat.js b/src/bongocat.js index a87e2df..3d80e91 100644 --- a/src/bongocat.js +++ b/src/bongocat.js @@ -42,7 +42,7 @@ var queue = []; var bongoEnabled = true; var playing = false; setBPM(128); -var githubUrl = "https://raw.githubusercontent.com/jvpeek/twitch-bongocat/master/songs/"; +var githubUrls = ["https://raw.githubusercontent.com/jvpeek/twitch-bongocat/master/songs/","https://raw.githubusercontent.com/awsdcrafting/bongocat-songs/live/songs/"]; var stackMode = false; var maxSongLength = 90_000; //90 secs var defaultNotation = "bongo"; @@ -329,17 +329,22 @@ async function playFromGithub(song, user) song += ".json"; console.log("Playing", song, "from github for", user); - const response = await fetch(encodeURI(githubUrl + song.trim())); - if (response.status != 200) - { - return; + for (let githubUrl of githubUrls) { + const response = await fetch(encodeURI(githubUrl + song.trim())); + if (response.status != 200) + { + continue; + } + //console.log(response) + const jsonData = await response.json(); + console.log(jsonData); + jsonData.performer = user; + jsonData.dedications = dedications; + addToQueue(jsonData); + break } - //console.log(response) - const jsonData = await response.json(); - console.log(jsonData); - jsonData.performer = user; - jsonData.dedications = dedications; - addToQueue(jsonData); + + } From 086e8a9e602a86a09d8cc67785449949168bd7dd Mon Sep 17 00:00:00 2001 From: scisneromam Date: Wed, 24 Jan 2024 12:08:57 +0100 Subject: [PATCH 3/3] feat: Discord Bot --- discord_bot/.example.env | 4 + discord_bot/bot.py | 283 +++++++++++++++++++++++++++++++++++ discord_bot/requirements.txt | 24 +++ discord_bot/todos.txt | 3 + 4 files changed, 314 insertions(+) create mode 100644 discord_bot/.example.env create mode 100644 discord_bot/bot.py create mode 100644 discord_bot/requirements.txt create mode 100644 discord_bot/todos.txt diff --git a/discord_bot/.example.env b/discord_bot/.example.env new file mode 100644 index 0000000..88c6ea6 --- /dev/null +++ b/discord_bot/.example.env @@ -0,0 +1,4 @@ +DISCORD_BOT_TOKEN=your-token-here +GITHUB_APP_ID=000000 +GITHUB_APP_KEY_FILE=your-path-to-secret-key-file-here +GITHUB_APP_INSTALLATION_ID=111111 \ No newline at end of file diff --git a/discord_bot/bot.py b/discord_bot/bot.py new file mode 100644 index 0000000..cc96d06 --- /dev/null +++ b/discord_bot/bot.py @@ -0,0 +1,283 @@ +import os +from os.path import isfile +import sys +import json +import re +from typing import Optional +from dataclasses import dataclass + +import discord +from discord import CategoryChannel, ForumChannel, Interaction, Member, Message, TextStyle, app_commands, ui +from discord.utils import MISSING +from dotenv import load_dotenv + +from github import Github, Auth +from github.Repository import Repository + +intents = discord.Intents.default() +intents.message_content = True +client = discord.Client(intents=intents) +tree = app_commands.CommandTree(client) +github: Github +repo: Repository +allowed_role:str|None + +state_file_path = "./state.json" +state = {} + + +class SaveSongModal(ui.Modal, title="Save Song"): + + song_title = ui.TextInput(label="Song title") + file_name = ui.TextInput(label="Song name") + author = ui.TextInput(label="Song author") + #notation = ui.Select(options=[SelectOption(label="Modern (bongo+)", value="bongo+"), SelectOption(label="legacy", value="bongo+")]) + notation = ui.TextInput(label="Song notation") + notes = ui.TextInput(label="Song notes", style=TextStyle.paragraph) + extras = {} + + def __init__(self, *, title: str = MISSING, timeout: Optional[float] = None, custom_id: str = MISSING, default_song_title: str = "", default_file_name: str = "", default_author: str = "", default_notation="bongo+", default_notes: str = "", **kwargs) -> None: + self.song_title.default = default_song_title + self.file_name.default = default_file_name + self.author.default = default_author + self.notation.default = default_notation + #for option in self.notation.options: + # if option.value == default_notation: + # option.default = True + self.notes.default = default_notes + + for key, value in kwargs.items(): + self.extras[key] = value + + super().__init__(title=title, timeout=timeout, custom_id=custom_id) + + + + async def on_submit(self, interaction: Interaction) -> None: + global state + #message = interaction.extras["message"] + print(self.song_title.value) + print(self.file_name.value) + print(self.author.value) + print(self.notation.value) + print(self.notes.value) + print(interaction.extras) + print(self.extras) + print(interaction.message) + file_name = self.file_name.value.strip().replace(".json", "").replace(".", "") + file_name = re.sub(r"\s+", "_", file_name).replace("/", "").replace("\\", "") + file_name += ".json" + notation = self.notation.value + if notation == "modern" or notation == "default" or notation == "bongo": + notation = "bongo+" + if notation == "legacy" or notation == "old": + notation = "bongol" + if notation == "experimental" or notation == "experimentell": + notation = "bongox" + song = {"notes": self.notes.value.strip(), "author": self.author.value.strip(), "title": self.song_title.value.strip(), "notation": notation.strip()} + await interaction.response.send_message("Saving Song", ephemeral=True) + content = json.dumps(song, indent=4).strip() + content += "\n" + + repo.create_file(f"songs/{file_name}", f"Add Song {song['title']}", content) + + message : Message = self.extras["message"] + await message.add_reaction("💾") + await interaction.edit_original_response(content="Saved Song") + state["msgs"].add(message.id) + write_state() + +@dataclass +class Song(): + title: str + file_name: str + author: str + notation: str + notes: str + + +def parse_song(message: discord.Message): + + if not "!bongo" in message.content: + return None + + note_begin = message.content.find("!bongo") + title = message.content[:note_begin].strip() + note_begin = message.content.find(" ", note_begin+1) + notes = message.content[note_begin:].strip() + + notation = "bongo+" + if "!bongol" in message.content: + notation = "bongol" + elif "!bongo+" in message.content: + notation = "bongo+" + else: + #find actual notation + notation = "bongol" + if "#" in notes: + notation = "bongo+" + if "^" in notes: + notation = "bongo+" + if "v" in notes: + notation = "bongo+" + + pass + return Song(title, title.replace(" ", "_"), message.author.name, notation, notes) + + + +parsing_songs = False + +#@tree.command( +# name="get_old_songs", +# description="Parses the entire channel for old songs", +#) +async def get_old_songs(interaction : Interaction): + global parsing_songs + global state + channel = interaction.channel + if channel == None: + await interaction.response.send_message("No channel found for this command!", ephemeral=True) + return + if parsing_songs: + await interaction.response.send_message("Already parsing songs!", ephemeral=True) + return + + if isinstance(channel, ForumChannel) or isinstance(channel,CategoryChannel): + await interaction.response.send_message("Wrong Channel!", ephemeral=True) + return + + parsing_songs = True + try: + await interaction.response.defer(ephemeral=True, thinking=True) + after = state["last_time"] + msgs = channel.history(limit=None,after=after, oldest_first=True) + last_time = None + msg_count = 0 + songs = [] + state_msgs = state["msgs"] + async for msg in msgs: + if msg.id in state_msgs: + continue + state_msgs.append(msg.id) + msg_count += 1 + song = parse_song(msg) + if song: + songs.append(song) + last_time = msg.created_at + if msg.edited_at: + last_time = msg.edited_at + + state["last_time"] = last_time + await interaction.followup.send(f"Parsed {msg_count} messages found {len(songs)} songs!", ephemeral=True) + finally: + parsing_songs = False + write_state() + + +@tree.context_menu( + name="save_song" +) +@app_commands.describe(message="The Message with the song to save") +async def save_song(interaction: Interaction, message: Message): + global state + if message.id in state["msgs"]: + await interaction.response.send_message("Already parsed this song!", ephemeral=True) + return + if allowed_role: + role_id = int(allowed_role) + if not isinstance(interaction.user, Member): + await interaction.response.send_message("You do not have permission to save songs!", ephemeral=True) + return + allowed = False + for role in interaction.user.roles: + if role.id == role_id: + allowed = True + break + if not allowed: + await interaction.response.send_message("You do not have permission to save songs!", ephemeral=True) + return + + interaction.extras["message"] = message + song = parse_song(message) + if not song: + await interaction.response.send_message("No song detected!", ephemeral=True) + return + interaction.extras["song"] = song + interaction.extras["modal"] = SaveSongModal(default_song_title=song.title, default_file_name=song.file_name, default_author=song.author, default_notation=song.notation, default_notes=song.notes, message=message) + await interaction.response.send_modal(interaction.extras["modal"]) + + +@client.event +async def on_ready(): + print(f'We have logged in as {client.user}') + print("Syncing commands") + await tree.sync() + print("Commands synced") + +@client.event +async def on_message(message): + if message.author == client.user: + return + + if message.content.startswith('$hello'): + await message.channel.send('Hello!') + + +def write_state(): + global state + global state_file_path + with open(state_file_path, "w") as state_file: + state_data = {} + state_data["last_time"] = state["last_time"] + state_data["msgs"] = list(state["msgs"]) + json.dump(state_data, state_file) + +def read_state(): + global state + global state_file_path + if not os.path.isfile(state_file_path): + state = {"last_time": None, "msgs": []} + return + with open(state_file_path, "r") as state_file: + state_data = json.load(state_file) + state["last_time"] = state_data["last_time"] + state["msgs"] = set(state_data) + +if __name__ == "__main__": + load_dotenv() + read_state() + token = os.environ.get("DISCORD_BOT_TOKEN") + if not token: + print("No discord token provided!") + sys.exit(1) + allowed_role = os.environ.get("DISCORD_ALLOWED_ROLE") + + app_id = os.environ.get("GITHUB_APP_ID") + private_key_path = os.environ.get("GITHUB_APP_KEY_FILE") + installation_id = os.environ.get("GITHUB_APP_INSTALLATION_ID") + if not app_id: + print("No github app id provided!") + sys.exit(1) + if not private_key_path: + print("No github key provided!") + sys.exit(1) + if not os.path.isfile(private_key_path): + print("No github key provided!") + sys.exit(1) + private_key = "" + with open(private_key_path) as private_key_file: + private_key = private_key_file.read() + if not private_key: + print("No github key provided!") + sys.exit(1) + if not installation_id: + print("No github installation id provided!") + sys.exit(1) + + installation_id = int(installation_id) + auth = Auth.AppAuth(app_id, private_key).get_installation_auth(installation_id) + github = Github(auth=auth) + repo = github.get_repo("awsdcrafting/bongocat-songs") + + client.run(token) diff --git a/discord_bot/requirements.txt b/discord_bot/requirements.txt new file mode 100644 index 0000000..f890c4d --- /dev/null +++ b/discord_bot/requirements.txt @@ -0,0 +1,24 @@ +aiohttp==3.9.1 +aiosignal==1.3.1 +attrs==23.2.0 +certifi==2023.11.17 +cffi==1.16.0 +charset-normalizer==3.3.2 +cryptography==41.0.7 +Deprecated==1.2.14 +discord.py==2.3.2 +frozenlist==1.4.1 +idna==3.6 +multidict==6.0.4 +pycparser==2.21 +PyGithub==2.1.1 +PyJWT==2.8.0 +PyNaCl==1.5.0 +python-dateutil==2.8.2 +python-dotenv==1.0.0 +requests==2.31.0 +six==1.16.0 +typing_extensions==4.9.0 +urllib3==2.1.0 +wrapt==1.16.0 +yarl==1.9.4 \ No newline at end of file diff --git a/discord_bot/todos.txt b/discord_bot/todos.txt new file mode 100644 index 0000000..84eb7cc --- /dev/null +++ b/discord_bot/todos.txt @@ -0,0 +1,3 @@ +MAYBE: +TWITCH anbindung +