diff --git a/.gitignore b/.gitignore index 89d30ec..f5c0150 100644 --- a/.gitignore +++ b/.gitignore @@ -162,4 +162,6 @@ cython_debug/ config/ db/ -test.py \ No newline at end of file +test.py +data/ +export/ \ No newline at end of file diff --git a/Dockerfile_auth b/Dockerfile_auth new file mode 100644 index 0000000..f2fbcc6 --- /dev/null +++ b/Dockerfile_auth @@ -0,0 +1,22 @@ +FROM python:3 +USER root + +RUN mkdir /dislocker + +RUN apt-get update +RUN apt-get -y install locales && \ + localedef -f UTF-8 -i ja_JP ja_JP.UTF-8 +ENV LANG ja_JP.UTF-8 +ENV LANGUAGE ja_JP:ja +ENV LC_ALL ja_JP.UTF-8 +ENV TZ JST-9 +ENV TERM xterm + +RUN apt-get install -y nano less +RUN pip install --upgrade pip +RUN pip install --upgrade setuptools + +RUN python -m pip install flask psycopg2-binary requests + +WORKDIR /dislocker +CMD python -u ./dislocker_auth.py diff --git a/Dockerfile_bot b/Dockerfile_bot new file mode 100644 index 0000000..e07f1af --- /dev/null +++ b/Dockerfile_bot @@ -0,0 +1,22 @@ +FROM python:3 +USER root + +RUN mkdir /dislocker + +RUN apt-get update +RUN apt-get -y install locales && \ + localedef -f UTF-8 -i ja_JP ja_JP.UTF-8 +ENV LANG ja_JP.UTF-8 +ENV LANGUAGE ja_JP:ja +ENV LC_ALL ja_JP.UTF-8 +ENV TZ JST-9 +ENV TERM xterm + +RUN apt-get install -y nano less +RUN pip install --upgrade pip +RUN pip install --upgrade setuptools + +RUN python -m pip install discord.py psycopg2-binary requests openpyxl + +WORKDIR /dislocker +CMD python -u ./dislocker.py diff --git a/README.md b/README.md index 09c0383..11725cf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,9 @@ # Dislocker - +課題研究用リポジトリ +# 環境構築 +## サーバー側 +基本的にはDocker上での起動を推奨します。 +このリポジトリをクローンし、`docker compose up -d`で起動すると一式のコンテナが起動します。 +データベースだけを起動したい場合は、ファイルに`compose_db.yml`を指定してください。 +## クライアント側 +pyinstallerでビルドしたものを、起動してください。 diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..f498f9c --- /dev/null +++ b/compose.yml @@ -0,0 +1,45 @@ +services: + bot: + build: + context: "./" + dockerfile: "Dockerfile_bot" + restart: always + environment: + - TZ=Asia/Tokyo + depends_on: + - db + volumes: + - ./:/dislocker + networks: + - dislocker_network + + auth: + build: + context: "./" + dockerfile: "Dockerfile_auth" + restart: always + environment: + - TZ=Asia/Tokyo + volumes: + - ./:/dislocker + ports: + - 12244:5000 + networks: + - dislocker_network + + db: + image: postgres:alpine3.20 + restart: always + environment: + - TZ=Asia/Tokyo + volumes: + - ./data/db:/var/lib/postgresql/data + ports: + - 12245:5432 + env_file: + - ./data/.env + networks: + - dislocker_network + +networks: + dislocker_network: diff --git a/compose_db.yml b/compose_db.yml new file mode 100644 index 0000000..7cff067 --- /dev/null +++ b/compose_db.yml @@ -0,0 +1,18 @@ +services: + db: + image: postgres:alpine3.20 + restart: always + environment: + - TZ=Asia/Tokyo + - POSTGRES_DB=dislocker + - POSTGRES_USER=dislocker + - POSTGRES_PASSWORD=Password + volumes: + - ./data/db:/var/lib/postgresql/data + ports: + - 12245:5432 + networks: + - dislocker_network + +networks: + dislocker_network: diff --git a/dislocker.py b/dislocker.py index 465e3ba..7da40da 100644 --- a/dislocker.py +++ b/dislocker.py @@ -1,14 +1,116 @@ import json import discord +from discord import Interaction, TextStyle, app_commands +from discord.ui import TextInput, View, Modal import os import psycopg2 +from psycopg2 import sql import hashlib import string import random +from datetime import datetime, timedelta +from openpyxl import Workbook +import threading +import time + +class DL(): + def __init__(self): + self.config_dir_path = "./config/" + self.export_dir_path = "./export/" + self.server_config_path = self.config_dir_path + "server.json" + try: + if not os.path.isdir(self.config_dir_path): + print("config ディレクトリが見つかりません... 作成します。") + os.mkdir(self.config_dir_path) + + if not os.path.isfile(self.server_config_path): + print("config ファイルが見つかりません... 作成します。") + self.server_config = { + "db": { + "host": "localhost", + "port": "5432", + "db_name": "dislocker", + "username": "user", + "password": "password" + }, + "bot": { + "token": "TYPE HERE BOTS TOKEN KEY", + "activity": { + "name": "Dislocker", + "details": "ロック中...", + "type": "playing", + "state": "ロック中..." + }, + "log_channel_id" : "TYPE HERE CHANNEL ID (YOU MUST USE INT !!!!)", + "config_channel_id": "TYPE HERE CHANNEL ID (YOU MUST USE INT !!!!)", + "config_public_channel_id": "TYPE HERE CHANNEL ID (YOU MUST USE INT !!!!)", + "monitor": { + "search_frequency": 1, + "allowable_time": 180, + "fstop_time": "21:00:00" + } + } + } + + with open(self.server_config_path, "w", encoding="utf-8") as w: + json.dump(self.server_config, w, indent=4, ensure_ascii=False) + + + elif os.path.isfile(self.server_config_path): + with open(self.server_config_path, "r", encoding="utf-8") as r: + self.server_config = json.load(r) + print("config ファイルを読み込みました。") + + + if not os.path.isdir(self.export_dir_path): + print("export ディレクトリが見つかりません... 作成します。") + os.mkdir(self.export_dir_path) + + if type(self.server_config["bot"]["log_channel_id"]) is not int or type(self.server_config["bot"]["config_channel_id"]) is not int: + print("config ファイル内でチャンネルIDがint型で記述されていません。int型で記述して、起動してください。") + self.init_result = "not_int" + else: + self.db = psycopg2.connect(f"host={self.server_config['db']['host']} dbname={self.server_config['db']['db_name']} port={self.server_config['db']['port']} user={self.server_config['db']['username']} password={self.server_config['db']['password']}") + cursor = self.db.cursor() + + self.pc_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + cursor.execute("SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'club_member')") + find_club_member_table = cursor.fetchall() + print(find_club_member_table) + if find_club_member_table[0][0] == False: + cursor.execute("CREATE TABLE club_member (id SERIAL NOT NULL, name VARCHAR(128) NOT NULL, discord_username VARCHAR(128) NOT NULL, discord_userid VARCHAR(18) NOT NULL, PRIMARY KEY (id))") + self.db.commit() + + cursor.execute("SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'pc_list')") + find_pc_list_table = cursor.fetchall() + print(find_pc_list_table) + if find_pc_list_table[0][0] == False: + cursor.execute("CREATE TABLE pc_list (pc_number INTEGER NOT NULL, using_user_id INTEGER, password_hash VARCHAR(32), PRIMARY KEY (pc_number), FOREIGN KEY (using_user_id) REFERENCES club_member(id))") + for i in range(10): + print(i) + cursor.execute("INSERT INTO pc_list (pc_number) VALUES (%s)", (i + 1,)) + self.db.commit() + + cursor.execute("SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'pc_usage_history')") + find_pc_usage_history_table = cursor.fetchall() + print(find_pc_usage_history_table) + if find_pc_usage_history_table[0][0] == False: + cursor.execute("CREATE TABLE pc_usage_history (id SERIAL NOT NULL, member_id INTEGER NOT NULL, pc_number INTEGER NOT NULL, device_number INTEGER NOT NULL, start_use_time TIMESTAMP NOT NULL, end_use_time TIMESTAMP, use_detail VARCHAR(128), bot_about VARCHAR(128), PRIMARY KEY (id), FOREIGN KEY (member_id) REFERENCES club_member(id), FOREIGN KEY (pc_number) REFERENCES pc_list(pc_number))") + self.db.commit() + + cursor.close() + self.init_result = "ok" + + except (Exception) as error: + print("初回処理でエラーが発生しました。\nエラー内容\n" + str(error)) + self.init_result = "error" + + finally: + pass + class Bot(discord.Client): - def db_connect(self, host, db, port, user, password): - self.db = psycopg2.connect(f"host={host} dbname={db} port={port} user={user} password={password}") def password_generate(self, length): numbers = string.digits # (1) @@ -19,106 +121,331 @@ class Bot(discord.Client): hashed = hashlib.md5(source.encode()) return hashed.hexdigest() - def register(self, **kwrags): - discord_user_id = str(kwrags["user_id"]) - pc_number = int(kwrags["pc_number"]) - device_number = int(kwrags["device_number"]) - if "detail" in kwrags: - detail = str(kwrags["detail"]) - else: - detail = None - #パスワード生成、ハッシュ化 - password = self.password_generate(4) - password_hash = self.hash_genarate(password) - print("password generated") - #メンバーリストと送信者を照合 - cursor = self.db.cursor() - cursor.execute("SELECT * FROM club_member WHERE discord_userid = %s", (discord_user_id,)) - user_record = cursor.fetchall() - #ユーザーデータがなかったら(未登録の場合) - if not user_record: - result = {"result": "user_data_not_found"} - #ユーザーデータが見つかった場合(登録済みの場合) - else: - print("found user data") - cursor.execute("SELECT * FROM pc_usage_history WHERE member_id=%s ORDER BY id DESC LIMIT 1", (user_record[0][0],)) - pc_usage_history_record = cursor.fetchall() - if pc_usage_history_record: - print("used") - if pc_usage_history_record[0][5] == None: - result = {"result": "pc_already_in_use_by_you", "pc_number": str(pc_usage_history_record[0][2]), "device_number": str(pc_usage_history_record[0][3]), "start_time": str(pc_usage_history_record[0][4]), "detail": str(pc_usage_history_record[0][6])} - else: - cursor.execute("SELECT * FROM pc_list WHERE pc_number=%s", (pc_number,)) - pc_list_record = cursor.fetchall() - if not pc_list_record[0][1] == None: - result = {"result": "pc_already_in_use_by_other"} + def register(self, **kwargs): + try: + user_info = { + "id": str(kwargs["user_id"]), + "name": str(kwargs["name"]), + "display_name": str(kwargs["display_name"]), + "pc_number": int(kwargs["pc_number"]), + "device_number": int(kwargs["device_number"]), + "detail": None + } + if "detail" in kwargs: + user_info["detail"] = str(kwargs["detail"]) + else: + pass + #パスワード生成、ハッシュ化 + password = self.password_generate(4) + password_hash = self.hash_genarate(password) + print("password generated") + #メンバーリストと送信者を照合 + cursor = dislocker.db.cursor() + cursor.execute("SELECT * FROM club_member WHERE discord_userid = %s", (user_info["id"],)) + user_record = cursor.fetchall() + #ユーザーデータがなかったら(未登録の場合) + if not user_record: + result = {"result": "user_data_not_found"} + #ユーザーデータが見つかった場合(登録済みの場合) + else: + print("found user data") + cursor.execute("SELECT * FROM pc_usage_history WHERE member_id=%s ORDER BY id DESC LIMIT 1", (user_record[0][0],)) + pc_usage_history_record = cursor.fetchall() + if pc_usage_history_record: + print("used") + if pc_usage_history_record[0][5] == None: + result = {"result": "pc_already_in_use_by_you", "pc_number": str(pc_usage_history_record[0][2]), "device_number": str(pc_usage_history_record[0][3]), "start_time": str(pc_usage_history_record[0][4]), "detail": str(pc_usage_history_record[0][6])} else: - if detail == None: - cursor.execute("INSERT INTO pc_usage_history (member_id, pc_number, device_number, start_use_time) VALUES (%s, %s, %s, current_timestamp)", (user_record[0][0], pc_number, device_number)) + cursor.execute("SELECT * FROM pc_list WHERE pc_number=%s", (user_info["pc_number"],)) + pc_list_record = cursor.fetchall() + if not pc_list_record[0][1] == None: + result = {"result": "pc_already_in_use_by_other"} + else: + if user_info["detail"] == None: + cursor.execute("INSERT INTO pc_usage_history (member_id, pc_number, device_number, start_use_time) VALUES (%s, %s, %s, clock_timestamp())", (user_record[0][0], user_info["pc_number"], user_info["device_number"])) + #使用用途があるとき + else: + cursor.execute("INSERT INTO pc_usage_history (member_id, pc_number, device_number, start_use_time, use_detail) VALUES (%s, %s, %s, clock_timestamp(), %s)", (user_record[0][0], user_info["pc_number"], user_info["device_number"], user_info["detail"])) + + cursor.execute("UPDATE pc_list SET using_user_id = %s WHERE pc_number = %s", (user_record[0][0], user_info["pc_number"])) + cursor.execute("UPDATE pc_list SET password_hash = %s WHERE pc_number = %s", (password_hash, user_info["pc_number"])) + dislocker.db.commit() + result = {"result": "ok", "password": str(password), "name": str(user_record[0][1])} + else: + print("unused") + cursor.execute("SELECT * FROM pc_list WHERE pc_number=%s", (user_info["pc_number"],)) + pc_list_record = cursor.fetchall() + if pc_list_record: + if not pc_list_record[0][1] == None: + result = {"result": "pc_already_in_use_by_other"} + else: + if user_info["detail"] == None: + cursor.execute("INSERT INTO pc_usage_history (member_id, pc_number, device_number, start_use_time) VALUES (%s, %s, %s, clock_timestamp())", (user_record[0][0], user_info["pc_number"], user_info["device_number"])) + #使用用途があるとき + else: + cursor.execute("INSERT INTO pc_usage_history (member_id, pc_number, device_number, start_use_time, use_detail) VALUES (%s, %s, %s, clock_timestamp(), %s)", (user_record[0][0], user_info["pc_number"], user_info["device_number"], user_info["detail"])) + + cursor.execute("UPDATE pc_list SET using_user_id = %s WHERE pc_number = %s", (user_record[0][0], user_info["pc_number"])) + cursor.execute("UPDATE pc_list SET password_hash = %s WHERE pc_number = %s", (password_hash, user_info["pc_number"])) + dislocker.db.commit() + result = {"result": "ok", "password": str(password), "name": str(user_record[0][1])} + else: + if user_info["detail"] == None: + cursor.execute("INSERT INTO pc_usage_history (member_id, pc_number, device_number, start_use_time) VALUES (%s, %s, %s, clock_timestamp())", (user_record[0][0], user_info["pc_number"], user_info["device_number"])) #使用用途があるとき else: - cursor.execute("INSERT INTO pc_usage_history (member_id, pc_number, device_number, start_use_time, use_detail) VALUES (%s, %s, %s, current_timestamp, %s)", (user_record[0][0], pc_number, device_number, detail)) + cursor.execute("INSERT INTO pc_usage_history (member_id, pc_number, device_number, start_use_time, use_detail) VALUES (%s, %s, %s, clock_timestamp(), %s)", (user_record[0][0], user_info["pc_number"], user_info["device_number"], user_info["detail"])) - cursor.execute("UPDATE pc_list SET using_user_id = %s WHERE pc_number = %s", (user_record[0][0], pc_number)) - cursor.execute("UPDATE pc_list SET password_hash = %s WHERE pc_number = %s", (password_hash, pc_number)) - self.db.commit() + cursor.execute("UPDATE pc_list SET using_user_id = %s WHERE pc_number = %s", (user_record[0][0], user_info["pc_number"])) + cursor.execute("UPDATE pc_list SET password_hash = %s WHERE pc_number = %s", (password_hash, user_info["pc_number"])) + dislocker.db.commit() result = {"result": "ok", "password": str(password), "name": str(user_record[0][1])} + + + except Exception as error: + print("登録処理中にエラーが発生しました。\nエラー内容") + print(str(error.__class__.__name__)) + print(str(error.args)) + print(str(error)) + result = {"result": "error"} + + finally: + cursor.close() + return result + + def stop(self, **kwargs): + try: + discord_user_id = str(kwargs["user_id"]) + if "bot_about" in kwargs: + bot_about = kwargs["bot_about"] else: - cursor.execute("SELECT * FROM pc_list WHERE pc_number=%s", (pc_number,)) + bot_about = None + cursor = dislocker.db.cursor() + cursor.execute("SELECT * FROM club_member WHERE discord_userid = %s", (discord_user_id,)) + user_record = cursor.fetchall() + if user_record: + cursor.execute("SELECT * FROM pc_usage_history WHERE member_id= %s ORDER BY id DESC LIMIT 1", (user_record[0][0],)) + pc_usage_history_record = cursor.fetchall() + if pc_usage_history_record: + if not pc_usage_history_record[0][5] == None: + result = {"result": "unused"} + else: + if not bot_about == None: + cursor.execute("UPDATE pc_usage_history SET end_use_time = clock_timestamp(), bot_about = %s WHERE id = %s", (bot_about, pc_usage_history_record[0][0])) + else: + cursor.execute("UPDATE pc_usage_history SET end_use_time = clock_timestamp() WHERE id = %s", (pc_usage_history_record[0][0],)) + + cursor.execute("UPDATE pc_list SET using_user_id = NULL, password_hash = NULL WHERE pc_number = %s", (pc_usage_history_record[0][2],)) + dislocker.db.commit() + result = {"result": "ok", "pc_number": str(pc_usage_history_record[0][2]), "name": str(user_record[0][1])} + else: + result = {"result": "unused"} + else: + result = {"result": "user_data_not_found"} + except: + print("停止処理にエラーが発生しました。") + result = {"result": "error"} + + finally: + cursor.close() + return result + + def user_register(self, **kwargs): + try: + discord_user_id = str(kwargs["discord_user_id"]) + discord_user_name = str(kwargs["discord_user_name"]) + name = str(kwargs["name"]) + cursor = dislocker.db.cursor() + cursor.execute("SELECT * FROM club_member WHERE discord_userid = %s", (discord_user_id,)) + user_record = cursor.fetchall() + if not user_record: + cursor.execute("INSERT INTO club_member (name, discord_username, discord_userid) VALUES (%s, %s, %s)", (name, discord_user_name, discord_user_id)) + dislocker.db.commit() + result = {"result": "ok"} + else: + result = {"result": "already_exists"} + + except Exception as error: + print("ユーザー登録中にエラーが発生しました。\nエラー内容") + print(str(error.__class__.__name__)) + print(str(error.args)) + print(str(error)) + result = {"result": "error"} + + finally: + cursor.close() + return result + + def format_datetime(self, value): + if isinstance(value, datetime): + return value.strftime('%Y-%m-%d %H:%M:%S') + return value + + def pc_register(self, **kwargs): + try: + pc_number = int(kwargs["pc_number"]) + cursor = dislocker.db.cursor() + cursor.execute("SELECT * FROM pc_list WHERE pc_number = %s", (pc_number,)) + pc_list = cursor.fetchall() + if not pc_list: + cursor.execute("INSERT INTO pc_list (pc_number) VALUES (%s)", (pc_number,)) + dislocker.db.commit() + result = {"result": "ok"} + else: + result = {"result": "already_exists"} + + except Exception as error: + print("PCの登録中にエラーが発生しました。\nエラー内容") + print(str(error.__class__.__name__)) + print(str(error.args)) + print(str(error)) + result = {"result": "error"} + + finally: + cursor.close() + return result + + def report_export(self, **kwargs): + try: + cursor = dislocker.db.cursor() + csv_file_path = dislocker.export_dir_path + "pc_usage_history.csv" + main_table = "pc_usage_history" + related_table = "club_member" + excel_file_path = dislocker.export_dir_path + "pc_usage_history.xlsx" + + # メインテーブルの列情報を取得(user_idを除く) + cursor.execute(sql.SQL("SELECT * FROM {} LIMIT 0").format(sql.Identifier(main_table))) + main_columns = [desc[0] for desc in cursor.description if desc[0] != 'member_id'] + + # クエリを作成(列名を明確に指定) + query = sql.SQL(""" + SELECT {main_columns}, {related_table}.name + FROM {main_table} + LEFT JOIN {related_table} ON {main_table}.member_id = {related_table}.id + ORDER BY id + """).format( + main_columns=sql.SQL(', ').join([sql.SQL("{}.{}").format(sql.Identifier(main_table), sql.Identifier(col)) for col in main_columns]), + main_table=sql.Identifier(main_table), + related_table=sql.Identifier(related_table) + ) + + cursor.execute(query) + + # 列名を再構成(nameを2番目に配置) + column_names = [main_columns[0], 'name'] + main_columns[1:] + + rows = cursor.fetchall() + + # Excelワークブックを作成 + wb = Workbook() + ws = wb.active + + # 列名を書き込み + ws.append(column_names) + + # データを書き込み + for row in rows: + # nameを2番目に移動 + formatted_row = [self.format_datetime(row[0])] + [row[-1]] + [self.format_datetime(field) if field is not None else '' for field in row[1:-1]] + ws.append(formatted_row) + + # 列幅を自動調整 + for col in ws.columns: + max_length = 0 + column = col[0].column_letter + for cell in col: + try: + if len(str(cell.value)) > max_length: + max_length = len(str(cell.value)) + except: + pass + adjusted_width = (max_length + 2) * 1.2 + ws.column_dimensions[column].width = adjusted_width + + # Excelファイルを保存 + wb.save(excel_file_path) + + print(f"テーブル '{main_table}' の内容を '{excel_file_path}' に出力しました。") + result = {"result": "ok", "file_path": excel_file_path} + + except (Exception, psycopg2.Error) as error: + print("使用履歴のエクスポート時にエラーが発生しました\nエラー内容\n", str(error)) + result = {"result": "export_error"} + + finally: + cursor.close() + return result + + def force_stop(self, **kwargs): + try: + pc_number = kwargs["pc_number"] + if "bot_about" in kwargs: + bot_about = kwargs["bot_about"] + cursor = dislocker.db.cursor() + cursor.execute("SELECT * FROM pc_list WHERE pc_number = %s", (pc_number,)) pc_list_record = cursor.fetchall() if not pc_list_record[0][1] == None: - result = {"result": "pc_already_in_use_by_other"} + cursor.execute("UPDATE pc_list SET using_user_id = NULL WHERE pc_number = %s", (pc_number,)) + + if not pc_list_record[0][2] == None: + cursor.execute("UPDATE pc_list SET password_hash = NULL WHERE pc_number = %s", (pc_number,)) + + cursor.execute("SELECT * FROM pc_usage_history WHERE member_id = %s AND pc_number = %s ORDER BY id DESC LIMIT 1", (pc_list_record[0][1], pc_number)) + pc_usage_history_record = cursor.fetchall() + cursor.execute("UPDATE pc_usage_history SET end_use_time = clock_timestamp(), bot_about = %s WHERE id = %s", (bot_about, pc_usage_history_record[0][0])) + dislocker.db.commit() + result = {"result": "ok"} + else: - if detail == None: - cursor.execute("INSERT INTO pc_usage_history (member_id, pc_number, device_number, start_use_time) VALUES (%s, %s, %s, current_timestamp)", (user_record[0][0], pc_number, device_number)) - #使用用途があるとき - else: - cursor.execute("INSERT INTO pc_usage_history (member_id, pc_number, device_number, start_use_time, use_detail) VALUES (%s, %s, %s, current_timestamp, %s)", (user_record[0][0], pc_number, device_number, detail)) - - cursor.execute("UPDATE pc_list SET using_user_id = %s WHERE pc_number = %s", (user_record[0][0], pc_number)) - cursor.execute("UPDATE pc_list SET password_hash = %s WHERE pc_number = %s", (password_hash, pc_number)) - self.db.commit() - result = {"result": "ok", "password": str(password), "name": str(user_record[0][1])} - - return result - - def stop(self, **kwrags): - discord_user_id = str(kwrags["user_id"]) - cursor = self.db.cursor() - cursor.execute("SELECT * FROM club_member WHERE discord_userid = %s", (discord_user_id,)) - user_record = cursor.fetchall() - cursor.execute("SELECT * FROM pc_usage_history WHERE member_id= %s ORDER BY id DESC LIMIT 1", (user_record[0][0],)) - pc_usage_history_record = cursor.fetchall() - if pc_usage_history_record: - if not pc_usage_history_record[0][5] == None: - result = {"result": "unused"} + result = {"result": "not_used"} else: - cursor.execute("UPDATE pc_usage_history SET end_use_time = current_timestamp WHERE id = %s", (pc_usage_history_record[0][0],)) - cursor.execute("UPDATE pc_list SET using_user_id = NULL WHERE pc_number = %s", (pc_usage_history_record[0][2],)) - cursor.execute("UPDATE pc_list SET password_hash = NULL WHERE pc_number = %s", (pc_usage_history_record[0][2],)) - self.db.commit() - result = {"result": "ok", "pc_number": str(pc_usage_history_record[0][2]), "name": str(user_record[0][1])} + bot_about = None + result = {"result": "bot_about_not_found"} + - return result - - def user_register(self, **kwrags): - discord_user_id = str(kwrags["discord_user_id"]) - discord_user_name = str(kwrags["discord_user_name"]) - name = str(kwrags["name"]) - cursor = self.db.cursor() - cursor.execute("SELECT * FROM club_member WHERE discord_userid = %s", (discord_user_id,)) - user_record = cursor.fetchall() - if not user_record: - cursor.execute("INSERT INTO club_member (name, discord_username, discord_userid) VALUES (%s, %s, %s)", (name, discord_user_name, discord_user_id)) - self.db.commit() - result = {"result": "ok"} - else: - result = {"result": "already_exists"} + except: + result = {"result": "error"} + + finally: + cursor.close() + return result + + + async def timeout_notify(self, **kwargs): + try: + pc_number = kwargs["pc_number"] + discord_display_name = kwargs["discord_display_name"] + + await self.get_channel(dislocker.server_config["bot"]["log_channel_id"]).send(f':negative_squared_cross_mark: {discord_display_name} さんのPC {pc_number} の使用登録はタイムアウトにより解除されました。') + result = {"result": "ok"} + + except Exception as error: + print("自動停止処理中にエラーが発生しました。\nエラー内容") + print(str(error.__class__.__name__)) + print(str(error.args)) + print(str(error)) + result = {"result": "error"} + + finally: + return result - return result async def on_ready(self): - print("ログイン成功") + print("DiscordのBotが起動しました。") + dislocker_activity = discord.Activity( + name=dislocker.server_config["bot"]["activity"]["name"], + type=discord.ActivityType.competing, + details=dislocker.server_config["bot"]["activity"]["details"], + state=dislocker.server_config["bot"]["activity"]["state"] + ) + await bot.change_presence(activity=dislocker_activity) + + async def on_interaction(self, interaction:discord.Interaction): + try: + if interaction.data["component_type"] == 2: + await self.on_button(interaction) + except KeyError: + pass async def on_message(self, message): if message.author.bot: @@ -129,7 +456,7 @@ class Bot(discord.Client): if msg_split[0] == "/password" or msg_split[0] == "/start": #メッセージの要素が2つ以下の場合は拒否 if len(msg_split) <= 2: - await message.channel.send("PC番号、もしくはデバイス番号が入力されていません。") + await message.channel.send("# :warning: PC番号、もしくはデバイス番号が入力されていません。") #メッセージの要素が3つ以上の場合 elif len(msg_split) >= 3: #番号が数字であることを確認 @@ -137,92 +464,338 @@ class Bot(discord.Client): #PC番号が1以上10以下であることを確認 if int(msg_split[1]) <= 10 and int(msg_split[1]) >= 1: if len(msg_split) == 3: - register = self.register(user_id=message.author.id, pc_number=msg_split[1], device_number=msg_split[2]) + register = self.register(user_id=message.author.id, name=message.author.name, display_name=message.author.display_name, pc_number=msg_split[1], device_number=msg_split[2]) elif len(msg_split) == 4: - register = self.register(user_id=message.author.id, pc_number=msg_split[1], device_number=msg_split[2], detail=msg_split[3]) + register = self.register(user_id=message.author.id, name=message.author.name, display_name=message.author.display_name, pc_number=msg_split[1], device_number=msg_split[2], detail=msg_split[3]) if register["result"] == "ok": if len(msg_split) == 3: - await message.channel.send(f"使用が開始されました。\nパスワード | {register["password"]}\nPC番号 | {msg_split[1]}\nデバイス番号 | {msg_split[2]}") + await message.channel.send(f":white_check_mark: 使用が開始されました。\n>>> # パスワード | {register["password"]}\n## PC番号 | {msg_split[1]}\n## デバイス番号 | {msg_split[2]}") elif len(msg_split) == 4: - await message.channel.send(f"使用が開始されました。\nパスワード | {register["password"]}\nPC番号 | {msg_split[1]}\nデバイス番号 | {msg_split[2]}\n使用目的 | {msg_split[3]}") - await self.get_channel(bot_config["log_channel_id"]).send(f'{register["name"]} さんがPC {msg_split[1]} を使用しています') + await message.channel.send(f":white_check_mark: 使用が開始されました。\n>>> # パスワード | {register["password"]}\n## PC番号 | {msg_split[1]}\n## デバイス番号 | {msg_split[2]}\n## 使用目的 | {msg_split[3]}") + await self.get_channel(dislocker.server_config["bot"]["log_channel_id"]).send(f':white_check_mark: {register["name"]} さんがPC {msg_split[1]} の使用を開始しました。') elif register["result"] == "user_data_not_found": - await message.channel.send("ユーザーとして登録されていないようです。管理者に問い合わせてください。") + await message.channel.send("# :dizzy_face: ユーザーとして登録されていないようです。\n最初にサーバーで登録を行ってください。") elif register["result"] == "pc_already_in_use_by_you": - await message.channel.send(f"あなたはPCをもう使用されているようです。使用状態を解除するには /stop で使用終了をお知らせください。\nPC番号 | {register["pc_number"]}\nデバイス番号 | {register["device_number"]}\n使用開始時刻 | {register["start_time"]}\n使用目的 | {register["detail"]}") + await message.channel.send(f"# :exploding_head: あなたはPCをもう使用されているようです。\n使用状態を解除するには /stop で使用終了をお知らせください。\n>>> # PC番号 | {register["pc_number"]}\n# デバイス番号 | {register["device_number"]}\n# 使用開始時刻 | {register["start_time"]}\n# 使用目的 | {register["detail"]}") elif register["result"] == "pc_already_in_use_by_other": - await message.channel.send(f"PCはもう使用されています。別のPC番号を指定して、再度お試しください。") + await message.channel.send(f"# :man_gesturing_no: そのPCは他のメンバーによって使用されています。\n別のPC番号を指定して、再度お試しください。") else: - await message.channel.send("番号がおかしいようです。") + await message.channel.send("# :dizzy_face: 番号がおかしいようです。") else: - await message.channel.send("指定された番号は不正です。") + await message.channel.send("# :dizzy_face: 指定された番号は不正です。") elif msg_split[0] == "/stop": stop = self.stop(user_id=message.author.id) if stop["result"] == "unused": - await message.channel.send("使用されていないようです...") + await message.channel.send("# :shaking_face: 使用されていないようです...") elif stop["result"] == "ok": - await message.channel.send(f"PC番号 {stop["pc_number"]} の使用が終了されました。") - await self.get_channel(bot_config["log_channel_id"]).send(f'{stop["name"]} さんがPC {stop["pc_number"]} の使用を終了しました') + await message.channel.send(f":white_check_mark: PC番号 {stop["pc_number"]} の使用が終了されました。") + await self.get_channel(dislocker.server_config["bot"]["log_channel_id"]).send(f':negative_squared_cross_mark: {stop["name"]} さんがPC {stop["pc_number"]} の使用を終了しました。') - elif message.channel.id == bot_config["config_channel_id"]: + elif message.channel.id == dislocker.server_config["bot"]["config_channel_id"]: msg_split = message.content.split() if msg_split[0] == "/register": - if len(msg_split) <= 3: - await message.channel.send("名前、Discordのユーザー名、DiscordのユーザーIDのいずれかが入力されていません。") + print(len(msg_split)) + if len(msg_split) == 1: + register = self.user_register(name=message.author.display_name, discord_user_name=message.author.name, discord_user_id=message.author.id) + print(register) + if register["result"] == "ok": + await message.channel.send(f"# :white_check_mark: ユーザー情報が登録されました。\n>>> ユーザー名:{message.author.display_name}") + elif register["result"] == "already_exists": + await message.channel.send("# :no_entry: 登録できませんでした。\nもう登録されている可能性があります。") + else: + await message.channel.send("# :no_entry: 登録できませんでした。\n内部エラーが発生しています。") + + elif len(msg_split) <= 3: + await message.channel.send("# :japanese_goblin: 入力内容に不備があります。\n名前、Discordのユーザー名、DiscordのユーザーIDのいずれかが入力されていません。") elif len(msg_split) == 4: if msg_split[3].isdigit(): register = self.user_register(name=msg_split[1], discord_user_name=msg_split[2], discord_user_id=msg_split[3]) if register["result"] == "ok": - await message.channel.send(f"登録が完了しました。\n名前 | {msg_split[1]}\nDiscordのユーザー名 | {msg_split[2]}\nDiscordのユーザーID | {msg_split[3]}") + await message.channel.send(f"# :white_check_mark: 登録が完了しました。\n>>> # 名前 | {msg_split[1]}\n# Discordのユーザー名 | {msg_split[2]}\n# DiscordのユーザーID | {msg_split[3]}") elif register["result"] == "already_exists": - await message.channel.send("そのDiscordアカウントはすでに登録されています。") + await message.channel.send("# :skull_crossbones: 登録できませんでした。\nそのDiscordアカウントはすでに登録されています。") else: - await message.channel.send("DiscordのユーザーIDが不正です。") + await message.channel.send("# :skull_crossbones: 登録できませんでした。\nDiscordのユーザーIDが不正です。") else: - await message.channel.send("なんでかわからんけど不正です。") + await message.channel.send("# :skull_crossbones: 登録できませんでした。\n内部エラーが発生しています。") + + elif msg_split[0] == "/export": + export = self.report_export() + if export["result"] == "ok": + await message.channel.send("# :page_facing_up: 使用履歴のレポートです。", file=discord.File(export["file_path"])) + pass + elif export["result"] == "export_error": + await message.channel.send("# :volcano: エクスポートに失敗しました。") + + elif msg_split[0] == "/fstop": + if len(msg_split) == 1: + await message.channel.send("# :warning: 登録を解除できませんでした。\n使用を停止したいPC番号を指定してください。\n-# /fstop PC番号") + elif len(msg_split) == 2: + if msg_split[1].isdigit(): + fstop = self.force_stop(pc_number=msg_split[1], bot_about="管理者による強制停止。") + if fstop["result"] == "ok": + await message.channel.send(f"# :white_check_mark: PC番号 {msg_split[1]} の使用登録を解除しました。") + elif fstop["result"] == "not_used": + await message.channel.send("# :exploding_head: 登録を解除できませんでした。\nPCは使用されていないようです...") + else: + await message.channel.send("# :x: 登録を解除できませんでした。\n内部エラーが発生しています。") + else: + await message.channel.send("# :warning: 登録を解除できませんでした。\nPC番号を認識できません。\n-# 半角数字で入力してください。") + else: + await message.channel.send("# warning: 登録を解除できませんでした。\構文が間違っています。\n-# /fstop PC番号") + + elif msg_split[0] == "/pcregister": + if len(msg_split) == 1: + await message.channel.send("# :warning: PCを登録できませんでした。\n登録したいPC番号を指定してください。\n-# 半角数字で入力してください。") + elif len(msg_split) == 2: + if msg_split[1].isdigit(): + pc_register = self.pc_register(pc_number=msg_split[1]) + if pc_register["result"] == "ok": + await message.channel.send(f"# :white_check_mark: PCを登録しました。\n>>> # PC番号 | {msg_split[1]}") + elif pc_register["result"] == "already_exists": + await message.channel.send(f":x: PCを登録できませんでした。\nその番号のPCは既に存在します。") + else: + await message.channel.send("# :x: PCを登録できませんでした。\n内部エラーが発生しています。") + else: + await message.channel.send("# :warning: PCを登録できませんでした。\nPC番号を認識できません。\n-# 半角数字で入力してください。") + else: + await message.channel.send("# :warning: PCを登録できませんでした。\n構文が間違っています。\n-# /pcregister PC番号") + + elif msg_split[0] == "/registerbutton": + pc_button_view = View(timeout=None) + for i in range(1, 11): + pc_register_button = discord.ui.Button(style=discord.ButtonStyle.primary, label=f"{i}", custom_id=f"pcregister_{i}") + pc_button_view.add_item(pc_register_button) + + await self.get_channel(dislocker.server_config["bot"]["config_public_channel_id"]).send(f'# :index_pointing_at_the_viewer: 使いたいPCの番号を選んでください!', view=pc_button_view) + + elif msg_split[0] == "/stopbutton": + stop_button_view = View(timeout=None) + stop_button = discord.ui.Button(style=discord.ButtonStyle.danger, label="PCの使用を停止", custom_id="stop") + stop_button_view.add_item(stop_button) + + await self.get_channel(dislocker.server_config["bot"]["config_public_channel_id"]).send(f'# :index_pointing_at_the_viewer: 使用を停止しますか?', view=stop_button_view) + + elif msg_split[0] == "/userbutton": + user_register_button_view = View(timeout=None) + user_register_button = discord.ui.Button(style=discord.ButtonStyle.green, label="ユーザー登録", custom_id="user_register") + user_register_button_view.add_item(user_register_button) + + await self.get_channel(dislocker.server_config["bot"]["config_public_channel_id"]).send(f'# :index_pointing_at_the_viewer: ユーザー登録はお済ですか?', view=user_register_button_view) + + + + elif message.channel.id == dislocker.server_config["bot"]["config_public_channel_id"]: + msg_split = message.content.split() + if msg_split[0] == "/register": + print(len(msg_split)) + if len(msg_split) == 1: + register = self.user_register(name=message.author.display_name, discord_user_name=message.author.name, discord_user_id=message.author.id) + print(register) + if register["result"] == "ok": + await message.channel.send(f"# :white_check_mark: ユーザー情報が登録されました。\nユーザー名:{message.author.display_name}") + elif register["result"] == "already_exists": + await message.channel.send("# :skull_crossbones: 登録できませんでした。\nそのDiscordアカウントはすでに登録されています。") + else: + await message.channel.send("# :skull_crossbones: 登録できませんでした。\n内部エラーが発生しています。") + else: + await message.channel.send("# :skull_crossbones: 登録できませんでした。\n\n-# もしかして...\n-# 手動でメンバーを登録したいですか?\n-# もしそうなら、このチャンネルにはその権限がありません。\n-# そのチャンネルに移動してから、もう一度試してみてください!") + + async def on_button(self, interaction: Interaction): + custom_id = interaction.data["custom_id"] + custom_id_split = custom_id.split("_") + print(custom_id, custom_id_split[0]) + + if custom_id_split[0] == "pcregister": + device_register_view = View(timeout=15) + pc_number = custom_id_split[1] + print(custom_id_split) + for i in range(1, 11): + device_register_button = discord.ui.Button(style=discord.ButtonStyle.primary, label=f"{i}", custom_id=f"deviceregister_{str(pc_number)}_{i}") + device_register_view.add_item(device_register_button) + + await interaction.response.send_message(f"# :keyboard: デバイス番号を選んでください!\n>>> # PC番号 | {str(pc_number)}", view=device_register_view, ephemeral=True) + + elif custom_id_split[0] == "deviceregister": + pc_number = custom_id_split[1] + device_number = custom_id_split[2] + reason_register_view = View(timeout=15) + reason_button = discord.ui.Button(style=discord.ButtonStyle.primary, label="使用目的を入力する", custom_id=f"reasonregister_{str(pc_number)}_{str(device_number)}") + reason_register_view.add_item(reason_button) + + await interaction.response.send_message(f"# :regional_indicator_q: 使用目的を書いてください!\n>>> # PC番号 | {str(pc_number)}\n# デバイス番号 | {str(device_number)}", view=reason_register_view, ephemeral=True) + + elif custom_id_split[0] == "reasonregister": + pc_number = custom_id_split[1] + device_number = custom_id_split[2] + reason_input_form = Reason(title="Dislocker | 登録", pc_number=str(pc_number), device_number=str(device_number)) + await interaction.response.send_modal(reason_input_form) + + elif custom_id_split[0] == "stop": + print("STOP running") + pc_stop = self.stop(user_id=interaction.user.id) + print(pc_stop) + stop_view = View(timeout=15) + if pc_stop["result"] == "unused": + await interaction.response.send_message("# :shaking_face: 使用されていないようです...", ephemeral=True) + elif pc_stop["result"] == "user_data_not_found": + await interaction.response.send_message("# :dizzy_face: ユーザーとして登録されていないようです。\n最初にサーバーで登録を行ってください。", ephemeral=True) + elif pc_stop["result"] == "ok": + await interaction.response.send_message(f":white_check_mark: PC番号 {pc_stop["pc_number"]} の使用が終了されました。", ephemeral=True) + await self.get_channel(dislocker.server_config["bot"]["log_channel_id"]).send(f':negative_squared_cross_mark: {pc_stop["name"]} さんがPC {pc_stop["pc_number"]} の使用を終了しました。') + else: + await interaction.response.send_message("# :skull_crossbones: 停止できませんでした。\n内部エラーが発生しています。", ephemeral=True) + + elif custom_id_split[0] == "user" and custom_id_split[1] == "register": + print("User Register RUnning") + user_register = self.user_register(name=interaction.user.display_name, discord_user_name=interaction.user.name, discord_user_id=interaction.user.id) + if user_register["result"] == "ok": + await interaction.response.send_message(f"# :white_check_mark: ユーザー情報が登録されました。\n>>> ユーザー名:{interaction.user.display_name}", ephemeral=True) + elif user_register["result"] == "already_exists": + await interaction.response.send_message("# :no_entry: 登録できませんでした。\nもう登録されている可能性があります。", ephemeral=True) + else: + await interaction.response.send_message("# :no_entry: 登録できませんでした。\n内部エラーが発生しています。", ephemeral=True) + +class Monitor(): + def __init__(self, **kwargs) -> None: + self.search_frequency = kwargs["search_frequency"] + self.allowable_time = kwargs["allowable_time"] + self.fstop_time = kwargs["fstop_time"] + self.init_wait_time = 10 + + def start(self, **kwargs): + search_thread = threading.Thread(target=self.search) + search_thread.start() -config_dir_path = "./config/" -db_config_path = config_dir_path + "db.json" -if not os.path.isfile(db_config_path): - if not os.path.isdir(config_dir_path): - os.mkdir(config_dir_path) + def search(self): + try: + time.sleep(self.init_wait_time) + while True: + cursor = dislocker.db.cursor() + cursor.execute("SELECT * FROM pc_list WHERE password_hash IS NOT NULL") + pc_list = cursor.fetchall() + current_datetime = datetime.now() + fstop_time = self.fstop_time + if current_datetime.time().strftime("%H:%M:%S") == fstop_time: + for i in dislocker.pc_list: + stop = bot.force_stop(pc_number=i, bot_about="使用停止忘れによるBotによる強制停止。") + result = {"result": "FSTOP"} + else: + if pc_list: + if len(pc_list) == 1: + user_id = pc_list[0][1] + cursor.execute("SELECT * FROM pc_usage_history WHERE member_id= %s AND end_use_time IS NULL ORDER BY id DESC LIMIT 1", (user_id,)) + pc_usage = cursor.fetchall() + print(pc_usage) + start_time = pc_usage[0][4] + print(start_time) + print(type(start_time)) + time_difference = current_datetime - start_time + print(current_datetime, start_time) + print(time_difference.seconds, timedelta(seconds=self.allowable_time).seconds) + if time_difference.seconds >= timedelta(seconds=self.allowable_time).seconds: + cursor.execute("SELECT * FROM club_member WHERE id = %s", (user_id,)) + user_info = cursor.fetchall() + stop = bot.stop(user_id=user_info[0][3], bot_about="パスワードのタイムアウトでBotによる強制停止。") + + bot.timeout_notify(pc_number=pc_list[0][0], discord_display_name=user_info[0][1]) + result = {"result": "STOP", "details": str(pc_usage)} + else: + result = {"result": "BUT SAFE", "details": str(pc_usage)} - db_config = { - "host": "localhost", - "db": "dislocker", - "username": "user", - "password": "example_pass", - "port": "5432" - } - with open(db_config_path, "w") as w: - json.dump(db_config, w, indent=4) -elif os.path.isfile(db_config_path): - with open(db_config_path, "r") as r: - db_config = json.load(r) -bot_config_path = config_dir_path + "bot.json" -if not os.path.isfile(bot_config_path): - if not os.path.isdir(config_dir_path): - os.mkdir(config_dir_path) + elif len(pc_list) >= 2: + for i in pc_list: + print(i) + user_id = i[1] + cursor.execute("SELECT * FROM pc_usage_history WHERE member_id= %s AND end_use_time IS NULL ORDER BY id DESC LIMIT 1", (user_id,)) + pc_usage = cursor.fetchall() + print(pc_usage) + start_time = pc_usage[0][4] + print(start_time) + print(type(start_time)) + time_difference = current_datetime - start_time + print(time_difference.seconds, timedelta(seconds=self.allowable_time).seconds) + if time_difference.seconds >= timedelta(seconds=self.allowable_time).seconds: + cursor.execute("SELECT * FROM club_member WHERE id = %s", (user_id,)) + user_info = cursor.fetchall() + stop = bot.stop(user_id=user_info[0][3], bot_about="タイムアウトでBotによる強制停止。") + + bot.timeout_notify(pc_number=i[0], discord_display_name=user_info[0][1]) + result = {"result": "STOP", "details": str(pc_usage)} + else: + result = {"result": "BUT SAFE", "details": str(pc_usage)} - bot_config = { - "token": "TYPE HERE BOTS TOKEN KEY", - "log_channel_id" : "TYPE HERE CHANNEL ID", - "config_channel_id": "TYPE HERE CHANNEL ID" - } - with open(bot_config_path, "w") as w: - json.dump(bot_config, w, indent=4) + else: + result = {"result": "NONE"} + else: + result = {"result": "NONE"} -elif os.path.isfile(bot_config_path): - with open(bot_config_path, "r") as r: - bot_config = json.load(r) + if result["result"] == "NONE": + pass + else: + print(current_datetime) + print(result["result"]) + time.sleep(self.search_frequency) -intents = discord.Intents.default() -intents.message_content = True -bot = Bot(intents=intents) -bot.db_connect(db_config["host"], db_config["db"], db_config["port"], db_config["username"], db_config["password"]) -bot.run(bot_config["token"]) \ No newline at end of file + except Exception as error: + print("自動停止処理中にエラーが発生しました。\nエラー内容") + print(str(error.__class__.__name__)) + print(str(error.args)) + print(str(error)) + result = {"result": "error"} + dislocker.db.rollback() + + finally: + cursor.close() + print(result["result"]) + return result + + + +class Reason(Modal): + def __init__(self, title: str, pc_number: str, device_number: str, timeout=15) -> None: + super().__init__(title=title, timeout=timeout) + print(pc_number) + print(device_number) + self.reason_input_form = TextInput(label="使用目的を入力してください", style=TextStyle.short, custom_id=f"register_{pc_number}_{device_number}") + self.add_item(self.reason_input_form) + + async def on_submit(self, interaction: Interaction) -> None: + custom_id = interaction.data["components"][0]["components"][0]["custom_id"] + print(custom_id) + custom_id_split = custom_id.split("_") + pc_number = custom_id_split[1] + device_number = custom_id_split[2] + register = bot.register(user_id=interaction.user.id, name=interaction.user.name, display_name=interaction.user.display_name, pc_number=pc_number, device_number=device_number, detail=self.reason_input_form.value) + print(register["result"]) + + if register["result"] == "ok": + await interaction.response.send_message(f":white_check_mark: 使用が開始されました。\n>>> # パスワード | {register["password"]}\n## PC番号 | {pc_number}\n## デバイス番号 | {device_number}\n## 使用目的 | {self.reason_input_form.value}", ephemeral=True) + await bot.get_channel(dislocker.server_config["bot"]["log_channel_id"]).send(f':white_check_mark: {register["name"]} さんがPC {pc_number} の使用を開始しました。') + elif register["result"] == "pc_already_in_use_by_you": + await interaction.response.send_message(f"# :exploding_head: あなたはPCをもう使用されているようです。\n使用状態を解除するには 終了ボタン で使用終了をお知らせください。\n>>> # PC番号 | {register["pc_number"]}\n# デバイス番号 | {register["device_number"]}\n# 使用開始時刻 | {register["start_time"]}\n# 使用目的 | {register["detail"]}", ephemeral=True) + elif register["result"] == "pc_already_in_use_by_other": + await interaction.response.send_message(f"# :man_gesturing_no: そのPCは他のメンバーによって使用されています。\n別のPC番号を指定して、再度お試しください。", ephemeral=True) + elif register["result"] == "user_data_not_found": + await interaction.response.send_message("# :dizzy_face: ユーザーとして登録されていないようです。\n最初にサーバーで登録を行ってください。", ephemeral=True) + else: + await interaction.response.send_message("# :skull_crossbones: 登録できませんでした。\n内部エラーが発生しています。", ephemeral=True) + + + +dislocker = DL() +if dislocker.init_result == "ok": + print("Botを起動します...") + intents = discord.Intents.default() + intents.message_content = True + bot = Bot(intents=intents) + monitor = Monitor(search_frequency=dislocker.server_config["bot"]["monitor"]["search_frequency"], allowable_time=dislocker.server_config["bot"]["monitor"]["allowable_time"], fstop_time=dislocker.server_config["bot"]["monitor"]["fstop_time"]) + monitor.start() + bot.run(dislocker.server_config['bot']['token']) +else: + pass \ No newline at end of file diff --git a/dislocker_auth.py b/dislocker_auth.py index 2d1453c..a16ab4d 100644 --- a/dislocker_auth.py +++ b/dislocker_auth.py @@ -4,56 +4,122 @@ import json from flask import Flask, request, jsonify, render_template config_dir_path = "./config/" -db_config_path = config_dir_path + "db.json" -if not os.path.isfile(db_config_path): +server_config_path = config_dir_path + "server.json" +if not os.path.isfile(server_config_path): if not os.path.isdir(config_dir_path): os.mkdir(config_dir_path) - db_config = { - "host": "localhost", - "db": "dislocker", - "username": "user", - "password": "example_pass", - "port": "5432" + server_config = { + "db": { + "host": "localhost", + "port": "5432", + "db_name": "dislocker", + "username": "user", + "password": "password" + }, + "bot": { + "token": "TYPE HERE BOTS TOKEN KEY", + "activity": { + "name": "Dislocker", + "details": "ロック中...", + "type": "playing", + "state": "ロック中..." + }, + "log_channel_id" : "TYPE HERE CHANNEL ID (YOU MUST USE INT !!!!)", + "config_channel_id": "TYPE HERE CHANNEL ID (YOU MUST USE INT !!!!)", + "config_public_channel_id": "TYPE HERE CHANNEL ID (YOU MUST USE INT !!!!)" + } } - with open(db_config_path, "w") as w: - json.dump(db_config, w, indent=4) -elif os.path.isfile(db_config_path): - with open(db_config_path, "r") as r: - db_config = json.load(r) + with open(server_config_path, "w") as w: + json.dump(server_config, w, indent=4) +elif os.path.isfile(server_config_path): + with open(server_config_path, "r") as r: + server_config = json.load(r) class Auth(): def __init__(self, host, db, port, user, password): self.db = psycopg2.connect(f"host={host} dbname={db} port={port} user={user} password={password}") def check(self, pc_number, password): - cursor = self.db.cursor() - cursor.execute("SELECT * FROM pc_list WHERE pc_number = %s AND password_hash = %s", (pc_number, password)) - pc_info = cursor.fetchall() - if not pc_info: - return 1 - else: - return 0 + try: + cursor = self.db.cursor() + cursor.execute("SELECT * FROM pc_list WHERE pc_number = %s AND password_hash = %s", (pc_number, password)) + pc_info = cursor.fetchall() + if not pc_info: + return 1 + else: + return 0 + finally: + cursor.close() def delete(self, pc_number): - cursor = self.db.cursor() - cursor.execute("UPDATE pc_list SET password_hash = NULL WHERE pc_number = %s", (pc_number,)) - self.db.commit() + try: + cursor = self.db.cursor() + cursor.execute("UPDATE pc_list SET password_hash = NULL WHERE pc_number = %s", (pc_number,)) + self.db.commit() + finally: + cursor.close() + + def stop(self, **kwargs): + try: + pc_number = int(kwargs["pc_number"]) + cursor = self.db.cursor() + cursor.execute("SELECT * FROM pc_list WHERE pc_number = %s", (pc_number,)) + pc_list_record = cursor.fetchall() + if not pc_list_record[0][1] == None: + cursor.execute("UPDATE pc_list SET using_user_id = NULL WHERE pc_number = %s", (pc_number,)) + + if not pc_list_record[0][2] == None: + cursor.execute("UPDATE pc_list SET password_hash = NULL WHERE pc_number = %s", (pc_number,)) + + cursor.execute("SELECT * FROM pc_usage_history WHERE member_id = %s AND pc_number = %s ORDER BY id DESC LIMIT 1", (pc_list_record[0][1], pc_number)) + pc_usage_history_record = cursor.fetchall() + cursor.execute("UPDATE pc_usage_history SET end_use_time = current_timestamp WHERE id = %s", (pc_usage_history_record[0][0],)) + self.db.commit() + result = {"result": "ok"} + + else: + result = {"result": "not_used"} + + except: + result = {"result": "error"} + + finally: + cursor.close() + return result + app = Flask(__name__, static_folder="./resource/") -auth = Auth(db_config["host"], db_config["db"], db_config["port"], db_config["username"], db_config["password"]) +auth = Auth(server_config["db"]["host"], server_config["db"]["db_name"], server_config["db"]["port"], server_config["db"]["username"], server_config["db"]["password"]) @app.route('/verify', methods=['POST']) def verify(): pc_number = int(request.json.get('pc_number')) password = request.json.get('password') + print(str(pc_number) + "の認証処理を開始...") if auth.check(pc_number, password) == 0: auth.delete(pc_number) + print(str(pc_number) + "の認証処理は成功しました.") return jsonify({'message': 'ok'}), 200 else: + print(str(pc_number) + "の認証処理は失敗しました.") return jsonify({'message': 'damedesu'}), 401 +@app.route('/stop', methods=['POST']) +def stop(): + pc_number = int(request.json.get('pc_number')) + print(str(pc_number) + "の使用停止処理を開始...") + pc_stop = auth.stop(pc_number=pc_number) + if pc_stop["result"] == "ok": + print(str(pc_number) + "の使用停止処理は成功しました.") + return jsonify({'message': 'ok'}), 200 + else: + print(str(pc_number) + "の使用停止処理は失敗しました.") + return jsonify({'message': 'error'}), 500 + + + if __name__ == '__main__': - app.run(host="0.0.0.0", port=5000, debug=True) \ No newline at end of file + app.run(host="0.0.0.0", port=5000, debug=False) \ No newline at end of file diff --git a/dislocker_client.py b/dislocker_client.py index 02e8af7..79c5bd7 100644 --- a/dislocker_client.py +++ b/dislocker_client.py @@ -11,22 +11,78 @@ import string import random import tkinter import threading +import signal +import sys +import shutil + +app_name = "Dislocker" +dislocker_dir = os.path.dirname(os.path.abspath(sys.argv[0])) + +os.chdir(dislocker_dir) + +resource_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "resource") +config_dir_path = "./config/" +client_config_path = config_dir_path + "client.json" +if not os.path.isfile(client_config_path): + if not os.path.isdir(config_dir_path): + os.mkdir(config_dir_path) + + client_config = { + "initial": 1, + "auth_host_url": "http://localhost", + "pc_number": 1, + "master_password_hash": "", + "testing": 0 + } + +elif os.path.isfile(client_config_path): + with open(client_config_path, "r") as r: + client_config = json.load(r) + +def init(**kwargs): + sp_startupinfo = subprocess.STARTUPINFO() + sp_startupinfo.dwFlags = subprocess.STARTF_USESHOWWINDOW + sp_startupinfo.wShowWindow = subprocess.SW_HIDE + task_exist = subprocess.run('tasklist /fi "IMAGENAME eq dislocker_client.exe"', startupinfo=sp_startupinfo, stdout=subprocess.PIPE, text=True) + if 'dislocker_client.exe' in task_exist.stdout: + task_count = task_exist.stdout.count("dislocker_client.exe") + if task_count == 1: + pass + else: + return 1 + + if client_config["initial"] == 1: + master_password = master_password_gen() + msgbox = tkinter.messagebox.showinfo(title=f"{app_name} | 初回起動を検出", message=f"初回起動のようです。\nマスターパスワードを記録しておいてください。\nこれ以降二度と表示されることはないでしょう。\n\n{master_password["password"]}\n\nまた、認証先サーバーの接続先を指定してください。ロックを解除できなくなります。") + client_config["master_password_hash"] = master_password["password_hash"] + client_config["initial"] = 0 + if "pc_number" in kwargs: + client_config["pc_number"] = int(kwargs["pc_number"]) + else: + client_config["pc_number"] = 1 + with open(client_config_path, "w") as w: + json.dump(client_config, w, indent=4) + return 2 + else: + return 0 + class App(customtkinter.CTk): def __init__(self): super().__init__() self.title(f"{app_name} | ロック中") + self.iconbitmap(default=resource_path + '\\icon\\dislocker.ico') if client_config["testing"] == 1: pass else: self.attributes('-fullscreen', True) self.attributes('-topmost', True) + self.block_taskmgr() + self.block_key() self.frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color='transparent') self.frame.grid(row=0, column=0, sticky='nsew') - self.block_taskmgr() - self.block_key() lock = Lock() def exit(self): @@ -34,13 +90,44 @@ class App(customtkinter.CTk): self.toast() self.destroy() + def delete_appdata(self, **kwargs): + process_name = kwargs["process_name"] + dir_path = kwargs["dir_path"] + + if not os.path.exists(dir_path): + print(f"エラー: 指定されたディレクトリ {dir_path} が存在しません。") + return 1 + + try: + # プロセスの終了 + subprocess.run(['taskkill', '/f', '/t', '/im', process_name]) + print(f"{process_name} を終了しました。") + + # ディレクトリの削除 + shutil.rmtree(dir_path) + print(f"{dir_path} を削除しました。") + + return 0 + except subprocess.CalledProcessError as e: + print(f"プロセス終了エラー: {e}") + return 1 + except PermissionError as e: + print(f"権限エラー: {e}") + return 1 + except Exception as error: + print("エラーが発生しました。\nエラー内容:") + print(f"エラータイプ: {error.__class__.__name__}") + print(f"エラー引数: {error.args}") + print(f"エラーメッセージ: {str(error)}") + return 1 + def block_key(self): block_keys = ['ctrl', 'alt', 'windows', 'shift', 'delete'] for i in block_keys: keyboard.block_key(i) def block_taskmgr(self): - block = subprocess.run(['reg', 'add', 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System', '/v', 'DisableTaskMgr', '/t', 'REG_DWORD', '/d', '1']) + block = subprocess.run(['reg', 'add', 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System', '/v', 'DisableTaskMgr', '/t', 'REG_DWORD', '/d', '1', '/f']) print(block) def unlock_taskmgr(self): @@ -48,7 +135,6 @@ class App(customtkinter.CTk): print(unlock) def toast(self): - resource_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "resource") success = Notification( app_id='Dislocker', title='ご協力ありがとうございます!', @@ -65,47 +151,87 @@ class App(customtkinter.CTk): class Lock(customtkinter.CTkToplevel): def __init__(self): super().__init__() - self.title(f'{app_name} | PC番号 {client_config["pc_number"]} | ロックされています') - self.geometry('400x200') + if client_config["testing"] == 1: + self.title(f'{app_name} | PC番号 {client_config["pc_number"]} | テストモード') + else: + self.title(f'{app_name} | PC番号 {client_config["pc_number"]} | ロックされています') + + self.iconbitmap(default=resource_path + '\\icon\\dislocker.ico') + self.window_width = 600 + self.window_height = 320 + self.screen_width = self.winfo_screenwidth() + self.screen_height = self.winfo_screenheight() + self.center_x = int(self.screen_width/2 - self.window_width/2) + self.center_y = int(self.screen_height/2 - self.window_height/2) + self.geometry(f"{str(self.window_width)}x{str(self.window_height)}+{str(self.center_x)}+{str(self.center_y)}") self.resizable(height=False, width=False) self.attributes('-topmost', True) self.grab_set() self.lift() self.protocol("WM_DELETE_WINDOW", self.handler_close) - self.title_font = customtkinter.CTkFont(family="meiryo", size=24, weight="bold") - self.general_font = customtkinter.CTkFont(family="meiryo", size=14) + self.emoji_font = customtkinter.CTkFont(family="Segoe UI Emoji", size=32) + self.title_font = customtkinter.CTkFont(family="meiryo", size=32, weight="bold") + self.pc_number_font = customtkinter.CTkFont(family="meiryo", size=64, weight="bold") + self.title_small_font = customtkinter.CTkFont(family="meiryo", size=16) + self.general_font = customtkinter.CTkFont(family="meiryo", size=18) self.general_small_font = customtkinter.CTkFont(family="meiryo", size=12) + self.textbox_font = customtkinter.CTkFont(family="meiryo", size=14) + self.button_font = customtkinter.CTkFont(family="meiryo", size=14) + self.msg_title_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color='transparent') - self.msg_title_frame.grid(row=0, column=0, sticky="nsew") + self.msg_title_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") - self.msg_title_1 = customtkinter.CTkLabel(self.msg_title_frame, text='ちょっと待って!!', font=self.title_font, anchor=tkinter.W) - self.msg_title_1.grid(row=0, column=0, padx=10, pady=10) + self.icon_title_1 = customtkinter.CTkLabel(self.msg_title_frame, text='😎', font=self.emoji_font, justify="left") + self.icon_title_1.grid(row=0, column=0, padx=10, sticky="w") + + self.msg_title_1 = customtkinter.CTkLabel(self.msg_title_frame, text=f'ちょっと待って!! PC番号 | {client_config["pc_number"]}', font=self.title_font, justify="left") + self.msg_title_1.grid(row=0, column=1, padx=10, sticky="w") + + self.msg_title_2 = customtkinter.CTkLabel(self.msg_title_frame, text="本当にあなたですか?", font=self.title_small_font, justify="left") + self.msg_title_2.grid(row=1, column=1, padx=10, sticky="w") self.msg_subtitle_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color='transparent') - self.msg_subtitle_frame.grid(row=1, column=0, sticky="nsew") + self.msg_subtitle_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew") + self.msg_subtitle_frame.grid_columnconfigure(0, weight=1) - self.msg_subtitle_1 = customtkinter.CTkLabel(self.msg_subtitle_frame, text='サインインするには、パスワードが必要です。', font=self.general_font, anchor=tkinter.W) - self.msg_subtitle_1.grid(row=0, column=0, padx=10, pady=10) + self.msg_subtitle_1 = customtkinter.CTkLabel(self.msg_subtitle_frame, text='サインインするには、Discordのダイレクトメッセージに送信された\nパスワードを入力してください。', font=self.general_font, justify="left") + self.msg_subtitle_1.grid(row=0, column=0, padx=10, sticky="ew") + + self.msg_subtitle_2 = customtkinter.CTkLabel(self.msg_subtitle_frame, text='※ パスワードの有効期限は23:59までです。', font=self.general_small_font, justify="left") + self.msg_subtitle_2.grid(row=1, column=0, padx=10, sticky="w") - self.input_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color='transparent') - self.input_frame.grid(row=2, column=0, sticky="nsew") + self.input_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color='transparent') + self.input_frame.grid(row=2, column=0, padx=10, pady=10, sticky="nsew") + self.input_frame.columnconfigure(0, weight=1) - self.password_entry = customtkinter.CTkEntry(self.input_frame, placeholder_text='パスワード', width=380, show='*', font=self.general_small_font) - self.password_entry.grid(row=0, column=0, padx=10, pady=10) + self.password_entry = customtkinter.CTkEntry(self.input_frame, placeholder_text='パスワード', show='*', font=self.textbox_font) + self.password_entry.grid(row=0, column=0, padx=10, sticky="ew") + self.password_entry.bind("", self.auth_start_ev) self.button_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color='transparent') - self.button_frame.grid(row=3, column=0, sticky="nsew") + self.button_frame.grid(row=3, column=0, padx=10, pady=10, sticky="nsew") + self.button_frame.columnconfigure(0, weight=3) + self.button_frame.columnconfigure(1, weight=1) + self.button_frame.columnconfigure(2, weight=1) - self.signin_button = customtkinter.CTkButton(self.button_frame, text='サインイン', command=self.auth, font=self.general_font) - self.signin_button.grid(row=0, column=1, padx=10, pady=10) - self.signin_button.pack(padx=10, anchor=tkinter.E) + self.signin_button = customtkinter.CTkButton(self.button_frame, text='サインイン', command=self.auth_start, font=self.button_font) + self.signin_button.grid(row=0, column=2, padx=10, sticky="e") + self.signout_button = customtkinter.CTkButton(self.button_frame, text='サインアウト', command=self.signout, font=self.button_font) + self.signout_button.grid(row=0, column=1, padx=10, sticky="e") + + self.help_button = customtkinter.CTkButton(self.button_frame, text='ヘルプ', command=self.help_dummy, font=self.button_font) + self.help_button.grid(row=0, column=0, padx=10, sticky="w") + self.keyboard_listener_thread = threading.Thread(target=self.keyboard_listener) self.keyboard_listener_thread.daemon = True self.keyboard_listener_thread.start() + def help_wakeup(self): + help = Help() + def keyboard_listener(self): keyboard.add_hotkey('ctrl+shift+q', self.exit) @@ -113,7 +239,46 @@ class Lock(customtkinter.CTkToplevel): hashed = hashlib.md5(source.encode()) return hashed.hexdigest() + def auth_start(self): + auth_thread = threading.Thread(target=self.auth) + auth_thread.daemon = True + auth_thread.start() + + def auth_start_ev(self, event): + auth_thread = threading.Thread(target=self.auth) + auth_thread.daemon = True + auth_thread.start() + + def button_disable(self): + self.help_button.configure(state="disabled", fg_color="gray") + self.signin_button.configure(state="disabled", fg_color="gray") + self.signout_button.configure(state="disabled", fg_color="gray") + + def button_enable(self): + self.help_button.configure(state="normal", fg_color="#3c8dd0") + self.signin_button.configure(state="normal", fg_color="#3c8dd0") + self.signout_button.configure(state="normal", fg_color="#3c8dd0") + + def auth(self): + self.button_disable() + password = str(self.password_entry.get()) + if len(password) == 10: + print("マスターパスワードで認証を試行します。") + master_password_hash = self.hash_genarate(str(self.password_entry.get())) + if client_config["master_password_hash"] == master_password_hash: + print("マスターパスワードで認証しました。") + self.exit() + else: + print("マスターパスワードで認証できませんでした。") + self.withdraw() + msgbox = tkinter.messagebox.showinfo(title=f"{app_name} | 誤ったパスワード", message=f"パスワードが間違っています!") + self.msg_subtitle_1.configure(text='パスワードが間違っています! ') + self.button_enable() + self.deiconify() + + + print("認証サーバーにアクセスします。") auth_url = client_config["auth_host_url"] + "/verify" auth_json = { "pc_number": int(client_config["pc_number"]), @@ -121,29 +286,113 @@ class Lock(customtkinter.CTkToplevel): } try: responce = requests.post(auth_url, json=auth_json) + if responce.status_code == 200: + print("認証サーバー経由で認証しました。") + self.exit() + else: + print("認証サーバー経由での認証に失敗しました。") + self.withdraw() + msgbox = tkinter.messagebox.showinfo(title=f"{app_name} | 誤ったパスワード", message=f"パスワードが間違っています!") + self.msg_subtitle_1.configure(text='パスワードが間違っています! ') + self.button_enable() + self.deiconify() except: + print("認証サーバーにアクセスできません。マスターパスワードで認証を試行します。") master_password_hash = self.hash_genarate(str(self.password_entry.get())) if client_config["master_password_hash"] == master_password_hash: + print("マスターパスワードで認証しました。") self.exit() else: - self.msg_subtitle_1.configure(text='サインインするには、パスワードが必要です。\nネットワークエラーが発生しています。\n続行するには、マスターパスワードを入力して下さい。') + print("マスターパスワードで認証できませんでした。") + self.withdraw() + msgbox = tkinter.messagebox.showinfo(title=f"{app_name} | ネットワークエラー", message=f"認証サーバーにアクセスできませんでした。\n続行するには、マスターパスワードを入力してください。") + self.msg_subtitle_1.configure(text='ネットワークエラーが発生しています。\n続行するには、マスターパスワードを入力して下さい。 ') + self.button_enable() + self.deiconify() + + def signout(self): + app.unlock_taskmgr() + self.destroy() + signout_command = subprocess.run(['shutdown', '/l', '/f']) + print(signout_command) + - if responce.status_code == 200: - self.exit() - else: - master_password_hash = self.hash_genarate(str(self.password_entry.get())) - if client_config["master_password_hash"] == master_password_hash: - self.exit() - else: - self.msg_subtitle_1.configure(text='サインインするには、パスワードが必要です。\nパスワードが違います!') - def handler_close(self): pass + def help_dummy(self): + self.withdraw() + msgbox = tkinter.messagebox.showinfo(title=f"{app_name} | 未実装", message=f"ヘルプページは製作途中です。\nDiscordサーバーの指示に従って、認証を進めてください。") + self.deiconify() + def exit(self): self.destroy() app.exit() + +class Help(customtkinter.CTkToplevel): + def __init__(self): + super().__init__() + if client_config["testing"] == 1: + self.title(f'{app_name} | ヘルプ | テストモード') + else: + self.title(f'{app_name} | ヘルプ') + self.iconbitmap(default=resource_path + '\\icon\\dislocker.ico') + self.geometry("600x400") + self.resizable(height=False, width=False) + self.attributes('-topmost', True) + self.grab_set() + self.lift() + self.protocol("WM_DELETE_WINDOW", self.handler_close) + msgbox = tkinter.messagebox.showinfo(title=f"{app_name} | 未実装", message=f"ヘルプページは製作途中です。\nDiscordサーバーの指示に従って、認証を進めてください。") + self.destroy() + + def handler_close(self): + self.destroy() + +class Monitor(): + def __init__(self) -> None: + pass + + def start(self): + monitor_thread = threading.Thread(target=self.run) + monitor_thread.start() + + def signal_handler(self): + print("停止処理を実行。") + appdata_local = os.path.expandvars("%LOCALAPPDATA%") + appdata_roaming = os.path.expandvars("%APPDATA%") + epic_del = app.delete_appdata(process_name="EpicGamesLauncher.exe", dir_path=f"{appdata_local}\\EpicGamesLauncher\\Saved") + chrome_del = app.delete_appdata(process_name="chrome.exe", dir_path=f"{appdata_local}\\Google\\Chrome\\User Data") + discord_del = app.delete_appdata(process_name="discord.exe", dir_path=f"{appdata_roaming}\\discord") + steam_del = app.delete_appdata(process_name="steam.exe", dir_path=f"{appdata_local}\\Steam") + ea_del = app.delete_appdata(process_name="EADesktop.exe", dir_path=f"{appdata_local}\\Electronic Arts") + riot_del = app.delete_appdata(process_name="RiotClientServices.exe", dir_path=f"{appdata_local}\\Riot Games\\Riot Client") + stop_url = client_config["auth_host_url"] + "/stop" + stop_json = { + "pc_number": int(client_config["pc_number"]) + } + try: + responce = requests.post(stop_url, json=stop_json) + if responce.status_code == 200: + print("停止処理は成功しました。") + else: + print("内部エラーにより停止処理に失敗しました。") + result_msgbox = tkinter.messagebox.showwarning(title=f"{app_name} | エラー", message=f"内部エラーにより停止処理に失敗しました。\nDiscordサーバーの指示に従って、停止処理を自身で行ってください。") + except: + print("ネットワークエラーにより停止処理に失敗しました。") + result_msgbox = tkinter.messagebox.showwarning(title=f"{app_name} | エラー", message=f"ネットワークエラーにより停止処理に失敗しました。\nDiscordサーバーの指示に従って、停止処理を自身で行ってください。") + finally: + self.shutdown() + + def shutdown(self): + shutdown_command = subprocess.run(['shutdown', '/s', '/t', '1']) + + + def run(self): + signal.signal(signal.SIGTERM, self.signal_handler) + + def master_password_gen(): numbers = string.digits # (1) password = ''.join(random.choice(numbers) for _ in range(10)) # (2) @@ -151,39 +400,37 @@ def master_password_gen(): result = {"password": password, "password_hash": password_hash} return result -app_name = "Dislocker" -config_dir_path = "./config/" -client_config_path = config_dir_path + "client.json" -if not os.path.isfile(client_config_path): - if not os.path.isdir(config_dir_path): - os.mkdir(config_dir_path) - - client_config = { - "initial": 1, - "auth_host_url": "http://localhost", - "pc_number": 1, - "master_password_hash": "", - "testing": 0 - - } - with open(client_config_path, "w") as w: - json.dump(client_config, w, indent=4) -elif os.path.isfile(client_config_path): - with open(client_config_path, "r") as r: - client_config = json.load(r) - - -if client_config["initial"] == 1: - master_password = master_password_gen() - msgbox = tkinter.messagebox.showinfo(title=f"{app_name} | 初回起動を検出", message=f"初回起動のようです。\nマスターパスワードを記録しておいてください。\nこれ以降二度と表示されることはないでしょう。\n\n{master_password["password"]}\n\nまた、認証先サーバーの接続先を指定してください。ロックを解除できなくなります。") - client_config["master_password_hash"] = master_password["password_hash"] - client_config["initial"] = 0 - with open(client_config_path, "w") as w: - json.dump(client_config, w, indent=4) - -else: - if __name__ == '__main__': - app = App() - app.protocol("WM_DELETE_WINDOW", app.handler_close) - app.mainloop() +if __name__ == '__main__': + args = sys.argv + if len(args) >= 2: + if args[1] == "stop": + init_result = init() + if init_result == 1: + warning_msgbox = tkinter.messagebox.showwarning(title=f"{app_name} | 多重起動エラー", message=f"もう終了処理を行っています。\nPCがシャットダウンするまで、もう少しお待ちください。") + elif init_result == 2: + pass + else: + app = App() + monitor = Monitor() + monitor.signal_handler() + elif args[1] == "setup": + init_result = init(pc_number=args[2]) + if init_result == 1: + warning_msgbox = tkinter.messagebox.showwarning(title=f"{app_name} | 多重起動エラー", message=f"すでに {app_name} は実行されています。\n正常に起動しない場合は、既に起動しているプロセスを終了してから、もう一度起動してみてください。") + elif init_result == 2: + pass + else: + pass + else: + print("引数エラー。") + else: + init_result = init() + if init_result == 1: + warning_msgbox = tkinter.messagebox.showwarning(title=f"{app_name} | 多重起動エラー", message=f"すでに {app_name} は実行されています。\n正常に起動しない場合は、既に起動しているプロセスを終了してから、もう一度起動してみてください。") + elif init_result == 2: + pass + else: + app = App() + app.protocol("WM_DELETE_WINDOW", app.handler_close) + app.mainloop() diff --git a/docs/pyinstaller.txt b/docs/pyinstaller.txt new file mode 100644 index 0000000..dcb4fb2 --- /dev/null +++ b/docs/pyinstaller.txt @@ -0,0 +1 @@ +pyinstaller .\dislocker_client.py --noconsole --icon .\resource\icon\dislocker.ico --add-data ".\resource:resource" \ No newline at end of file diff --git a/docs/データベースセットアップ.txt b/docs/データベースセットアップ.txt index affbd63..44c603b 100644 --- a/docs/データベースセットアップ.txt +++ b/docs/データベースセットアップ.txt @@ -5,4 +5,7 @@ CREATE TABLE club_member (id SERIAL NOT NULL, name VARCHAR(30) NOT NULL, discord CREATE TABLE pc_list (pc_number INTEGER NOT NULL, using_user_id INTEGER, password_hash VARCHAR(32), PRIMARY KEY (pc_number), FOREIGN KEY (using_user_id) REFERENCES club_member(id)); CREATE TABLE pc_usage_history (id SERIAL NOT NULL, member_id INTEGER NOT NULL, pc_number INTEGER NOT NULL, device_number INTEGER NOT NULL, start_use_time TIMESTAMP NOT NULL, end_use_time TIMESTAMP, use_detail VARCHAR(30), PRIMARY KEY (id), FOREIGN KEY (member_id) REFERENCES club_member(id), FOREIGN KEY (pc_number) REFERENCES pc_list(pc_number)); +# PC登録 +INSERT INTO pc_list (pc_number) VALUES (PC番号をいれる); + INSERTのとき文字列はシングルクォーテーションで囲む!!! \ No newline at end of file diff --git a/requirements_client.txt b/requirements_client.txt new file mode 100644 index 0000000..8c989de --- /dev/null +++ b/requirements_client.txt @@ -0,0 +1,4 @@ +customtkinter +winotify +keyboard +requests \ No newline at end of file diff --git a/resource/icon/dislocker.ico b/resource/icon/dislocker.ico new file mode 100644 index 0000000..815a743 Binary files /dev/null and b/resource/icon/dislocker.ico differ diff --git a/resource/icon/png/128.png b/resource/icon/png/128.png new file mode 100644 index 0000000..ecbd10b Binary files /dev/null and b/resource/icon/png/128.png differ diff --git a/resource/icon/png/16.png b/resource/icon/png/16.png new file mode 100644 index 0000000..00dfc3f Binary files /dev/null and b/resource/icon/png/16.png differ diff --git a/resource/icon/png/256.png b/resource/icon/png/256.png new file mode 100644 index 0000000..1259f82 Binary files /dev/null and b/resource/icon/png/256.png differ diff --git a/resource/icon/png/32.png b/resource/icon/png/32.png new file mode 100644 index 0000000..e473636 Binary files /dev/null and b/resource/icon/png/32.png differ diff --git a/resource/icon/png/512.png b/resource/icon/png/512.png new file mode 100644 index 0000000..6a3f0f9 Binary files /dev/null and b/resource/icon/png/512.png differ diff --git a/resource/icon/png/64.png b/resource/icon/png/64.png new file mode 100644 index 0000000..f4963bb Binary files /dev/null and b/resource/icon/png/64.png differ diff --git a/script/setup.cmd b/script/setup.cmd new file mode 100644 index 0000000..bfbfa42 --- /dev/null +++ b/script/setup.cmd @@ -0,0 +1,21 @@ +@echo off +set dir=%~dp0 +cd %dir% +schtasks /create /tn "Dislocker_Client" /tr "%dir%dislocker_client.exe" /sc onlogon /ru "%USERNAME%" /rl highest /f +if %ERRORLEVEL% == 0 ( + echo ^XNXPW[[̐ݒ͊܂B +) +if %ERRORLEVEL% == 1 ( + echo ^XNXPW[[̐ݒŃG[܂B +) +Cscript "%dir%shortcut.vbs" "%dir%" "%dir%" "dislocker_client.exe" "stop" "Vbg_Eƒ~" +if %ERRORLEVEL% == 0 ( + Cscript "%dir%shortcut.vbs" "%USERPROFILE%/Desktop" "%dir%" "dislocker_client.exe" "stop" "Vbg_Eƒ~" + echo V[gJbg̍쐬͊܂B +) +if %ERRORLEVEL% == 1 ( + echo V[gJbg̍쐬ŃG[܂B +) +set /P pc_number=PCԍ +start %dir%dislocker_client.exe setup %pc_number% +pause \ No newline at end of file diff --git a/script/shortcut.vbs b/script/shortcut.vbs new file mode 100644 index 0000000..2a7a56e --- /dev/null +++ b/script/shortcut.vbs @@ -0,0 +1,8 @@ +Set WshShell = CreateObject("WScript.Shell") +Set args = Wscript.Arguments +Set Shortcut = WshShell.CreateShortcut(args(0) & "\Vbg_EƏI.lnk") +Shortcut.TargetPath = args(1) & args(2) +Shortcut.Arguments = args(3) +Shortcut.WorkingDirectory = args(1) +Shortcut.Save +WScript.Quit(0) \ No newline at end of file diff --git a/temp/client_playground.py b/temp/client_playground.py new file mode 100644 index 0000000..f0a4879 --- /dev/null +++ b/temp/client_playground.py @@ -0,0 +1,40 @@ +import os +import subprocess +import shutil + +def delete_appdata(**kwargs): + process_name = kwargs["process_name"] + dir_path = kwargs["dir_path"] + + if not os.path.exists(dir_path): + print(f"エラー: 指定されたディレクトリ {dir_path} が存在しません。") + return 1 + + try: + # プロセスの終了 + subprocess.run(['taskkill', '/f', '/t', '/im', process_name]) + print(f"{process_name} を終了しました。") + + # ディレクトリの削除 + shutil.rmtree(dir_path) + print(f"{dir_path} を削除しました。") + + return 0 + except subprocess.CalledProcessError as e: + print(f"プロセス終了エラー: {e}") + return 1 + except PermissionError as e: + print(f"権限エラー: {e}") + return 1 + except Exception as error: + print("エラーが発生しました。\nエラー内容:") + print(f"エラータイプ: {error.__class__.__name__}") + print(f"エラー引数: {error.args}") + print(f"エラーメッセージ: {str(error)}") + return 1 + +appdata_local = os.path.expandvars("%LOCALAPPDATA%") +appdata_roaming = os.path.expandvars("%APPDATA%") +print(appdata_local, appdata_roaming) +print(f"{appdata_local}\\Steam") +steam_del = delete_appdata(process_name="steam.exe", dir_path=f"{appdata_local}\\Steam") \ No newline at end of file diff --git a/temp/csv_test.py b/temp/csv_test.py new file mode 100644 index 0000000..9b30aa8 --- /dev/null +++ b/temp/csv_test.py @@ -0,0 +1,77 @@ +import psycopg2 +import csv +from psycopg2 import sql +from datetime import datetime + +def format_datetime(value): + if isinstance(value, datetime): + return value.strftime('%Y-%m-%d %H:%M:%S') + return value + +def export_table_to_csv(host, port, database, user, password, main_table, related_table, related_column, csv_file_path): + try: + # データベースに接続 + conn = psycopg2.connect( + host=host, + port=port, + database=database, + user=user, + password=password + ) + + cur = conn.cursor() + + # メインテーブルの列情報を取得(user_idを除く) + cur.execute(sql.SQL("SELECT * FROM {} LIMIT 0").format(sql.Identifier(main_table))) + main_columns = [desc[0] for desc in cur.description if desc[0] != 'member_id'] + + # クエリを作成(列名を明確に指定) + query = sql.SQL(""" + SELECT {main_columns}, {related_table}.name + FROM {main_table} + LEFT JOIN {related_table} ON {main_table}.member_id = {related_table}.id + """).format( + main_columns=sql.SQL(', ').join([sql.SQL("{}.{}").format(sql.Identifier(main_table), sql.Identifier(col)) for col in main_columns]), + main_table=sql.Identifier(main_table), + related_table=sql.Identifier(related_table) + ) + + cur.execute(query) + + # 列名を再構成(nameを2番目に配置) + column_names = [main_columns[0], 'name'] + main_columns[1:] + + rows = cur.fetchall() + + with open(csv_file_path, 'w', newline='', encoding='utf-8-sig') as csvfile: + csvwriter = csv.writer(csvfile) + csvwriter.writerow(column_names) + + for row in rows: + # nameを2番目に移動 + formatted_row = [format_datetime(row[0])] + [row[-1]] + [format_datetime(field) if field is not None else '' for field in row[1:-1]] + csvwriter.writerow(formatted_row) + + print(f"テーブル '{main_table}' の内容を '{csv_file_path}' に出力しました。") + + except (Exception, psycopg2.Error) as error: + print("エラーが発生しました:", error) + + finally: + if conn: + cur.close() + conn.close() + print("データベース接続を閉じました。") + +# 使用例 +host = "192.168.1.220" +port = "12245" +database = "dislocker" +user = "suti7" +password = "Testing1234" +main_table = "pc_usage_history" +related_table = "club_member" +related_column = "name" # 例: 登録者の名前を表示したい場合 +csv_file_path = "output.csv" + +export_table_to_csv(host, port, database, user, password, main_table, related_table, related_column, csv_file_path) diff --git a/temp/xcel.py b/temp/xcel.py new file mode 100644 index 0000000..684cfc7 --- /dev/null +++ b/temp/xcel.py @@ -0,0 +1,97 @@ +import psycopg2 +import csv +from psycopg2 import sql +from datetime import datetime +from openpyxl import Workbook +from openpyxl.utils import get_column_letter + +def format_datetime(value): + if isinstance(value, datetime): + return value.strftime('%Y-%m-%d %H:%M:%S') + return value + +def export_table_to_excel(host, port, database, user, password, main_table, related_table, excel_file_path): + try: + conn = psycopg2.connect( + host=host, + port=port, + database=database, + user=user, + password=password + ) + + cur = conn.cursor() + + # メインテーブルの列情報を取得(member_idを除く) + cur.execute(sql.SQL("SELECT * FROM {} LIMIT 0").format(sql.Identifier(main_table))) + main_columns = [desc[0] for desc in cur.description if desc[0] != 'member_id'] + + # クエリを作成(列名を明確に指定) + query = sql.SQL(""" + SELECT {main_columns}, {related_table}.name + FROM {main_table} + LEFT JOIN {related_table} ON {main_table}.member_id = {related_table}.id + """).format( + main_columns=sql.SQL(', ').join([sql.SQL("{}.{}").format(sql.Identifier(main_table), sql.Identifier(col)) for col in main_columns]), + main_table=sql.Identifier(main_table), + related_table=sql.Identifier(related_table) + ) + + cur.execute(query) + + # 列名を再構成(nameを2番目に配置) + column_names = [main_columns[0], 'name'] + main_columns[1:] + + rows = cur.fetchall() + + # Excelワークブックを作成 + wb = Workbook() + ws = wb.active + + # 列名を書き込み + ws.append(column_names) + + # データを書き込み + for row in rows: + # nameを2番目に移動 + formatted_row = [format_datetime(row[0])] + [row[-1]] + [format_datetime(field) if field is not None else '' for field in row[1:-1]] + ws.append(formatted_row) + + # 列幅を自動調整 + for col in ws.columns: + max_length = 0 + column = col[0].column_letter + for cell in col: + try: + if len(str(cell.value)) > max_length: + max_length = len(str(cell.value)) + except: + pass + adjusted_width = (max_length + 2) * 1.2 + ws.column_dimensions[column].width = adjusted_width + + # Excelファイルを保存 + wb.save(excel_file_path) + + print(f"テーブル '{main_table}' の内容を '{excel_file_path}' に出力しました。") + + except (Exception, psycopg2.Error) as error: + print("エラーが発生しました:", error) + + finally: + if conn: + cur.close() + conn.close() + print("データベース接続を閉じました。") + +# 使用例 +host = "192.168.1.220" +port = "12245" +database = "dislocker" +user = "suti7" +password = "Testing1234" +main_table = "pc_usage_history" +related_table = "club_member" +excel_file_path = "output.xlsx" + +export_table_to_excel(host, port, database, user, password, main_table, related_table, excel_file_path)