first commit
This commit is contained in:
commit
cdb89d5264
181
.gitignore
vendored
Normal file
181
.gitignore
vendored
Normal file
@ -0,0 +1,181 @@
|
||||
# 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
|
||||
|
||||
config/config.dev.json
|
||||
config/config.prod.json
|
||||
config/system_setting.txt
|
||||
config/word_dictionary.json
|
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
# Dockerfile
|
||||
FROM python:3.10-slim
|
||||
|
||||
# 基本パッケージのインストール
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ffmpeg
|
||||
|
||||
# Pythonパッケージのインストール
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
RUN pip install -r /app/requirements.txt
|
||||
|
||||
# アプリケーションのコピー
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
# エントリーポイントスクリプトをコピーして実行権限を付与
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
175
app/bot.py
Normal file
175
app/bot.py
Normal file
@ -0,0 +1,175 @@
|
||||
import discord
|
||||
import signal
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from utils.file_utils import read_text
|
||||
from config import Config
|
||||
from classes.VoiceSynthesizer import VoiceSynthesizer
|
||||
from classes.WordDictionary import WordDictionary
|
||||
from classes.CommandHandler import CommandHandler
|
||||
from classes.VoiceChannelManager import VoiceChannelManager
|
||||
from classes.MessageReader import MessageReader
|
||||
from classes.IndependenceChat import IndependenceChat
|
||||
from threading import Thread
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
class MyDiscordBot(discord.Client):
|
||||
def __init__(self, config, intents, system_setting_file_path="config/system_setting.txt"):
|
||||
super().__init__(intents=intents)
|
||||
self.config = config
|
||||
self.system_setting_file_path = system_setting_file_path
|
||||
self.voice_synthesizer = VoiceSynthesizer(
|
||||
config["voicepeak_executable_path"],
|
||||
config["voicevox_url"],
|
||||
config["default_navigator"]
|
||||
)
|
||||
self.word_dictionary = WordDictionary()
|
||||
self.prefix = config["prefix"]
|
||||
self.target_channel_id = None
|
||||
self.volume = 1.0
|
||||
self.emotion_happy = 50
|
||||
self.emotion_sad = 50
|
||||
self.emotion_angry = 0
|
||||
self.emotion_fun = 0
|
||||
self.is_name_reading = False
|
||||
self.is_independence_chat = False
|
||||
self.independence_chat = IndependenceChat(self)
|
||||
self.command_handler = CommandHandler(self)
|
||||
self.voice_channel_manager = VoiceChannelManager()
|
||||
self.message_reader = MessageReader(self.voice_synthesizer, self)
|
||||
|
||||
self.independence_channel_ids = config["independence_channel_ids"]
|
||||
self.independence_timers = []
|
||||
|
||||
# デバッグモードのチェック
|
||||
self.debug_mode_file = "debug_mode.txt"
|
||||
self.debug_mode_enabled = os.path.exists(self.debug_mode_file)
|
||||
|
||||
# シグナルハンドラを設定
|
||||
signal.signal(signal.SIGINT, self.shutdown)
|
||||
signal.signal(signal.SIGTERM, self.shutdown)
|
||||
|
||||
# Flaskサーバーを別スレッドで起動
|
||||
self.start_flask_server()
|
||||
|
||||
def start_flask_server(self):
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/debug', methods=['POST'])
|
||||
def toggle_debug():
|
||||
data = request.json
|
||||
if 'enable' not in data:
|
||||
return jsonify({'error': 'Invalid request'}), 400
|
||||
|
||||
if data['enable']:
|
||||
open(self.debug_mode_file, 'w').close()
|
||||
self.debug_mode_enabled = True
|
||||
logging.info("デバッグモードが有効になりました。")
|
||||
asyncio.run_coroutine_threadsafe(self.independence_chat.start_debug_mode_check(), self.loop)
|
||||
else:
|
||||
if os.path.exists(self.debug_mode_file):
|
||||
os.remove(self.debug_mode_file)
|
||||
self.debug_mode_enabled = False
|
||||
logging.info("デバッグモードが無効になりました。")
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
def run_flask():
|
||||
app.run(host='0.0.0.0', port=5000)
|
||||
|
||||
flask_thread = Thread(target=run_flask)
|
||||
flask_thread.daemon = True
|
||||
flask_thread.start()
|
||||
|
||||
def reset_emotion(self):
|
||||
"""感情の設定をリセットし、音声合成器に反映させる"""
|
||||
self.emotion_happy = 50
|
||||
self.emotion_sad = 50
|
||||
self.emotion_angry = 0
|
||||
self.emotion_fun = 0
|
||||
self.voice_synthesizer.set_emotion(self.emotion_happy, self.emotion_sad, self.emotion_angry, self.emotion_fun)
|
||||
self.voice_synthesizer.set_pitch(50)
|
||||
|
||||
async def on_ready(self):
|
||||
"""ボットが準備完了時に実行されるイベントハンドラ"""
|
||||
self.voice_synthesizer.set_emotion(self.emotion_happy, self.emotion_sad, self.emotion_angry, self.emotion_fun)
|
||||
self.voice_synthesizer.set_pitch(50)
|
||||
await self.set_bot_status(f"タイキチュウ… | {self.prefix}help")
|
||||
logging.info(f'{self.user} がログインしました')
|
||||
|
||||
# 独立チャットモードの自動参加を開始
|
||||
await self.independence_chat.start_independence_mode_check()
|
||||
|
||||
async def on_message(self, message):
|
||||
"""メッセージ受信時に実行されるイベントハンドラ"""
|
||||
await self.command_handler.handle_message(message)
|
||||
|
||||
async def on_voice_state_update(self, member, before, after):
|
||||
"""ボイスチャンネルの状態が更新された時に実行されるイベントハンドラ"""
|
||||
try:
|
||||
if before.channel is None and after.channel is not None:
|
||||
# ユーザーがVCに参加した場合
|
||||
if self.is_independence_chat and after.channel == self.voice_channel_manager.get_connected_channel(member.guild):
|
||||
greeting = await self.independence_chat.generate_greeting(member.display_name)
|
||||
await self.message_reader.read_message(greeting, self.word_dictionary, self.config["ffmpeg_executable_path"], self.volume)
|
||||
elif before.channel and not after.channel and before.channel == self.voice_channel_manager.get_connected_channel(member.guild):
|
||||
# ユーザーがVCから退出した場合
|
||||
members = [m for m in before.channel.members if m != self.user]
|
||||
if not members:
|
||||
self.target_channel_id = None
|
||||
await self.voice_channel_manager.leave_voice_channel(before.channel.guild, self)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to update voice state: {e}")
|
||||
raise
|
||||
|
||||
def is_valid_channel(self, message):
|
||||
"""メッセージが有効なチャンネルであるかどうかを確認する"""
|
||||
if self.target_channel_id and message.channel.id != self.target_channel_id:
|
||||
return False
|
||||
if not message.guild.voice_client or not message.guild.voice_client.is_connected():
|
||||
return False
|
||||
return True
|
||||
|
||||
def enable_independence_chat(self, voice_client):
|
||||
"""独立チャット機能を有効にする"""
|
||||
system_setting = read_text(self.system_setting_file_path)
|
||||
self.is_independence_chat = True
|
||||
self.independence_chat.enable(system_setting, voice_client, self.config["independence_character_name"])
|
||||
|
||||
def disable_independence_chat(self):
|
||||
"""独立チャット機能を無効にする"""
|
||||
self.is_independence_chat = False
|
||||
self.independence_chat.disable()
|
||||
|
||||
def shutdown(self, signum, frame):
|
||||
"""シグナルを受け取ってシャットダウンする"""
|
||||
logging.info("シャットダウン中...")
|
||||
asyncio.create_task(self.cleanup())
|
||||
|
||||
async def cleanup(self):
|
||||
"""クリーンアップ処理"""
|
||||
for guild in self.guilds:
|
||||
if guild.voice_client and guild.voice_client.is_connected():
|
||||
await self.voice_channel_manager.leave_voice_channel(guild, self)
|
||||
await self.close()
|
||||
|
||||
async def set_bot_status(self, status):
|
||||
"""ステータスを変更する"""
|
||||
activity = None
|
||||
if status:
|
||||
activity = discord.Activity(type=discord.ActivityType.listening, name=status)
|
||||
await self.change_presence(activity=activity)
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = Config.load_config()
|
||||
intents = discord.Intents.default()
|
||||
intents.guilds = True
|
||||
intents.messages = True
|
||||
intents.message_content = True
|
||||
intents.voice_states = True
|
||||
|
||||
bot = MyDiscordBot(config, intents)
|
||||
bot.run(config["discord_token"])
|
154
app/classes/CommandHandler.py
Normal file
154
app/classes/CommandHandler.py
Normal file
@ -0,0 +1,154 @@
|
||||
import logging
|
||||
from classes.VoiceChannelManager import VoiceChannelManager
|
||||
|
||||
class CommandHandler:
|
||||
def __init__(self, bot):
|
||||
"""CommandHandlerクラスの初期化を行う"""
|
||||
self.bot = bot
|
||||
self.voice_channel_manager = VoiceChannelManager()
|
||||
|
||||
async def handle_message(self, message):
|
||||
"""受信したメッセージを処理する"""
|
||||
if message.author.bot:
|
||||
return
|
||||
|
||||
try:
|
||||
if not message.content.startswith(self.bot.prefix):
|
||||
if not self.bot.is_valid_channel(message):
|
||||
return
|
||||
|
||||
if self.bot.is_independence_chat:
|
||||
input_message = f"{message.author.display_name}: {message.content}"
|
||||
await self.bot.independence_chat.input_message(input_message)
|
||||
else:
|
||||
await self.handle_synthesize(message)
|
||||
return
|
||||
|
||||
command, *args = message.content[len(self.bot.prefix):].split()
|
||||
command_handler = getattr(self, f'handle_{command}', None)
|
||||
if command_handler:
|
||||
await command_handler(message, args)
|
||||
else:
|
||||
await message.reply(f"不明なコマンドです。\n`{self.bot.prefix}help`で使用可能なコマンドを確認してください。")
|
||||
except Exception as e:
|
||||
logging.info(f"Failed to process message: {e}")
|
||||
raise
|
||||
|
||||
async def handle_join(self, message, args):
|
||||
"""ボイスチャンネルに接続する"""
|
||||
if message.author.voice and message.author.voice.channel:
|
||||
if message.guild.voice_client and message.guild.voice_client.is_connected():
|
||||
("他のボイスチャンネルに接続しています。")
|
||||
return
|
||||
self.bot.target_channel_id = message.channel.id
|
||||
await self.voice_channel_manager.join_voice_channel(message.author.voice.channel)
|
||||
await self.bot.set_bot_status("オシゴトチュウ…")
|
||||
else:
|
||||
await message.reply("ボイスチャンネルに接続してください。")
|
||||
|
||||
async def handle_ijoin(self, message, args):
|
||||
"""ボイスチャンネルに接続して独立チャットを有効にする"""
|
||||
await self.handle_join(message, args)
|
||||
await self.handle_ichat(message, args, log=False)
|
||||
|
||||
async def handle_leave(self, message, args):
|
||||
"""ボイスチャンネルから切断する"""
|
||||
self.bot.volume = 1.0
|
||||
self.bot.reset_emotion()
|
||||
self.bot.target_channel_id = None
|
||||
self.bot.is_name_reading = True
|
||||
self.bot.disable_independence_chat()
|
||||
await self.bot.set_bot_status(f"タイキチュウ… | {self.bot.prefix}help")
|
||||
await self.voice_channel_manager.leave_voice_channel(message.guild, self.bot)
|
||||
|
||||
async def handle_volume(self, message, args):
|
||||
"""ボットの音量を設定する"""
|
||||
try:
|
||||
if not self.bot.is_valid_channel(message):
|
||||
return
|
||||
new_volume = float(args[0])
|
||||
self.bot.volume = max(0.0, min(1.0, new_volume))
|
||||
await message.reply(f"音量を{self.bot.volume}に設定しました。")
|
||||
except (IndexError, ValueError):
|
||||
await message.reply(f"正しい音量を指定してください。例: `{self.bot.prefix}volume 0.5`")
|
||||
|
||||
async def handle_emotion(self, message, args):
|
||||
"""ボットの感情を設定する"""
|
||||
try:
|
||||
if not self.bot.is_valid_channel(message):
|
||||
return
|
||||
self.bot.emotion_happy = int(args[0])
|
||||
self.bot.emotion_sad = int(args[1])
|
||||
self.bot.emotion_angry = int(args[2])
|
||||
self.bot.emotion_fun = int(args[3])
|
||||
self.bot.voice_synthesizer.set_emotion(self.bot.emotion_happy, self.bot.emotion_sad, self.bot.emotion_angry, self.bot.emotion_fun)
|
||||
await message.reply(f"感情を設定しました。\nhappy={self.bot.emotion_happy}, sad={self.bot.emotion_sad}, angry={self.bot.emotion_angry}, fun={self.bot.emotion_fun}")
|
||||
except (IndexError, ValueError):
|
||||
await message.reply(f"正しい感情を指定してください。\n例: `{self.bot.prefix}emotion 100 0 0 0`")
|
||||
|
||||
async def handle_pitch(self, message, args):
|
||||
"""ボットの音程を設定する"""
|
||||
try:
|
||||
if not self.bot.is_valid_channel(message):
|
||||
return
|
||||
new_pitch = int(args[0])
|
||||
self.bot.pitch = max(-300, min(300, new_pitch))
|
||||
self.bot.voice_synthesizer.set_pitch(self.bot.pitch)
|
||||
await message.reply(f"音程を{self.bot.pitch}に設定しました。")
|
||||
except (IndexError, ValueError):
|
||||
await message.reply(f"正しい音程を指定してください。\n例: `{self.bot.prefix}pitch 50`")
|
||||
|
||||
async def handle_dictionary(self, message, args):
|
||||
"""単語の辞書登録を行う"""
|
||||
try:
|
||||
if not self.bot.is_valid_channel(message):
|
||||
return
|
||||
original_word, converted_word = args
|
||||
self.bot.word_dictionary.register_word(original_word, converted_word)
|
||||
await message.reply(f"'{original_word}'を'{converted_word}'として登録しました。")
|
||||
except ValueError:
|
||||
await message.reply(f"正しい形式で単語を指定してください。\n例: `{self.bot.prefix}dictionary 単語 読み方`")
|
||||
|
||||
async def handle_rname(self, message, args):
|
||||
"""名前の読み上げ機能のオン・オフを切り替える"""
|
||||
self.bot.is_name_reading = not self.bot.is_name_reading
|
||||
await message.reply(f"名前の読み上げを{'有効' if self.bot.is_name_reading else '無効'}にしました。")
|
||||
|
||||
async def handle_ichat(self, message, args, log=True):
|
||||
"""独立チャット機能のオン・オフを切り替える"""
|
||||
if not self.bot.is_valid_channel(message):
|
||||
return
|
||||
|
||||
bot_admin_user_id = self.bot.config["bot_admin_user_id"]
|
||||
if not message.author.id == bot_admin_user_id:
|
||||
await message.reply("権限がありません。")
|
||||
return
|
||||
|
||||
if self.bot.is_independence_chat:
|
||||
self.bot.disable_independence_chat()
|
||||
if log:
|
||||
await message.reply("独立チャットを無効にしました。")
|
||||
await self.bot.set_bot_status("オシゴトチュウ…")
|
||||
else:
|
||||
self.bot.enable_independence_chat(message.guild.voice_client)
|
||||
await self.bot.set_bot_status("オハナシチュウ…")
|
||||
if log:
|
||||
await message.reply("独立チャットを有効にしました。")
|
||||
|
||||
async def handle_help(self, message, args):
|
||||
"""ヘルプメッセージを表示する"""
|
||||
help_message = (f"**ボットの使い方**\n"
|
||||
f"> {self.bot.prefix}join: ボイスチャンネルに接続します。\n"
|
||||
f"> {self.bot.prefix}leave: ボイスチャンネルから切断します。\n"
|
||||
f"> {self.bot.prefix}volume: 音量を設定します。\n> \t例: `{self.bot.prefix}volume 0.5`\n"
|
||||
f"> {self.bot.prefix}emotion: 感情を設定します。\n> \t例: `{self.bot.prefix}emotion 100 0 0 0`\n"
|
||||
f"> {self.bot.prefix}pitch: 音程を設定します。\n> \t例: `{self.bot.prefix}pitch 50`\n"
|
||||
f"> {self.bot.prefix}dictionary: 単語の辞書登録を行います。\n> \t例: `{self.bot.prefix}dictionary 単語 読み方`\n"
|
||||
f"> {self.bot.prefix}rname: 名前の読み上げをするかどうかを変更します。\n"
|
||||
f"> {self.bot.prefix}help: ヘルプを表示します。\n")
|
||||
await message.reply(help_message)
|
||||
|
||||
async def handle_synthesize(self, message):
|
||||
"""メッセージの音声合成を行う"""
|
||||
logging.info(f"Synthesizing message: {message.content}")
|
||||
await self.bot.message_reader.read_message(message, self.bot.word_dictionary, self.bot.config["ffmpeg_executable_path"], self.bot.volume)
|
158
app/classes/IndependenceChat.py
Normal file
158
app/classes/IndependenceChat.py
Normal file
@ -0,0 +1,158 @@
|
||||
import openai
|
||||
import logging
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
import asyncio
|
||||
|
||||
class IndependenceChat:
|
||||
def __init__(self, bot):
|
||||
"""IndependenceChatクラスの初期化を行う"""
|
||||
self.bot = bot
|
||||
openai.api_key = self.bot.config["openai_api_key"]
|
||||
self.voice_client = None
|
||||
self.independence_character_name = None
|
||||
self.system = None
|
||||
self.input_list = []
|
||||
self.logs = []
|
||||
self.independence_timers = []
|
||||
self.log_file_path = "independence_chat_log.txt"
|
||||
|
||||
def enable(self, system_setting, voice_client, independence_character_name):
|
||||
"""独立チャット機能を有効にする"""
|
||||
self.system = {"role": "system", "content": system_setting}
|
||||
self.input_list = [self.system]
|
||||
self.voice_client = voice_client
|
||||
self.independence_character_name = independence_character_name
|
||||
self.log("独立チャットモードを有効にしました。")
|
||||
|
||||
def disable(self):
|
||||
"""独立チャット機能を無効にする"""
|
||||
self.voice_client = None
|
||||
self.independence_character_name = None
|
||||
self.system = None
|
||||
self.input_list = []
|
||||
self.log("独立チャットモードを無効にしました。")
|
||||
|
||||
async def input_message(self, input_text):
|
||||
"""ユーザーからの入力メッセージを処理し、応答を生成する"""
|
||||
self.input_list.append({"role": "user", "content": input_text})
|
||||
self.logs.append(input_text)
|
||||
self.log(input_text)
|
||||
|
||||
response = openai.ChatCompletion.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=self.input_list
|
||||
)
|
||||
|
||||
# 応答の最初の選択肢を取得し、メッセージ内容を取り出す
|
||||
result = response.choices[0].message['content']
|
||||
surgery_result = f"{self.independence_character_name}: {result}"
|
||||
self.logs.append(surgery_result)
|
||||
self.log(surgery_result)
|
||||
|
||||
self.input_list.append({"role": "assistant", "content": result})
|
||||
|
||||
logging.info("最新の3件の会話ログ:")
|
||||
for log_entry in self.input_list[-3:]:
|
||||
logging.info(log_entry)
|
||||
|
||||
await self.bot.message_reader.read_message(
|
||||
result, self.bot.word_dictionary, self.bot.config["ffmpeg_executable_path"], self.bot.volume
|
||||
)
|
||||
|
||||
async def start_independence_mode_check(self):
|
||||
"""独立チャットモードでランダムにVCに参加する処理を開始"""
|
||||
if self.independence_timers:
|
||||
for timer in self.independence_timers:
|
||||
timer.cancel()
|
||||
self.independence_timers = []
|
||||
|
||||
# 24時間を時間帯によってランダムにチェックする時間を設定
|
||||
now = datetime.now()
|
||||
for hour in range(24):
|
||||
if 18 <= hour or hour < 2: # 18時から2時の間
|
||||
checks_per_hour = 2 # 1時間に2回チェック
|
||||
else:
|
||||
checks_per_hour = 1 # 2時間に1回チェック
|
||||
|
||||
for _ in range(checks_per_hour):
|
||||
target_time = now.replace(hour=hour, minute=random.randint(0, 59), second=0, microsecond=0)
|
||||
if target_time < now:
|
||||
target_time += timedelta(days=1)
|
||||
delay = (target_time - now).total_seconds()
|
||||
timer = self.bot.loop.call_later(delay, self.bot.loop.create_task, self.check_independence_mode())
|
||||
self.independence_timers.append(timer)
|
||||
|
||||
async def check_independence_mode(self):
|
||||
"""独立チャットモードでランダムにVCに参加する"""
|
||||
try:
|
||||
if self.bot.voice_channel_manager.get_connected_channel(self.bot.guilds[0]) is not None:
|
||||
logging.info("すでにVCに参加しています。自動参加をキャンセルします。")
|
||||
self.log("VCに参加しようとしましたが、既に他のVCに参加していました。")
|
||||
return
|
||||
|
||||
channel = random.choice(self.bot.independence_channel_ids)
|
||||
voice_channel = self.bot.get_channel(channel)
|
||||
if voice_channel is None or not hasattr(voice_channel, 'members'):
|
||||
logging.warning("Voice channel not found or has no members attribute")
|
||||
return
|
||||
if len(voice_channel.members) == 0:
|
||||
dummy_message = type('Dummy', (object,), {
|
||||
"author": type('Author', (object,), {
|
||||
"voice": type('VoiceState', (object,), {"channel": voice_channel})(),
|
||||
"display_name": "IndependenceBot"
|
||||
})(),
|
||||
"channel": type('Channel', (object,), {"id": None})(),
|
||||
"guild": voice_channel.guild
|
||||
})()
|
||||
await self.bot.command_handler.handle_ijoin(dummy_message, [])
|
||||
self.bot.loop.create_task(self.leave_if_empty(voice_channel))
|
||||
else:
|
||||
logging.info("Voice channel has members")
|
||||
self.log("VCに参加しようとしましたが、既に誰かが参加していました。")
|
||||
finally:
|
||||
await self.start_independence_mode_check() # タイマーを再設定
|
||||
|
||||
async def leave_if_empty(self, voice_channel):
|
||||
"""一定時間誰も来なければVCを退出する"""
|
||||
await asyncio.sleep(600) # 10分待つ
|
||||
if len(voice_channel.members) == 0:
|
||||
await self.bot.voice_channel_manager.set_vc_status(self.bot, None)
|
||||
|
||||
def start_debug_mode_check(self):
|
||||
"""デバッグモードでランダムにVCに参加する処理を開始"""
|
||||
if self.independence_timers:
|
||||
for timer in self.independence_timers:
|
||||
timer.cancel()
|
||||
self.independence_timers = []
|
||||
|
||||
# 即時にVCに参加する
|
||||
self.bot.loop.create_task(self.check_independence_mode())
|
||||
|
||||
async def generate_greeting(self, username):
|
||||
"""ユーザーに対する挨拶メッセージを生成する"""
|
||||
self.log(f"{username}が会話に参加しました。")
|
||||
self.input_list.append({"role": "user", "content": f"{username}が会話に参加しました。挨拶メッセージを生成してください。"})
|
||||
|
||||
response = openai.ChatCompletion.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=self.input_list
|
||||
)
|
||||
|
||||
# 応答の最初の選択肢を取得し、メッセージ内容を取り出す
|
||||
greeting = response.choices[0].message['content']
|
||||
self.logs.append(f"{self.independence_character_name}: {greeting}")
|
||||
|
||||
self.input_list.append({"role": "assistant", "content": greeting})
|
||||
self.log(greeting)
|
||||
|
||||
logging.info("最新の3件の会話ログ:")
|
||||
for log_entry in self.input_list[-3:]:
|
||||
logging.info(log_entry)
|
||||
|
||||
return greeting
|
||||
|
||||
def log(self, message):
|
||||
"""ログメッセージをファイルに出力する"""
|
||||
with open(self.log_file_path, "a", encoding="utf-8") as log_file:
|
||||
log_file.write(f"{datetime.now()}: {message}\n")
|
101
app/classes/MessageReader.py
Normal file
101
app/classes/MessageReader.py
Normal file
@ -0,0 +1,101 @@
|
||||
import uuid
|
||||
import discord
|
||||
import os
|
||||
import re
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
class MessageReader:
|
||||
# 半角カタカナと数字と全角へのマッピング
|
||||
half_width = "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンァィゥェォッャュョー"
|
||||
full_width = "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンアイウエオッヤユヨー"
|
||||
translation_table = str.maketrans(half_width, full_width)
|
||||
|
||||
# 濁点・半濁点を含む半角カタカナの変換マッピング
|
||||
dakuten_map = {
|
||||
'ガ': 'ガ', 'ギ': 'ギ', 'グ': 'グ', 'ゲ': 'ゲ', 'ゴ': 'ゴ',
|
||||
'ザ': 'ザ', 'ジ': 'ジ', 'ズ': 'ズ', 'ゼ': 'ゼ', 'ゾ': 'ゾ',
|
||||
'ダ': 'ダ', 'ヂ': 'ヂ', 'ヅ': 'ヅ', 'デ': 'デ', 'ド': 'ド',
|
||||
'バ': 'バ', 'ビ': 'ビ', 'ブ': 'ブ', 'ベ': 'ベ', 'ボ': 'ボ',
|
||||
'パ': 'パ', 'ピ': 'ピ', 'プ': 'プ', 'ペ': 'ペ', 'ポ': 'ポ'
|
||||
}
|
||||
|
||||
def __init__(self, voice_synthesizer, bot):
|
||||
self.voice_synthesizer = voice_synthesizer
|
||||
self.queue = asyncio.Queue()
|
||||
self.is_reading = False
|
||||
self.bot = bot
|
||||
|
||||
async def read_message(self, message, word_dictionary, ffmpeg_executable_path, volume=1.0):
|
||||
"""メッセージを読み上げるための処理を開始する"""
|
||||
await self.queue.put((message, word_dictionary, ffmpeg_executable_path, volume))
|
||||
if not self.is_reading:
|
||||
await self._process_queue()
|
||||
|
||||
async def _process_queue(self):
|
||||
"""メッセージキューを処理する"""
|
||||
self.is_reading = True
|
||||
while not self.queue.empty():
|
||||
message, word_dictionary, ffmpeg_executable_path, volume = await self.queue.get()
|
||||
try:
|
||||
await self._synthesize_and_play(message, word_dictionary, ffmpeg_executable_path, volume)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to read message: {e}")
|
||||
self.is_reading = False
|
||||
|
||||
def _convert_to_full_width(self, text):
|
||||
"""テキスト内の半角カタカナを全角カタカナに変換する"""
|
||||
for half, full in self.dakuten_map.items():
|
||||
text = text.replace(half, full)
|
||||
text = text.translate(self.translation_table)
|
||||
return text
|
||||
|
||||
async def _synthesize_and_play(self, message, word_dictionary, ffmpeg_executable_path, volume):
|
||||
"""メッセージの音声合成を行い、再生する"""
|
||||
if not isinstance(message, str):
|
||||
if self.bot.is_name_reading:
|
||||
message.content = f'{message.author.display_name}さん、{message.content}'
|
||||
|
||||
for user in message.mentions:
|
||||
message.content = message.content.replace(f'<@{user.id}>', user.display_name)
|
||||
message.content = message.content.replace(f'<@!{user.id}>', user.display_name)
|
||||
|
||||
for role in message.role_mentions:
|
||||
message.content = message.content.replace(f'<@&{role.id}>', role.name)
|
||||
|
||||
for channel in message.channel_mentions:
|
||||
message.content = message.content.replace(f'<#{channel.id}>', f'#{channel.name}')
|
||||
|
||||
message.content = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\\-]+', 'URL省略', message.content)
|
||||
|
||||
message.content = re.sub(r'<:[a-zA-Z0-9_]+:[0-9]+>', '', message.content)
|
||||
|
||||
message.content = self._convert_to_full_width(message.content)
|
||||
|
||||
message.content = message.content.replace('\n', ' ')
|
||||
|
||||
voice_client = message.guild.voice_client
|
||||
play_message = message.content
|
||||
id = message.id
|
||||
else:
|
||||
voice_client = self.bot.independence_chat.voice_client
|
||||
play_message = message
|
||||
id = str(uuid.uuid4())
|
||||
|
||||
if voice_client and voice_client.is_connected():
|
||||
script = word_dictionary.convert_text(play_message)
|
||||
if len(script) > 140:
|
||||
script = script[:140]
|
||||
|
||||
output_path = f"{id}.wav"
|
||||
output_path = await self.voice_synthesizer.synthesize(script, output_path=output_path)
|
||||
|
||||
if not os.path.exists(output_path):
|
||||
logging.error(f"Expected audio file not found: {output_path}")
|
||||
return
|
||||
|
||||
audio_source = discord.FFmpegPCMAudio(executable=ffmpeg_executable_path, source=output_path)
|
||||
volume_adjusted_source = discord.PCMVolumeTransformer(audio_source, volume=volume)
|
||||
voice_client.play(volume_adjusted_source, after=lambda e: os.remove(output_path))
|
||||
while voice_client.is_playing():
|
||||
await asyncio.sleep(1)
|
28
app/classes/VoiceChannelManager.py
Normal file
28
app/classes/VoiceChannelManager.py
Normal file
@ -0,0 +1,28 @@
|
||||
import logging
|
||||
|
||||
class VoiceChannelManager:
|
||||
async def join_voice_channel(self, channel):
|
||||
"""ボイスチャンネルに接続する"""
|
||||
try:
|
||||
await channel.connect()
|
||||
except Exception as e:
|
||||
logging.info(f"Failed to join voice channel: {e}")
|
||||
raise
|
||||
|
||||
async def leave_voice_channel(self, guild, bot=None):
|
||||
"""ボイスチャンネルから切断する"""
|
||||
try:
|
||||
if guild.voice_client is not None:
|
||||
await guild.voice_client.disconnect()
|
||||
if bot is not None:
|
||||
bot.volume = 1.0
|
||||
bot.reset_emotion()
|
||||
except Exception as e:
|
||||
logging.info(f"Failed to leave voice channel: {e}")
|
||||
raise
|
||||
|
||||
def get_connected_channel(self, guild):
|
||||
"""ボットが接続しているボイスチャンネルを取得する"""
|
||||
if guild.voice_client and guild.voice_client.is_connected():
|
||||
return guild.voice_client.channel
|
||||
return None
|
104
app/classes/VoiceSynthesizer.py
Normal file
104
app/classes/VoiceSynthesizer.py
Normal file
@ -0,0 +1,104 @@
|
||||
import os
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
class VoiceSynthesizer:
|
||||
def __init__(self, voicepeak_path, voicevox_url, narrator):
|
||||
self.voicepeak_path = voicepeak_path
|
||||
self.voicevox_url = voicevox_url
|
||||
self.narrator = narrator
|
||||
self.pitch = 50
|
||||
self.emotion_happy = 0
|
||||
self.emotion_sad = 0
|
||||
self.emotion_angry = 0
|
||||
self.emotion_fun = 0
|
||||
|
||||
def set_emotion(self, happy, sad, angry, fun):
|
||||
"""ボットの感情を設定する"""
|
||||
self.emotion_happy = happy
|
||||
self.emotion_sad = sad
|
||||
self.emotion_angry = angry
|
||||
self.emotion_fun = fun
|
||||
|
||||
def set_pitch(self, pitch):
|
||||
"""ボットの音程を設定する"""
|
||||
self.pitch = pitch
|
||||
|
||||
async def synthesize(self, script, output_path="output.wav"):
|
||||
"""スクリプトの音声合成を行う"""
|
||||
if self.voicepeak_path:
|
||||
return self._synthesize_with_voicepeak(script, output_path)
|
||||
else:
|
||||
return await self._synthesize_with_voicevox(script, output_path)
|
||||
|
||||
def _synthesize_with_voicepeak(self, script, output_path):
|
||||
args = {
|
||||
'voicepeak_path': self.voicepeak_path,
|
||||
'script': script,
|
||||
'narrator': self.narrator,
|
||||
'output_path': output_path,
|
||||
'emotions': f"happy={self.emotion_happy},sad={self.emotion_sad},angry={self.emotion_angry},fun={self.emotion_fun}",
|
||||
'pitch': self.pitch,
|
||||
}
|
||||
command_args = self._build_command_args(args)
|
||||
|
||||
try:
|
||||
subprocess.run(command_args, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_message = self._format_error_message(e)
|
||||
logging.error(error_message)
|
||||
raise RuntimeError(error_message) from e
|
||||
|
||||
if not os.path.exists(output_path):
|
||||
error_message = f"Expected audio file not found: {output_path}"
|
||||
logging.error(error_message)
|
||||
raise FileNotFoundError(error_message)
|
||||
|
||||
return output_path
|
||||
|
||||
async def _synthesize_with_voicevox(self, script, output_path):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
url_audio_query = f'{self.voicevox_url}/audio_query?speaker={self.narrator}&text={script}'
|
||||
|
||||
async with session.post(url_audio_query) as response:
|
||||
if response.status != 200:
|
||||
logging.error(f"Error in audio_query: {await response.text()}")
|
||||
return None
|
||||
query = await response.json()
|
||||
|
||||
url_synthesis = f'{self.voicevox_url}/synthesis'
|
||||
params_synthesis = {
|
||||
"speaker": self.narrator
|
||||
}
|
||||
headers = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
async with session.post(url_synthesis, headers=headers, params=params_synthesis, json=query) as response:
|
||||
if response.status != 200:
|
||||
logging.error(f"Error in synthesis: {await response.text()}")
|
||||
return None
|
||||
audio_data = await response.read()
|
||||
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(audio_data)
|
||||
|
||||
return output_path
|
||||
|
||||
def _build_command_args(self, args):
|
||||
"""音声合成コマンドの引数を構築する"""
|
||||
return [
|
||||
args['voicepeak_path'],
|
||||
"-s", args['script'],
|
||||
"-n", args['narrator'],
|
||||
"-o", args['output_path'],
|
||||
"-e", args['emotions'],
|
||||
"--pitch", str(args['pitch']),
|
||||
]
|
||||
|
||||
def _format_error_message(self, e):
|
||||
"""エラーメッセージをフォーマットする"""
|
||||
stderr_output = e.stderr.decode('utf-8') if e.stderr else 'No stderr output'
|
||||
return f"Voicepeak failed with error code {e.returncode}: {stderr_output}"
|
37
app/classes/WordDictionary.py
Normal file
37
app/classes/WordDictionary.py
Normal file
@ -0,0 +1,37 @@
|
||||
import logging
|
||||
from utils.file_utils import read_json, write_json
|
||||
|
||||
|
||||
class WordDictionary:
|
||||
def __init__(self, dictionary=None, file_path='config/word_dictionary.json'):
|
||||
"""WordDictionaryクラスの初期化を行う"""
|
||||
self.dictionary = dictionary if dictionary else {}
|
||||
self.file_path = file_path
|
||||
self.load_from_json()
|
||||
|
||||
def register_word(self, word, reading):
|
||||
"""単語を辞書に登録する"""
|
||||
self.dictionary[word] = reading
|
||||
self.save_to_json()
|
||||
|
||||
def convert_text(self, text):
|
||||
"""テキスト内の単語を辞書を使って変換する"""
|
||||
for word in sorted(self.dictionary, key=len, reverse=True):
|
||||
reading = self.dictionary[word]
|
||||
text = text.replace(word, reading)
|
||||
return text
|
||||
|
||||
def save_to_json(self):
|
||||
"""辞書をJSONファイルに保存する"""
|
||||
write_json(self.dictionary, self.file_path)
|
||||
|
||||
def load_from_json(self):
|
||||
"""JSONファイルから辞書を読み込む"""
|
||||
try:
|
||||
self.dictionary = read_json(self.file_path)
|
||||
except FileNotFoundError:
|
||||
write_json({}, self.file_path)
|
||||
self.dictionary = {}
|
||||
except Exception as e:
|
||||
logging.info(f"Failed to load dictionary from JSON: {e}")
|
||||
raise Exception(f"Failed to load dictionary from JSON: {e}")
|
10
app/config.py
Normal file
10
app/config.py
Normal file
@ -0,0 +1,10 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
class Config:
|
||||
@staticmethod
|
||||
def load_config():
|
||||
env = os.getenv('ENV', 'dev')
|
||||
config_file = f"config/config.{env}.json"
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
18
app/utils/file_utils.py
Normal file
18
app/utils/file_utils.py
Normal file
@ -0,0 +1,18 @@
|
||||
import json
|
||||
|
||||
|
||||
def read_json(file_path):
|
||||
"""指定されたファイルパスからJSONデータを読み込む"""
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def write_json(data, file_path):
|
||||
"""データを指定されたファイルパスにJSON形式で書き込む"""
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
def read_text(file_path):
|
||||
"""指定されたファイルパスからテキストを読み込んで文字列で返す"""
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
21
app/web_server.py
Normal file
21
app/web_server.py
Normal file
@ -0,0 +1,21 @@
|
||||
from flask import Flask, request, jsonify
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/debug', methods=['POST'])
|
||||
def toggle_debug():
|
||||
data = request.json
|
||||
if 'enable' not in data:
|
||||
return jsonify({'error': 'Invalid request'}), 400
|
||||
|
||||
if data['enable']:
|
||||
open('debug_mode.txt', 'w').close()
|
||||
else:
|
||||
if os.path.exists('debug_mode.txt'):
|
||||
os.remove('debug_mode.txt')
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000)
|
12
config/_config.dev.json
Normal file
12
config/_config.dev.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"prefix": "!",
|
||||
"voicevox_url": "http://voicevox_engine:50021",
|
||||
"discord_token": "",
|
||||
"openai_api_key": "",
|
||||
"default_navigator": "8",
|
||||
"ffmpeg_executable_path": "/usr/bin/ffmpeg",
|
||||
"voicepeak_executable_path": "",
|
||||
"independence_character_name": "",
|
||||
"independence_channel_ids": [123456789012345678, 234567890123456789],
|
||||
"bot_admin_user_id": 123456789012345678
|
||||
}
|
11
config/_config.prod.json
Normal file
11
config/_config.prod.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"prefix": "!",
|
||||
"discord_token": "",
|
||||
"openai_api_key": "",
|
||||
"default_navigator": "",
|
||||
"ffmpeg_executable_path": "",
|
||||
"voicepeak_executable_path": "",
|
||||
"independence_character_name": "",
|
||||
"independence_channel_ids": [123456789012345678, 234567890123456789],
|
||||
"bot_admin_user_id": 123456789012345678
|
||||
}
|
25
docker-compose.yml
Normal file
25
docker-compose.yml
Normal file
@ -0,0 +1,25 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
voicevox_engine:
|
||||
image: voicevox/voicevox_engine:cpu-ubuntu20.04-latest
|
||||
ports:
|
||||
- "50021:50021"
|
||||
|
||||
bot:
|
||||
build:
|
||||
context: .
|
||||
environment:
|
||||
- ENV=dev
|
||||
depends_on:
|
||||
- voicevox_engine
|
||||
ports:
|
||||
- "5000:5000"
|
||||
|
||||
web:
|
||||
build:
|
||||
context: ./web
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- bot
|
18
entrypoint.sh
Normal file
18
entrypoint.sh
Normal file
@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# エントリーポイントスクリプト
|
||||
# Voicevoxエンジンの起動
|
||||
run_voicevox &
|
||||
|
||||
# ボットの実行
|
||||
python /app/app/bot.py &
|
||||
|
||||
# 子プロセス(Voicevoxエンジンとボット)のPIDを取得
|
||||
VOICEVOX_PID=$!
|
||||
BOT_PID=$!
|
||||
|
||||
# シグナルハンドラを設定
|
||||
trap "echo 'シャットダウン中...' && kill -SIGTERM $VOICEVOX_PID $BOT_PID && wait $VOICEVOX_PID $BOT_PID" SIGINT SIGTERM
|
||||
|
||||
# 子プロセスが終了するのを待つ
|
||||
wait $VOICEVOX_PID $BOT_PID
|
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
pygame
|
||||
py-cord
|
||||
PyNaCl
|
||||
openai==0.28
|
||||
requests
|
||||
discord.py
|
||||
aiohttp
|
||||
flask
|
1
web/.gitignore
vendored
Normal file
1
web/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
33
web/Dockerfile
Normal file
33
web/Dockerfile
Normal file
@ -0,0 +1,33 @@
|
||||
# ベースイメージとしてnodeを使用
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json tsconfig.json ./
|
||||
COPY src ./src
|
||||
COPY public ./public
|
||||
|
||||
RUN npm ci && \
|
||||
npm run build && \
|
||||
npm prune --production
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 hono
|
||||
|
||||
COPY --from=builder /app/node_modules /app/node_modules
|
||||
COPY --from=builder /app/dist /app/dist
|
||||
COPY --from=builder /app/public /app/public
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
|
||||
RUN chown -R hono:nodejs /app
|
||||
|
||||
USER hono
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "/app/dist/index.js"]
|
8
web/README.md
Normal file
8
web/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
```
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```
|
||||
open http://localhost:3000
|
||||
```
|
659
web/package-lock.json
generated
Normal file
659
web/package-lock.json
generated
Normal file
@ -0,0 +1,659 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "web",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.11.1",
|
||||
"axios": "^1.4.0",
|
||||
"hono": "^4.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.17",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^4.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.11.1.tgz",
|
||||
"integrity": "sha512-GW1Iomhmm1o4Z+X57xGby8A35Cu9UZLL7pSMdqDBkD99U5cywff8F+8hLk5aBTzNubnsFAvWQ/fZjNwPsEn9lA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.12.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
|
||||
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.6.8",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
|
||||
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.7.5",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz",
|
||||
"integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.3.7.tgz",
|
||||
"integrity": "sha512-GXlsGnCAwGosu+COwYyYC8MwOY2L6Ihg9V1znYdMD8DHCJl+13Nk4o8dsBYJpae4oujjw24jBaITuYWVq2+V8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.10.4",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.10.4.tgz",
|
||||
"integrity": "sha512-Gtg9qnZWNqC/OtcgiXfoAUdAKx3/cgKOYvEocAsv+m21MV/eKpV/WUjRXe6/sDCaGBl2/v8S6v29BpUnGMCX5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.20.2",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
21
web/package.json
Normal file
21
web/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsx watch src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.11.1",
|
||||
"hono": "^4.3.7",
|
||||
"axios": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.17",
|
||||
"typescript": "^4.5.2",
|
||||
"tsx": "^4.7.1"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
78
web/public/index.html
Normal file
78
web/public/index.html
Normal file
@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Discord Bot デバッグモード</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
button {
|
||||
margin: 10px;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.enable {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
.disable {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Discord Bot デバッグモード</h1>
|
||||
<button class="enable" onclick="toggleDebug(true)">デバッグモードを有効にする</button>
|
||||
<button class="disable" onclick="toggleDebug(false)">デバッグモードを無効にする</button>
|
||||
<p id="status"></p>
|
||||
</div>
|
||||
<script>
|
||||
function toggleDebug(enable) {
|
||||
fetch('/debug', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ enable })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const status = document.getElementById('status');
|
||||
if (data.success) {
|
||||
status.textContent = `デバッグモードが${enable ? '有効' : '無効'}になりました。`;
|
||||
status.style.color = enable ? 'green' : 'red';
|
||||
} else {
|
||||
status.textContent = `デバッグモードの切り替えに失敗しました。`;
|
||||
status.style.color = 'red';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
const status = document.getElementById('status');
|
||||
status.textContent = `エラー: ${error}`;
|
||||
status.style.color = 'red';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
37
web/src/index.ts
Normal file
37
web/src/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { Hono } from 'hono'
|
||||
import { serve } from '@hono/node-server'
|
||||
import { serveStatic } from '@hono/node-server/serve-static'
|
||||
import axios from 'axios'
|
||||
import { resolve } from 'path'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
const DISCORD_BOT_API_URL = 'http://bot:5000/debug'
|
||||
|
||||
app.use('/*', serveStatic({ root: './public' }))
|
||||
|
||||
app.post('/debug', async (c) => {
|
||||
const body = await c.req.json()
|
||||
if (!('enable' in body)) {
|
||||
return c.json({ error: 'Invalid request' }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(DISCORD_BOT_API_URL, body)
|
||||
if (response.status === 200) {
|
||||
return c.json({ success: true })
|
||||
} else {
|
||||
return c.json({ error: 'Failed to toggle debug mode' }, 500)
|
||||
}
|
||||
} catch (error) {
|
||||
return c.json({ error: 'Failed to toggle debug mode' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
const port = 3000
|
||||
console.log(`Server is running on port ${port}`)
|
||||
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port
|
||||
})
|
14
web/tsconfig.json
Normal file
14
web/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user