├── tests ├── __init__.py ├── pytest │ └── __init__.py └── unittest │ └── __init__.py ├── cog ├── core │ ├── __init__.py │ ├── singleton.py │ ├── secret.py │ ├── safe_write.py │ ├── sendgift.py │ ├── downtime.py │ ├── sql.py │ └── sql_abstract.py ├── tests │ ├── __init__.py │ ├── test_user_record.py │ └── test_daily_charge.py ├── __init__.py ├── admin.py ├── rule_role.py ├── voice_chat.py ├── check_point.py ├── api │ ├── gift.py │ └── api.py ├── version_info.py ├── game.py ├── class_role.py ├── admin_gift.py ├── ticket.py ├── daily_charge.py └── comment.py ├── uwu.png ├── static ├── 404.jpg ├── bot.png ├── slot.png ├── zap.png ├── oauth.png ├── switch-btn.css ├── switch-btn.js ├── slot-machine.svg ├── slot.svg ├── table_struct.sql └── style.css ├── .gitattributes ├── HISTORY.md ├── test ├── README.md ├── enumstruct.py ├── api_request.py ├── emoji.py └── ctf_get.py ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── notion.yml │ ├── pylint.yml │ ├── unittest.yml │ └── black.yml ├── database ├── downtime.json ├── slot.json ├── server.config.json ├── server.config-alpha.json ├── courses.json └── products.json ├── SECURITY.md ├── generate_secrets.py ├── requirements_dev.txt ├── requirements.txt ├── docs ├── database.html ├── abstract_schema_products.json ├── abstract_schema_table.json └── database_layout.html ├── .coderabbit.yaml ├── .env.example ├── .gitignore ├── templates ├── already.html ├── star_success.html ├── 404.html └── home.html ├── main.py ├── pyproject.toml ├── channel_check.py ├── README_zh-Hant.md ├── CONTRIBUTING.md ├── README.md ├── LICENSE └── RELEASE-NOTES-0.1.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cog/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cog/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pytest/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unittest/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cog/__init__.py: -------------------------------------------------------------------------------- 1 | def setup(bot): 2 | pass 3 | -------------------------------------------------------------------------------- /uwu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SCAICT/SCAICT-uwu/HEAD/uwu.png -------------------------------------------------------------------------------- /static/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SCAICT/SCAICT-uwu/HEAD/static/404.jpg -------------------------------------------------------------------------------- /static/bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SCAICT/SCAICT-uwu/HEAD/static/bot.png -------------------------------------------------------------------------------- /static/slot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SCAICT/SCAICT-uwu/HEAD/static/slot.png -------------------------------------------------------------------------------- /static/zap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SCAICT/SCAICT-uwu/HEAD/static/zap.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html eol=lf 2 | *.json eol=lf 3 | *.py eol=lf 4 | *.toml eol=lf 5 | -------------------------------------------------------------------------------- /static/oauth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SCAICT/SCAICT-uwu/HEAD/static/oauth.png -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | Change notes from older releases. For current info, see RELEASE-NOTES-0.1. 4 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # 關於這個資料夾 2 | ## 各種和機器人執行時無關但需要事先執行測試的程式 3 | ### 為什麼要有這個資料夾? 4 | - 我要做 Git ,但這些東西散落在專案根目錄底下很亂 5 | 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # If you have any resources like to share with us, feel free to contact us! 2 | 3 | custom: [ 'mailto:contact@scaict.org', 'https://scaict.org' ] 4 | -------------------------------------------------------------------------------- /database/downtime.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "start": "2025-03-14 09:03:00+0800", 4 | "end": "2025-04-12 16:26:35+0800", 5 | "is_restored": true 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security information 2 | 3 | SCAICT-uwu takes security very seriously. 4 | If you believe you have found a security issue, please report it at 5 | . 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 許願池 Feature request 3 | about: 給點建議 Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 你想怎樣? What do you want? 11 | 12 | ## 為什麼? Why? 13 | -------------------------------------------------------------------------------- /generate_secrets.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Generate a random byte string 4 | random_bytes = os.urandom(24) 5 | 6 | # Convert the byte string to a hexadecimal string 7 | secret_key = random_bytes.hex() 8 | 9 | # Print the secret key 10 | print(secret_key) 11 | -------------------------------------------------------------------------------- /cog/core/singleton.py: -------------------------------------------------------------------------------- 1 | class SingletonMeta(type): 2 | _instances = {} 3 | 4 | def __call__(cls, *args, **kwargs): 5 | if cls not in cls._instances: 6 | instance = super().__call__(*args, **kwargs) 7 | cls._instances[cls] = instance 8 | return cls._instances[cls] 9 | -------------------------------------------------------------------------------- /test/enumstruct.py: -------------------------------------------------------------------------------- 1 | # enum 測試 2 | from enum import Enum 3 | 4 | 5 | class GiftType(Enum): 6 | point = "電電點" 7 | ticket = "抽獎券" 8 | 9 | 10 | for gt in GiftType: # equal to print(GiftType.{item}.name) 11 | print(gt) 12 | print(GiftType.point.name) 13 | 14 | print(GiftType.point.value) 15 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | astroid == 4.0.2 2 | black == 25.11.0 3 | click == 8.3.1 4 | colorama == 0.4.6 5 | dill == 0.4.0 6 | iniconfig == 2.3.0 7 | isort == 7.0.0 8 | mccabe == 0.7.0 9 | mypy-extensions == 1.1.0 10 | packaging == 25.0 11 | pathspec == 0.12.1 12 | platformdirs == 4.5.0 13 | pluggy == 1.6.0 14 | pylint == 4.0.4 15 | pytest == 9.0.1 16 | tomlkit == 0.13.3 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | open-pull-requests-limit: 10 9 | target-branch: "dev" 10 | - package-ecosystem: "pip" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | open-pull-requests-limit: 10 15 | target-branch: "dev" 16 | -------------------------------------------------------------------------------- /database/slot.json: -------------------------------------------------------------------------------- 1 | { 2 | "population": [ 3 | "all same", 4 | "all different", 5 | "one pair", 6 | "lightning" 7 | ], 8 | "weights": [ 9 | 10, 10 | 20, 11 | 20, 12 | 30 13 | ], 14 | "get": { 15 | "all same": 20, 16 | "all different": 5, 17 | "one pair": 10, 18 | "lightning": 50 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/notion.yml: -------------------------------------------------------------------------------- 1 | name: Sync issues to Notion 2 | 3 | on: 4 | issues: 5 | types: [opened, edited, deleted, reopened, closed] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | sync: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Notion GitHub Issues Automation 13 | uses: Edit-Mr/GitHub-issue-2-Notion@main 14 | with: 15 | repo: ${{ github.repository }} 16 | NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }} 17 | NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }} 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohappyeyeballs == 2.6.1 2 | aiohttp == 3.13.2 3 | aiosignal == 1.4.0 4 | attrs == 25.4.0 5 | blinker == 1.9.0 6 | certifi == 2025.11.12 7 | charset-normalizer == 3.4.4 8 | click == 8.3.1 9 | colorama == 0.4.6 10 | flask == 3.1.2 11 | frozenlist == 1.8.0 12 | idna == 3.11 13 | itsdangerous == 2.2.0 14 | jinja2 == 3.1.6 15 | markupsafe == 3.0.3 16 | multidict == 6.7.0 17 | mysql-connector-python == 9.5.0 18 | propcache == 0.4.1 19 | py-cord == 2.6.1 20 | python-dotenv == 1.2.1 21 | requests == 2.32.5 22 | urllib3 == 2.5.0 23 | werkzeug == 3.1.4 24 | yarl == 1.22.0 25 | -------------------------------------------------------------------------------- /cog/core/secret.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | import os 3 | 4 | # Third-party imports 5 | from dotenv import load_dotenv 6 | import mysql.connector 7 | 8 | 9 | load_dotenv(f"{os.getcwd()}/.env") 10 | DB_USER = os.getenv("MYSQL_USER") 11 | DB_PASSWORD = os.getenv("MYSQL_PASSWORD") 12 | DB_NAME = os.getenv("MYSQL_DATABASE") 13 | DB_HOST = os.getenv("HOST") 14 | DB_PORT = os.getenv("MYSQL_PORT") 15 | 16 | 17 | def connect(): 18 | return mysql.connector.connect( 19 | user=DB_USER, 20 | password=DB_PASSWORD, 21 | database=DB_NAME, 22 | host=DB_HOST, 23 | port=DB_PORT, 24 | ) 25 | -------------------------------------------------------------------------------- /docs/database.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SCAICT-uwu SQL table 7 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/api_request.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | import json 3 | import os 4 | 5 | # Third-party imports 6 | from dotenv import load_dotenv 7 | import requests 8 | 9 | load_dotenv(f"{os.getcwd()}/.env", verbose=True, override=True) 10 | guild_id = os.getenv("GUILD_ID") 11 | api_key = os.getenv("DISCORD_TOKEN") 12 | headers = { 13 | "Authorization": f"Bot {api_key}", 14 | "Content-Type": "application/json", 15 | } 16 | url = f"https://discord.com/api/v10/guilds/{guild_id}/members/898141506588770334" 17 | response = requests.get(url, headers=headers, timeout=5) 18 | 19 | formatted_json = json.dumps(response.json(), indent=4) 20 | print(formatted_json) 21 | -------------------------------------------------------------------------------- /test/emoji.py: -------------------------------------------------------------------------------- 1 | def analyze_string(s): 2 | elements = s.split() 3 | print(elements) 4 | unique_elements = set(elements) 5 | 6 | if len(unique_elements) > 2: 7 | print("超過兩種不同的元素,程式結束") 8 | return 9 | 10 | # 轉換元素為0和1 11 | element_map = {element: idx for idx, element in enumerate(unique_elements)} 12 | transformed_elements = [str(element_map[element]) for element in elements] 13 | 14 | # 將轉換後的元素拼接成字串 15 | transformed_string = "".join(transformed_elements) 16 | print(transformed_string) 17 | print(int(transformed_string, 2)) 18 | 19 | 20 | # 字串範例 21 | se = "1 0 3" 22 | 23 | analyze_string(se) 24 | -------------------------------------------------------------------------------- /cog/admin.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | # import csv 3 | # from datetime import datetime, timedelta 4 | # import json 5 | # import os 6 | 7 | # Third-party imports 8 | import discord 9 | from build.build import Build 10 | 11 | # Local imports 12 | 13 | 14 | class ManagerCommand(Build): 15 | @discord.slash_command(name="reload", description="你是管理員才讓你用") 16 | async def reload(self, ctx, package): 17 | if not ctx.author.guild_permissions.administrator: 18 | await ctx.respond("你沒有權限使用這個指令!", ephemeral=True) 19 | return 20 | self.bot.reload_extension(f"cog.{package}") 21 | await ctx.respond(f"🔄 {package} reloaded") 22 | 23 | 24 | def setup(bot): 25 | bot.add_cog(ManagerCommand(bot)) 26 | -------------------------------------------------------------------------------- /database/server.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "SCAICT-alpha": { 3 | "channel": { 4 | "serverID": 959823904266944562, 5 | "memberCount": 1215645338522755123, 6 | "pointCount": 1217447569978822727, 7 | "everyDayCharge": 1215248009097383956, 8 | "commandChannel": 1215248945601712158, 9 | "countChannel": 1219615518974148638, 10 | "colorChannel": 1225869655093284927, 11 | "exclude_point": [1219615518974148638,1225869655093284927] 12 | }, 13 | "SP-role": { 14 | "CTF_Maker": 1215248450502008832 15 | }, 16 | "stickers": { 17 | "zap": "<:zap:1215646859067138128>" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/server.config-alpha.json: -------------------------------------------------------------------------------- 1 | { 2 | "SCAICT-alpha": { 3 | "channel": { 4 | "serverID": 1203338928535379978, 5 | "memberCount": 1206548816552271873, 6 | "pointCount": 1206549021355810816, 7 | "everyDayCharge": 1206549724530999356, 8 | "commandChannel": 1206550021353242654, 9 | "countChannel": 1220130492821667973, 10 | "colorChannel": 1225850293762134036, 11 | "exclude_point":[1220130492821667973,1225850293762134036] 12 | }, 13 | "SP-role": { 14 | "CTF_Maker": 1210935361467977738 15 | }, 16 | "stickers": { 17 | "zap": "" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.coderabbit.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json 2 | language: "en-US" 3 | early_access: false 4 | tone_instructions: "You are a smart cat" 5 | reviews: 6 | profile: "assertive" 7 | path_instructions: 8 | - path: "templates/*.html" 9 | instructions: 10 | "All text should follow sparanoid/chinese-copywriting-guidelines. There should be space between English and Chinese." 11 | request_changes_workflow: false 12 | high_level_summary: true 13 | poem: true 14 | review_status: true 15 | collapse_walkthrough: false 16 | auto_review: 17 | enabled: true 18 | drafts: true 19 | base_branches: 20 | - main 21 | - development 22 | chat: 23 | auto_reply: true 24 | -------------------------------------------------------------------------------- /cog/tests/test_user_record.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mysql.connector.errors import Error as MySQLError 4 | 5 | from cog.core.sql import mysql_connection 6 | from cog.core.sql_abstract import UserRecord 7 | 8 | 9 | YUEVUWU = 545234619729969152 10 | 11 | skip = False 12 | 13 | try: 14 | with mysql_connection() as _: 15 | pass 16 | except (RuntimeError, MySQLError, TypeError): 17 | skip = True 18 | 19 | 20 | class TestFromSQL(unittest.TestCase): 21 | @unittest.skipIf(skip, "Failed to connect to database.") 22 | def test_yuevuwu_exist(self): 23 | data = UserRecord.from_sql(YUEVUWU) 24 | self.assertIsNotNone(data) 25 | print(data) 26 | 27 | 28 | if __name__ == "__main__": 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 回報彩蛋 "Feature" report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **問題說明 Describe the feature** 11 | 你有什麼問題? A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | 重現錯誤的步驟 Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | 你想要怎樣? A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | 若你覺得有幫助可以截圖 If applicable, add screenshots to help explain your problem. 26 | 27 | **Additional context** 28 | 還有什麼想說的? Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 這是一個範例檔案,請複製一份 .env.example 並命名為 .env 2 | # 並將以下的設定值填入 3 | 4 | # Database configuration 5 | MYSQL_USER=資料庫使用者 6 | MYSQL_PASSWORD=資料庫使用者密碼 7 | MYSQL_PORT=資料庫連接埠 8 | MYSQL_DATABASE=資料庫名稱 9 | HOST=資料庫主機位址 10 | 11 | # Global configuration 12 | DISCORD_TOKEN=Discord機器人token 13 | GUILD_ID=機器人所在伺服器ID 14 | 15 | # Flask configuration 16 | SECRET_KEY=隨機字串,給 flask session 使用 17 | DISCORD_CLIENT_ID=Discord Client ID 18 | DISCORD_CLIENT_SECRET=Discord Client Secret,到 Discord Developer Portal 取得 19 | DISCORD_REDIRECT_URI=商店網址/callback 20 | GITHUB_CLIENT_ID=GitHub Client ID,到 GitHub Developer 取得 21 | GITHUB_CLIENT_SECRET=GitHub Client Secret 22 | GITHUB_REDIRECT_URI=GitHub OAuth Redirect URI 23 | GITHUB_DISCORD_REDIRECT_URI= 24 | SEND_GIFT_ROLE=允許發送禮物的身分組ID,多個身分組ID以逗號分隔 25 | -------------------------------------------------------------------------------- /cog/core/safe_write.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from contextlib import contextmanager 4 | 5 | 6 | @contextmanager 7 | def safe_open_w(file: str | os.PathLike[str], *, encoding: str | None): 8 | dirpath, basename = os.path.split(file) 9 | 10 | fd, tmp_path = tempfile.mkstemp(prefix=f"{basename}.tmp_", dir=dirpath) 11 | 12 | try: 13 | with os.fdopen(fd, "w", encoding=encoding) as tmp_file: 14 | yield tmp_file 15 | 16 | os.replace(tmp_path, file) 17 | 18 | except Exception: 19 | os.remove(tmp_path) 20 | raise 21 | 22 | 23 | def safe_write( 24 | file: str | os.PathLike[str], 25 | data: str, 26 | encoding: str | None, 27 | ): 28 | 29 | with safe_open_w(file, encoding=encoding) as f: 30 | f.write(data) 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editors 2 | *~ 3 | \#*# 4 | .#* 5 | .*.swp 6 | .project 7 | *.orig 8 | ## Visual Studio Code 9 | *.vscode/ 10 | 11 | # Install & usage 12 | ## Python 13 | **/__pycache__ 14 | *.egg-info/ 15 | build/lib/ 16 | ## Linux 17 | flakLog.out 18 | nohup.out 19 | pointLog.out 20 | ## Configuration files 21 | .env 22 | # python 虛擬環境 23 | bin/ 24 | envuwu/ 25 | env-uwu/ 26 | lib/ 27 | lib64 28 | pyvenv.cfg 29 | ### Ignore token 30 | ### 如果你的token.json已經被追蹤可以執行: 31 | ### 1. git rm --cached token.json 32 | ### 2. git update-index --assume-unchanged token.json 33 | ### 這兩個指令 34 | token.json 35 | 36 | # Testing 37 | **/test.py 38 | 39 | # Operating systems 40 | ## Mac OS X 41 | .DS_Store 42 | ## Windows 43 | Thumbs.db 44 | 45 | # Misc 46 | read.txt 47 | 48 | # Docker 49 | docker-compose.override.yml 50 | -------------------------------------------------------------------------------- /database/courses.json: -------------------------------------------------------------------------------- 1 | { 2 | "akesel": { 3 | "name": "113 工人身分組", 4 | "theme": "959827031326081184", 5 | "teacher": "empty", 6 | "time": "2024~2025" 7 | }, 8 | "6OKhfUt3bMo": { 9 | "name": "11月主題課程", 10 | "theme": "PHP後端網頁設計", 11 | "teacher": ".xiulan", 12 | "time": "11/23 & 11/24 19:00-21:00" 13 | }, 14 | "6": { 15 | "name": "12月主題課程", 16 | "theme": "12月主題課程 - 基礎資料結構", 17 | "teacher": "summer", 18 | "time": "2024/12/28" 19 | }, 20 | "3BwzKrSU": { 21 | "name": "12月主題課程", 22 | "theme": "12月主題課程 - 基礎資料結構", 23 | "teacher": "summer", 24 | "time": "2024/12/28" 25 | }, 26 | "FebruaryClass": { 27 | "name": "2月主題課程", 28 | "theme": "2月主題課程-基礎演算法", 29 | "teacher": "kuei", 30 | "time": "2025/02/23" 31 | } 32 | } -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: 4 | pull_request: 5 | push: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == true 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.11"] 15 | steps: 16 | - uses: actions/checkout@v6 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v6 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | pip install -r requirements_dev.txt 26 | - name: Analysing the code with Pylint 27 | run: | 28 | pylint $(git ls-files '*.py') 29 | -------------------------------------------------------------------------------- /.github/workflows/unittest.yml: -------------------------------------------------------------------------------- 1 | name: Python unittest 2 | 3 | on: 4 | pull_request: 5 | push: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == true) 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.11"] 15 | steps: 16 | - uses: actions/checkout@v6 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v6 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | pip install -r requirements_dev.txt 26 | - name: Analysing the code with Python unittest 27 | run: | 28 | python -m unittest $(git ls-files 'cog/tests/*.py') 29 | python -m unittest $(git ls-files 'tests/unittest/*.py') 30 | -------------------------------------------------------------------------------- /templates/already.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 成功! 8 | 31 | 32 | 33 | 34 |

你已經進行過此操作囉!

35 |

你可以關閉此分頁

36 | 37 | 38 | -------------------------------------------------------------------------------- /templates/star_success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 成功! 8 | 31 | 32 | 33 | 34 |

帳戶綁定成功!

35 |

感謝你喜歡中電喵❤️

36 |

你可以關閉此分頁

37 | 38 | 39 | -------------------------------------------------------------------------------- /test/ctf_get.py: -------------------------------------------------------------------------------- 1 | # 把舊 CTF JSON 格式檔案的資料搬進資料庫,執行時拿到資料夾根木錄才能正確import SQL 2 | # Standard imports 3 | import json 4 | 5 | # Local imports 6 | from cog.core.sql import link_sql 7 | from cog.core.sql import end 8 | 9 | with open("./database/ctf.json", "r", encoding="utf-8") as file: 10 | # code to read and process the file goes here 11 | file = json.load(file) 12 | connection, cursor = link_sql() 13 | cursor.execute("USE `CTF`") 14 | 15 | for questionId, ctf in file.items(): 16 | # questionId 是題目ID 17 | # ctf 包含該題目的所有屬性,等等再用鍵值分離出來 18 | # cursor.execute(f"INSERT INTO `data`(id,flags,score,restrictions,message_id,case_status,start_time,end_time,title,tried) VALUES({questionId},'{ctf['flag']}',{ctf['score']},'{ctf['limit']}',{ctf['messageId']},{ctf['case']},'{ctf['start']}','{ctf['end']}','{ctf['title']}','{ctf['tried']}');") 19 | for h in ctf["history"]: 20 | solved = 1 if int(h) in ctf["solved"] else 0 21 | cursor.execute( 22 | f"INSERT INTO `history`(data_id,uid,count,solved) VALUES('{questionId}',{h},{ctf['history'][h]},{solved});" 23 | ) 24 | 25 | end(connection, cursor) 26 | break 27 | -------------------------------------------------------------------------------- /cog/rule_role.py: -------------------------------------------------------------------------------- 1 | # Third-party imports 2 | import discord 3 | from discord.ext import commands 4 | 5 | # Local imports 6 | from build.build import Build 7 | 8 | 9 | class RuleRoles(Build): 10 | # 當使用者按下表情符號 -> 領取身分組 11 | @commands.Cog.listener() 12 | async def on_raw_reaction_add(self, payload): 13 | # 取得反應的資訊 14 | guild = self.bot.get_guild(payload.guild_id) 15 | member = guild.get_member(payload.user_id) 16 | emoji = payload.emoji.name 17 | 18 | # 檢查是否為指定的訊息和 emoji 19 | if payload.message_id == 1208097539820232734 and emoji == "⚡": 20 | # 給予身分組 21 | role = discord.utils.get(guild.roles, name="二月主題課程") 22 | await member.add_roles(role) 23 | 24 | # 當使用者收回表情符號 -> 取消身分組 25 | @commands.Cog.listener() 26 | async def on_raw_reaction_remove(self, payload): 27 | # 取得反應的資訊 28 | guild = self.bot.get_guild(payload.guild_id) 29 | member = guild.get_member(payload.user_id) 30 | emoji = payload.emoji.name 31 | 32 | # 檢查是否為指定的 emoji 33 | if payload.message_id == 1208097539820232734 and emoji == "⚡": 34 | # 移除身分組 35 | role = discord.utils.get(guild.roles, name="二月主題課程") 36 | await member.remove_roles(role) 37 | 38 | 39 | def setup(bot): 40 | bot.add_cog(RuleRoles(bot)) 41 | -------------------------------------------------------------------------------- /static/switch-btn.css: -------------------------------------------------------------------------------- 1 | 2 | .switch-button { 3 | width: 400px; 4 | height: 40px; 5 | text-align: center; 6 | position: absolute; 7 | left: 50%; 8 | top: 25%; 9 | transform: translate3D(-50%, -50%, 0); 10 | will-change: transform; 11 | z-index: 197 !important; 12 | cursor: pointer; 13 | transition: .3s ease all; 14 | border: 1px solid white; 15 | } 16 | 17 | .switch-button-case { 18 | display: inline-block; 19 | background: none; 20 | width: 49%; 21 | height: 100%; 22 | color: white; 23 | position: relative; 24 | border: none; 25 | transition: .3s ease all; 26 | text-transform: uppercase; 27 | letter-spacing: 5px; 28 | padding-bottom: 1px; 29 | font-weight: 700; 30 | } 31 | 32 | .switch-button-case:hover { 33 | color: grey; 34 | cursor: pointer; 35 | } 36 | 37 | .switch-button-case:focus { 38 | outline: none; 39 | } 40 | 41 | .active { 42 | color: #151515; 43 | background-color: white; 44 | position: absolute; 45 | left: 0; 46 | top: 0; 47 | width: 50%; 48 | height: 100%; 49 | z-index: -1; 50 | transition: .3s ease-out all; 51 | } 52 | 53 | .active-case { 54 | color: #151515; 55 | } 56 | 57 | .signature { 58 | position: fixed; 59 | font-family: sans-serif; 60 | font-weight: 100; 61 | bottom: 10px; 62 | left: 0; 63 | letter-spacing: 4px; 64 | font-size: 10px; 65 | width: 100vw; 66 | text-align: center; 67 | color: white; 68 | text-transform: uppercase; 69 | text-decoration: none; 70 | } -------------------------------------------------------------------------------- /cog/voice_chat.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | import asyncio 3 | 4 | # Third-party imports 5 | import discord 6 | from discord.ext import commands 7 | 8 | # Local imports 9 | from build.build import Build 10 | 11 | 12 | # 建立動態語音頻道 13 | class VoiceChat(Build): 14 | async def check_and_delete_empty_channel(self, voice_channel): 15 | while voice_channel.members: 16 | # 持續 loop 直到沒有人在頻道裡 17 | await asyncio.sleep(20) 18 | await voice_channel.delete() 19 | 20 | @commands.Cog.listener() 21 | async def on_voice_state_update(self, member, before, after): 22 | target_voice_channel_name = "創建語音" 23 | target_category_name = "----------動態語音頻道----------" 24 | if ( 25 | after.channel 26 | and after.channel != before.channel 27 | and after.channel.name == target_voice_channel_name 28 | ): 29 | guild = after.channel.guild 30 | category = discord.utils.get(guild.categories, name=target_category_name) 31 | 32 | new_channel = await guild.create_voice_channel( 33 | f"{member.name}的頻道", category=category 34 | ) 35 | 36 | await member.move_to(new_channel) 37 | 38 | # await self.check_and_delete_empty_channel(new_channel) 39 | self.bot.loop.create_task(self.check_and_delete_empty_channel(new_channel)) 40 | 41 | 42 | def setup(bot): 43 | bot.add_cog(VoiceChat(bot)) 44 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | import os 3 | 4 | # Third-party imports 5 | import discord 6 | from dotenv import load_dotenv 7 | 8 | # Local imports 9 | from channel_check import update_channel # update_channel程式從core目錄底下引入 10 | from channel_check import change_status # update_channel程式從core目錄底下引入 11 | from cog.daily_charge import Charge 12 | 13 | intt = discord.Intents.default() 14 | intt.members = True 15 | intt.message_content = True 16 | bot = discord.Bot(intents=intt) 17 | 18 | 19 | for filename in os.listdir(f"{os.getcwd()}/cog"): 20 | if filename.endswith(".py"): 21 | bot.load_extension(f"cog.{filename[:-3]}") 22 | print(f"📖 {filename} loaded") # test 23 | 24 | 25 | @bot.command() 26 | async def load(ctx, extension): 27 | bot.load_extension(f"cog.{extension}") 28 | await ctx.send(f"📖 {extension} loaded") 29 | 30 | 31 | @bot.command() 32 | async def unload(ctx, extension): 33 | bot.unload_extension(f"cog.{extension}") 34 | await ctx.send(f"📖 {extension} unloaded") 35 | 36 | 37 | @bot.event 38 | async def on_ready(): 39 | print(f"✅ {bot.user} is online") 40 | 41 | bot.loop.create_task(update_channel(bot)) 42 | bot.loop.create_task(change_status(bot)) 43 | bot.loop.create_task(Charge(bot).restore_downtime_point()) 44 | 45 | 46 | if __name__ == "__main__": 47 | load_dotenv(f"{os.getcwd()}/.env", verbose=True, override=True) 48 | bot_token = os.getenv("DISCORD_TOKEN") 49 | bot.run(bot_token) 50 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 找不到頁面 13 | 39 | 40 | 41 | 42 |

哎呀!您所查找的頁面不存在,請檢查網址或返回首頁。

43 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /cog/core/sendgift.py: -------------------------------------------------------------------------------- 1 | # Third-party imports 2 | import discord 3 | 4 | from cog.core.sql import link_sql, end 5 | 6 | 7 | class MessageSendError(Exception): # 自定義的例外類型 8 | pass 9 | 10 | 11 | class DBError(Exception): # 自定義的例外類型 12 | pass 13 | 14 | 15 | async def send_gift_button( 16 | self, target_user: discord.User, gift_type: str, count: int, sender: int 17 | ) -> None: 18 | # 產生按鈕物件 19 | view = self.Gift() 20 | view.type = gift_type 21 | view.count = count 22 | embed = discord.Embed( 23 | title=f"你收到了 {count} {gift_type}!", 24 | description=":gift:", 25 | color=discord.Color.blurple(), 26 | ) 27 | 28 | async def record_db( 29 | btn_id: int, gift_type: str, count: int, recipient: str 30 | ) -> None: 31 | try: 32 | connection, cursor = link_sql() 33 | cursor.execute( 34 | "INSERT INTO `gift`(`btnID`, `type`, `count`, `recipient`,`sender`) VALUES (%s, %s, %s, %s,%s)", 35 | (btn_id, gift_type, count, recipient, sender), 36 | ) 37 | end(connection, cursor) 38 | except Exception as e: 39 | end(connection, cursor) 40 | raise DBError("無法成功插入禮物資料進資料庫") from e 41 | 42 | try: 43 | await target_user.send(embed=embed) 44 | msg = await target_user.send(view=view) 45 | await record_db(msg.id, gift_type, count, target_user.name) 46 | except discord.Forbidden as exc: 47 | raise MessageSendError( 48 | f"無法向使用者 {target_user.name} 傳送訊息,可能是因為他們關閉了 DM" 49 | ) from exc 50 | -------------------------------------------------------------------------------- /cog/check_point.py: -------------------------------------------------------------------------------- 1 | # Third-party imports 2 | import discord 3 | from discord.ext import commands 4 | 5 | # Local imports 6 | from cog.core.sql import read 7 | from cog.core.sql import link_sql 8 | from cog.core.sql import end 9 | 10 | 11 | class CheckPoint(commands.Cog): 12 | def __init__(self, bot): 13 | self.bot = bot 14 | self.embed = None 15 | 16 | async def send_message(self, point, combo, interaction): 17 | member = interaction.user.mention 18 | # mention the users 19 | 20 | self.embed = discord.Embed(color=0x14E15C) 21 | 22 | if interaction.user.avatar is not None: # 預設頭像沒有這個 23 | self.embed.set_thumbnail(url=str(interaction.user.avatar)) 24 | self.embed.add_field(name="\n", value="使用者:" + member, inline=False) 25 | self.embed.add_field(name="目前點數:" + str(point), value="\n", inline=False) 26 | self.embed.add_field(name="已連續充電:" + str(combo), value="\n", inline=False) 27 | self.embed.add_field( 28 | name="距離下次連續登入獎勵:" + str(combo + 7 - combo % 7), 29 | value="\n", 30 | inline=False, 31 | ) 32 | await interaction.response.send_message(embed=self.embed) 33 | 34 | @discord.slash_command(name="check_point", description="查看電電點") 35 | async def check(self, interaction): 36 | connection, cursor = link_sql() # SQL 會話 37 | user_id = interaction.user.id 38 | combo = read(user_id, "charge_combo", cursor) 39 | point = read(user_id, "point", cursor) 40 | await self.send_message(point, combo, interaction) 41 | end(connection, cursor) 42 | 43 | 44 | def setup(bot): 45 | bot.add_cog(CheckPoint(bot)) 46 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: Black 2 | 3 | on: 4 | pull_request: 5 | push: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == true 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.11"] 15 | steps: 16 | - if: github.event_name != 'pull_request' 17 | uses: actions/checkout@v6 18 | - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true 19 | uses: actions/checkout@v6 20 | with: 21 | ref: ${{ github.event.pull_request.head.ref }} 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v6 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -r requirements.txt 30 | pip install -r requirements_dev.txt 31 | - name: Formatting the code with Black 32 | run: | 33 | black $(git ls-files '*.py') 34 | - name: Git config 35 | run: | 36 | git config --local user.name "github-actions[bot]" 37 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 38 | - name: Git diff 39 | run: | 40 | git diff HEAD || true 41 | - name: Git add 42 | run: | 43 | git add * 44 | - name: Git commit & push 45 | run: | 46 | git diff-index --quiet HEAD || ( git commit -m "Format \"$(git show -s --format=%s)\" using Black" && git push ) 47 | -------------------------------------------------------------------------------- /static/switch-btn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var switchButton = document.querySelector('.switch-button'); 4 | var switchBtnRight = document.querySelector('.switch-button-case.right'); 5 | var switchBtnLeft = document.querySelector('.switch-button-case.left'); 6 | var activeSwitch = document.querySelector('.active'); 7 | 8 | // 決定要抽幾抽的表單 9 | var numDraws = document.getElementById('numDraws'); 10 | 11 | // 抓轉蛋機外觀元素,用來調整轉蛋機的外觀 12 | var st3 = document.getElementsByClassName('st3'); 13 | var st4 = document.getElementsByClassName('st4'); 14 | var st5 = document.getElementsByClassName('st5'); 15 | var st6 = document.getElementsByClassName('st6'); 16 | 17 | function changeMachine(color3, color4, color5, color6) { 18 | for (var i = 0; i < st3.length; i++) { 19 | st3[i].style.fill = color3; 20 | } 21 | 22 | for (var i = 0; i < st4.length; i++) { 23 | st4[i].style.fill = color4; 24 | } 25 | 26 | for (var i = 0; i < st5.length; i++) { 27 | st5[i].style.fill = color5; 28 | } 29 | 30 | for (var i = 0; i < st6.length; i++) { 31 | st6[i].style.fill = color6; 32 | } 33 | } 34 | 35 | // 左邊,單抽 36 | function switchLeft() { 37 | switchBtnRight.classList.remove('active-case'); 38 | switchBtnLeft.classList.add('active-case'); 39 | activeSwitch.style.left = '0%'; 40 | numDraws.value = 1; 41 | changeMachine('#1e90ff', '#00bfff', '#87cefa', '#4682b4'); 42 | } 43 | 44 | // 右邊,10連 45 | function switchRight() { 46 | switchBtnRight.classList.add('active-case'); 47 | switchBtnLeft.classList.remove('active-case'); 48 | activeSwitch.style.left = '50%'; 49 | numDraws.value = 10; 50 | changeMachine('#FF0000', '#FF4500', '#B22222', '#8B0000'); 51 | } 52 | 53 | switchBtnLeft.addEventListener('click', function() { 54 | switchLeft(); 55 | }, false); 56 | 57 | switchBtnRight.addEventListener('click', function() { 58 | switchRight(); 59 | }, false); 60 | -------------------------------------------------------------------------------- /static/slot-machine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 15 | 16 | 18 | 19 | 20 | 21 | 23 | 25 | 27 | 29 | 30 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | # Only use lowercase letters and hyphens here 3 | # @see https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#name 4 | # @see https://packaging.python.org/en/latest/specifications/name-normalization/ 5 | name = "scaict-uwu" 6 | # PEP 440 7 | # @see https://sethmlarson.dev/pep-440 8 | version = "0.1.13" 9 | description = "A cat living in SCAICT Discord server." 10 | readme.file = "README.md" 11 | readme.content-type = "text/markdown" 12 | requires-python = "== 3.11.*" 13 | license = "Apache-2.0" 14 | license-files = [ 15 | "LICENSE", 16 | ] 17 | # Use Git committer/author name here 18 | authors = [ 19 | { name = "ChiuDeYuan" }, 20 | { name = "Each Chen" }, 21 | { name = "Elvis Mao" }, 22 | { name = "g4o2" }, 23 | { name = "Larryeng" }, 24 | { name = "osga24" }, 25 | { name = "Winston Sung" }, 26 | { name = "YuevUwU" }, 27 | { name = "伊藤蒼太" }, 28 | ] 29 | classifiers = [ 30 | "Development Status :: 4 - Beta", 31 | "Environment :: Console", 32 | "Environment :: Web Environment", 33 | "Framework :: Flask", 34 | "Framework :: Pytest", 35 | "Intended Audience :: Developers", 36 | "Intended Audience :: Education", 37 | "Intended Audience :: Information Technology", 38 | "Natural Language :: Chinese (Traditional)", 39 | "Operating System :: OS Independent", 40 | "Programming Language :: JavaScript", 41 | "Programming Language :: Python", 42 | "Programming Language :: Python :: 3", 43 | "Programming Language :: Python :: 3 :: Only", 44 | "Programming Language :: Python :: 3.11", 45 | "Programming Language :: SQL", 46 | ] 47 | urls.repository = "https://github.com/SCAICT/SCAICT-uwu.git" 48 | urls.issues = "https://github.com/SCAICT/SCAICT-uwu/issues" 49 | # Lock file: requirements.txt 50 | dependencies = [ 51 | "flask == 3.1.2", 52 | "mysql-connector-python == 9.5.0", 53 | "py-cord == 2.6.1", 54 | "python-dotenv == 1.2.1", 55 | "requests == 2.32.5", 56 | ] 57 | # Lock file: requirements_dev.txt 58 | optional-dependencies.dev = [ 59 | "black == 25.11.0", 60 | "pylint == 4.0.4", 61 | "pytest == 9.0.1", 62 | ] 63 | -------------------------------------------------------------------------------- /channel_check.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | import asyncio 3 | import json 4 | import os 5 | from random import choice 6 | 7 | # Third-party imports 8 | import discord 9 | 10 | # Local imports 11 | from cog.core.sql import link_sql 12 | from cog.core.sql import end 13 | 14 | 15 | def open_json(): 16 | # open configuration file 17 | os.chdir("./") 18 | with open( 19 | f"{os.getcwd()}/database/server.config.json", "r", encoding="utf-8" 20 | ) as file: 21 | global_settings = json.load(file) 22 | return global_settings 23 | 24 | 25 | def get_total_points(): 26 | connection, cursor = link_sql() 27 | cursor.execute("SELECT SUM(point) FROM `user`") 28 | points = cursor.fetchone()[0] 29 | end(connection, cursor) 30 | return points 31 | 32 | 33 | async def update_channel(bot): 34 | channel = open_json()["SCAICT-alpha"]["channel"] 35 | await bot.wait_until_ready() 36 | guild = bot.get_guild(channel["serverID"]) # YOUR_GUILD_ID 37 | 38 | if guild is None: 39 | print("找不到指定的伺服器") 40 | return 41 | 42 | member_channel = guild.get_channel(channel["memberCount"]) # YOUR_CHANNEL_ID 43 | point_channel = guild.get_channel(channel["pointCount"]) 44 | if channel is None: 45 | print("找不到指定的頻道") 46 | return 47 | prev_points = get_total_points() 48 | prev_total_members = guild.member_count 49 | while not bot.is_closed(): 50 | points = get_total_points() 51 | total_members = guild.member_count 52 | if points != prev_points: 53 | await point_channel.edit(name=f"🔋總電量:{points}") 54 | prev_points = points 55 | if total_members != prev_total_members: 56 | await member_channel.edit(name=f"👥電池數:{total_members}") 57 | prev_total_members = total_members 58 | await asyncio.sleep(600) 59 | 60 | 61 | async def change_status(bot): 62 | await bot.wait_until_ready() 63 | announcements = [ 64 | "SCAICT.org", 65 | "今天 /charge 了嗎?", 66 | "要不要一起猜顏色", 67 | "要不要一起猜拳?", 68 | "debug", 69 | ] 70 | while not bot.is_closed(): 71 | status = choice(announcements) 72 | await bot.change_presence(activity=discord.Game(name=status)) 73 | await asyncio.sleep(10) 74 | -------------------------------------------------------------------------------- /docs/abstract_schema_products.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/schema#", 3 | "description": "Abstract description of a scaict-uwu products.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Optional. Name/short description of the JSON file" 10 | }, 11 | "description": { 12 | "type": "string", 13 | "description": "Optional. Description of the JSON file" 14 | }, 15 | "products": { 16 | "type": "array", 17 | "additionalItems": false, 18 | "description": "Array of products", 19 | "items": { 20 | "type": "object", 21 | "additionalProperties": false, 22 | "properties": { 23 | "id": { 24 | "type": "string", 25 | "description": "ID of the product", 26 | "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$" 27 | }, 28 | "name": { 29 | "type": "string", 30 | "description": "Name of the product" 31 | }, 32 | "description": { 33 | "type": "string", 34 | "description": "Description of the product item" 35 | }, 36 | "url": { 37 | "type": "string", 38 | "description": "Optional. The URL path without leading slash of the link target of the button", 39 | "pattern": "^[^\/\n].*$" 40 | }, 41 | "image": { 42 | "type": "string", 43 | "description": "Image URL of the product item. Preferred size of the image: 1000 × 1000 px, max. 2400, min. 300", 44 | "format": "uri", 45 | "pattern": "^https:\/\/.+$" 46 | }, 47 | "category": { 48 | "type": "string", 49 | "description": "Category of the product item", 50 | "enum": [ 51 | "實體周邊", 52 | "遊戲" 53 | ] 54 | }, 55 | "pay": { 56 | "type": "string", 57 | "description": "The payment method of the product item", 58 | "enum": [ 59 | "point", 60 | "ticket" 61 | ] 62 | }, 63 | "price": { 64 | "type": "integer", 65 | "description": "Price of the product item, using the payment method as the unit of price" 66 | }, 67 | "stock": { 68 | "type": "integer", 69 | "description": "Remaining stock count of the product item" 70 | } 71 | }, 72 | "required": [ 73 | "id", 74 | "name", 75 | "description", 76 | "image", 77 | "category", 78 | "pay", 79 | "price", 80 | "stock" 81 | ] 82 | } 83 | } 84 | }, 85 | "required": [ 86 | "products" 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /README_zh-Hant.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 中電喵 SCAICT uwu 6 | 7 | # 中電喵 SCAICT uwu 8 | 9 | 住在中電會 Discord 伺服器的貓咪 10 | 11 | [![同步代辦事項至 Notion](https://github.com/SCAICT/SCAICT-uwu/actions/workflows/notion.yml/badge.svg?event=issues)](https://github.com/SCAICT/SCAICT-uwu/actions/workflows/notion.yml) 12 | [![官方網站](https://img.shields.io/website?label=官方網站&&url=https%3A%2F%2Fscaict.org%2F)](https://scaict.org/) 13 | [![中電商店](https://img.shields.io/website?label=中電商店&&url=https%3A%2F%2Fstore.scaict.org%2F)](https://store.scaict.org/) 14 | [![加入 Discord 伺服器](https://img.shields.io/discord/959823904266944562?label=Discord&logo=discord&)](https://dc.scaict.org) 15 | [![追蹤 Instagram](https://img.shields.io/badge/follow-%40scaict.tw-pink?&logo=instagram)](https://www.instagram.com/scaict.tw/) 16 | 17 |
18 | 19 | > 這個專案目前處於開發階段,並且可能會有一些問題。如果您發現了任何問題或有任何建議,請透過提交 issue 來通知我們。 20 | 21 | ## 如何部署? 22 | 23 | 1. clone 此儲存庫。 24 | 2. 在 Python 3.11 中建立環境。 25 | 3. 安裝必要的函式庫。 26 | 27 | ```bash 28 | pip install -r requirements.txt 29 | ``` 30 | 31 | 4. 在 `database/server.config.json` 中設定頻道。 32 | 5. 啟動 SQL 伺服器。 33 | 6. 在 Breadcrumbs SCAICT-uwu 的 `cog/core/sql_acc.py` 中設定 SQL 伺服器。 34 | 7. 執行 Flask。 35 | 36 | ```bash 37 | flask run 38 | ``` 39 | 40 | 8. 執行 `main.py`。 41 | 42 | ```bash 43 | python main.py 44 | ``` 45 | 46 | ## 檔案 47 | 48 | * `main.py`:中電喵。 49 | * `app.py`:中電商店。 50 | * `generate_secrets.py`:為 `app.py` 產生密鑰,執行後儲存在 `token.json` 中。 51 | * 資料庫 MySQL:使用外部伺服器,相關設定在 `cog/core/secret.py` 中。 52 | * `token.json`: 53 | 54 | ```json 55 | { 56 | "discord_token": "", 57 | "secret_key": "", 58 | "discord_client_id": "", 59 | "discord_client_secret": "", 60 | "discord_redirect_uri": "http://127.0.0.1:5000/callback", 61 | "github_client_id": "", 62 | "github_client_secret": "", 63 | "github_redirect_uri": "http://127.0.0.1:5000/github/callback", 64 | "github_discord_redirect_uri": "http://127.0.0.1:5000/github/discord-callback" 65 | } 66 | ``` 67 | 68 | * `database/slot.json`: 69 | 70 | 設定老虎機的中獎機率。 71 | 72 | ```json 73 | { 74 | "element": [ percentage, reward ] 75 | } 76 | ``` 77 | 78 | > 更詳細的說明文件敘述可以參考[這裡](https://g.scaict.org/doc/) 79 | 80 | ## 鳴謝 81 | 82 | 中電喵是由中電會和[貢獻者們](https://github.com/SCAICT/SCAICT-uwu/graphs/contributors)共同開發和維護的專案。角色設計由[毛哥 EM](https://elvismao.com/) 和[瑞樹](https://www.facebook.com/ruishuowo)創作,而部分圖示則選用了來自 [Freepik - Flaticon](https://www.flaticon.com/free-icons/slot-machine) 的設計素材。 83 | -------------------------------------------------------------------------------- /database/products.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "id": "KawaiiSticker", 5 | "name": "中電會貼紙", 6 | "description": "可愛版中電會 Logo", 7 | "image": "https://raw.githubusercontent.com/SCAICT/website-data/main/converted/img/KawaiiLogo.webp", 8 | "category": "實體周邊", 9 | "pay": "point", 10 | "price": 50, 11 | "stock": 499 12 | }, 13 | { 14 | "id": "uwuSticker", 15 | "name": "中電喵貼紙", 16 | "description": "uwu", 17 | "image": "https://raw.githubusercontent.com/SCAICT/website-data/main/converted/img/scaict-uwu.webp", 18 | "category": "實體周邊", 19 | "pay": "point", 20 | "price": 50, 21 | "stock": 500 22 | }, 23 | { 24 | "id": "uwuLazerSticker", 25 | "name": "中電喵雷射貼模貼紙", 26 | "description": "閃亮的中電喵", 27 | "image": "https://raw.githubusercontent.com/SCAICT/website-data/main/converted/img/scaict-uwu.webp", 28 | "category": "實體周邊", 29 | "pay": "point", 30 | "price": 333, 31 | "stock": 36 32 | }, 33 | { 34 | "id": "stickNotes", 35 | "name": "中電喵便利貼", 36 | "description": "中電喵便利貼", 37 | "image": "https://raw.githubusercontent.com/SCAICT/website-data/main/converted/img/sticknote.webp", 38 | "category": "實體周邊", 39 | "pay": "point", 40 | "price": 500, 41 | "stock": 20 42 | }, 43 | { 44 | "id": "notebook", 45 | "name": "中電會筆記本", 46 | "description": "16 頁牛皮紙筆記本", 47 | "image": "https://raw.githubusercontent.com/SCAICT/website-data/main/converted/img/notebook.webp", 48 | "category": "實體周邊", 49 | "pay": "point", 50 | "price": 500, 51 | "stock": 100 52 | }, 53 | { 54 | "id": "usb", 55 | "name": "8 GB USB", 56 | "description": "插件。插進去就會發光", 57 | "image": "https://raw.githubusercontent.com/SCAICT/website-data/main/converted/img/usb.webp", 58 | "category": "實體周邊", 59 | "pay": "point", 60 | "price": 2000, 61 | "stock": 9 62 | }, 63 | { 64 | "id": "slot", 65 | "name": "貓咪機", 66 | "description": "來抽獎吧", 67 | "url": "slot", 68 | "image": "https://cdn-icons-png.flaticon.com/128/1055/1055823.png", 69 | "category": "遊戲", 70 | "pay": "ticket", 71 | "price": 1, 72 | "stock": 9999 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /cog/api/gift.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class Gift: 5 | def __init__(self, api_key: str, guild_id: int, recipient_id: int): 6 | self.api_key = api_key 7 | self.guild_id = guild_id 8 | self.error_msg = None 9 | self.headers = { 10 | "Authorization": f"Bot {self.api_key}", 11 | "Content-Type": "application/json", 12 | } 13 | try: 14 | fetch_usr = self.__new_dm(recipient_id) 15 | if "id" in fetch_usr: 16 | self.dm_room = fetch_usr["id"] 17 | else: 18 | self.dm_room = None 19 | except Exception as e: 20 | self.dm_room = None 21 | self.error_msg = str(e) 22 | 23 | def __new_dm(self, uid: int) -> dict: 24 | try: 25 | url = "https://discord.com/api/v10/users/@me/channels" 26 | payload = {"recipient_id": uid} 27 | # {'id': '', 'type': 1, 'last_message_id': '1276230139230814241', 'flags': 0, 'recipients': [{'id': '', 'username': '', 'avatar': '', 'discriminator': '0', 'public_flags': 256, 'flags': 256, 'banner': '', 'accent_color': 2054367, 'global_name': '', 'avatar_decoration_data': {'asset': '', 'sku_id': '1144058522808614923', 'expires_at': None}, 'banner_color': '#1f58df', 'clan': None}]} 28 | response = requests.post(url, headers=self.headers, json=payload, timeout=5) 29 | response.raise_for_status() 30 | return response.json() 31 | except requests.RequestException as e: 32 | return {"result": "internal server error", "status": 500, "error": str(e)} 33 | except Exception as e: 34 | return {"result": "internal server error", "status": 500, "error": str(e)} 35 | 36 | def _gen_msg(self, gift_type: str, gift_amount: int) -> str: 37 | embed = { 38 | "title": f"你收到了 {gift_amount} {gift_type}!", 39 | "color": 3447003, # (藍色) 40 | "description": ":gift:", 41 | } 42 | button = { 43 | "type": 1, 44 | "components": [ 45 | { 46 | "type": 2, 47 | "label": "前往確認", 48 | "style": 5, # `5` 表示 Link Button 49 | "url": "https://store.scaict.org", # 要導向的連結 50 | } 51 | ], 52 | } 53 | json_data = { 54 | "embeds": [embed], 55 | "components": [button], 56 | "tts": False, # Text-to-speech, 默認為 False 57 | } 58 | return json_data 59 | 60 | def send_gift(self, gift_type: str, gift_amount: int) -> str: 61 | url = f"https://discord.com/api/v10/channels/{self.dm_room}/messages" 62 | payload = self._gen_msg(gift_type, gift_amount) 63 | response = requests.post(url, headers=self.headers, json=payload, timeout=5) 64 | return response.json().get("id") 65 | -------------------------------------------------------------------------------- /static/slot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /cog/api/api.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | # import json 3 | 4 | # Third-party imports 5 | import requests 6 | 7 | 8 | class Apis: 9 | def __init__(self, api_key: str, guild_id: int): 10 | self.api_key = api_key 11 | self.guild_id = guild_id 12 | self.headers = { 13 | "Authorization": f"Bot {self.api_key}", 14 | "Content-Type": "application/json", 15 | } 16 | 17 | def get_user(self, uid): 18 | """ 19 | API 回傳的資料格式範例,已經把一些敏感資料隱藏掉 20 | { 21 | "avatar": null, 22 | "banner": null, 23 | "communication_disabled_until": null, 24 | "flags": 0, 25 | "joined_at": "", 26 | "nick": null, 27 | "pending": false, 28 | "premium_since": null, 29 | "roles": [ 30 | "12348763", 31 | "12448763", 32 | "12548763" 33 | ], 34 | "unusual_dm_activity_until": null, 35 | "user": { 36 | "id": "", 37 | "username": "", 38 | "avatar": "", 39 | "discriminator": "0", 40 | "public_flags": 256, 41 | "flags": 256, 42 | "banner": "", 43 | "accent_color": 2054367, 44 | "global_name": "", 45 | "avatar_decoration_data": { 46 | "asset": "a_d3da36040163ee0f9176dfe7ced45cdc", 47 | "sku_id": "1144058522808614923", 48 | "expires_at": null 49 | }, 50 | "banner_color": "#1f58df", 51 | "clan": null 52 | }, 53 | "mute": false, 54 | "deaf": false 55 | } 56 | """ 57 | try: 58 | url = f"https://discord.com/api/v10/guilds/{self.guild_id}/members/{uid}" 59 | usr = requests.get(url, headers=self.headers, timeout=5) 60 | usr.raise_for_status() # 檢查 HTTP 狀態碼 61 | return usr.json() 62 | except requests.exceptions.RequestException as e: 63 | # 如果發生錯誤,返回一個包含錯誤訊息和詳細報錯的字典 64 | return {"error": "get_user error", "details": str(e)} 65 | 66 | def create_dm_channel(self, target_user_id: str): 67 | try: 68 | url = "https://discord.com/api/v10/users/@me/channels" 69 | json_data = {"recipient_id": target_user_id} 70 | response = requests.post( 71 | url, headers=self.headers, json=json_data, timeout=10 72 | ) 73 | response.raise_for_status() # Raise an HTTPError for bad responses 74 | dm_channel = response.json() 75 | return dm_channel["id"] 76 | except requests.RequestException as e: 77 | return { 78 | "result": "internal server error", 79 | "status": 500, 80 | "error": str(e), 81 | } 82 | except Exception as e: 83 | return { 84 | "result": "internal server error", 85 | "status": 500, 86 | "error": str(e), 87 | } 88 | -------------------------------------------------------------------------------- /cog/core/downtime.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from dataclasses import dataclass, field 3 | from datetime import datetime 4 | import json 5 | import os 6 | 7 | from discord import Bot 8 | from discord.abc import Messageable 9 | 10 | from cog.core.safe_write import safe_open_w 11 | 12 | DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S%z" 13 | DOWNTIME_PATH = f"{os.getcwd()}/database/downtime.json" 14 | 15 | 16 | @dataclass 17 | class Downtime: 18 | start: datetime 19 | # TODO: will Downtime.end be None? 20 | end: datetime = field(default_factory=datetime.now) 21 | is_restored: bool = False 22 | 23 | def __post_init__(self): 24 | if self.end.tzinfo is None: 25 | self.end = self.end.astimezone() 26 | 27 | def __contains__(self, timestamp: datetime): 28 | if self.end.tzinfo is None: # XXX: True when self.end is set after init 29 | self.end = self.end.astimezone() 30 | 31 | return self.start <= timestamp <= self.end 32 | 33 | @staticmethod 34 | def from_str( 35 | start_str: str, end_str: str | None = None, is_restored=False 36 | ) -> Downtime: 37 | start = datetime.strptime(start_str, DATETIME_FORMAT) 38 | if end_str: 39 | end = datetime.strptime(end_str, DATETIME_FORMAT) 40 | else: 41 | end = datetime.now() 42 | return Downtime(start, end, is_restored) 43 | 44 | @staticmethod 45 | def from_dict(d: dict) -> Downtime: 46 | return Downtime.from_str( 47 | start_str=d["start"], 48 | end_str=d.get("end", None), 49 | is_restored=d.get("is_restored", False), 50 | ) 51 | 52 | def to_dict(self) -> dict[str, str | bool]: 53 | return { 54 | "start": datetime.strftime(self.start, DATETIME_FORMAT), 55 | "end": datetime.strftime(self.end, DATETIME_FORMAT), 56 | "is_restored": self.is_restored, 57 | } 58 | 59 | def marked_as_restored(self) -> Downtime: 60 | self.is_restored = True 61 | return self 62 | 63 | 64 | def get_downtime_list() -> list[Downtime]: 65 | with open(DOWNTIME_PATH, "r", encoding="utf-8") as file: 66 | data: list[dict[str, str | bool]] = json.load(file) 67 | 68 | return list(map(Downtime.from_dict, data)) 69 | 70 | 71 | def write_downtime_list(downtime_list: list[Downtime]): 72 | with safe_open_w(DOWNTIME_PATH, encoding="utf-8") as file: 73 | json.dump([downtime.to_dict() for downtime in downtime_list], file, indent=4) 74 | 75 | 76 | async def get_history( 77 | bot: Bot, channel_id, *, after: datetime, before: datetime | None = None 78 | ): 79 | if before is None: 80 | before = datetime.now() 81 | 82 | channel: Messageable = bot.get_channel(channel_id) 83 | 84 | if not channel: 85 | raise ValueError(f"Cannot get channel (id={channel}).") 86 | 87 | if not isinstance(channel, Messageable): 88 | raise ValueError( 89 | f"{channel.name} (id={channel_id}, type={type(channel)}) is not messageable." 90 | ) 91 | 92 | # if datetime is naive, it is assumed to be local time 93 | messages = await channel.history( 94 | limit=None, after=after, before=before, oldest_first=True 95 | ).flatten() 96 | 97 | return messages 98 | -------------------------------------------------------------------------------- /cog/core/sql.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from contextlib import contextmanager 3 | from typing import cast 4 | 5 | from mysql.connector.connection_cext import CMySQLConnection 6 | from mysql.connector.cursor_cext import CMySQLCursor 7 | from mysql.connector.types import MySQLConvertibleType 8 | from mysql.connector.errors import Error as MySQLError 9 | 10 | # Local imports 11 | from .secret import connect 12 | 13 | 14 | # TODO: replace link_sql() 15 | @contextmanager 16 | def mysql_connection(): 17 | connection: CMySQLConnection | None = None 18 | cursor: CMySQLCursor | None = None 19 | 20 | try: 21 | connection = cast(CMySQLConnection, connect()) 22 | if connection is None: 23 | raise RuntimeError("Cannot connect to database") 24 | cursor = connection.cursor() 25 | yield (connection, cursor) 26 | connection.commit() 27 | except TypeError: 28 | print("Please setup .env correctly.") 29 | raise 30 | except MySQLError: 31 | if connection: 32 | connection.rollback() 33 | raise 34 | finally: 35 | if cursor: 36 | cursor.close() 37 | if connection: 38 | connection.close() 39 | 40 | 41 | def end(connection, cursor): # 結束和SQL資料庫的會話 42 | cursor.close() 43 | connection.commit() 44 | connection.close() 45 | 46 | 47 | def link_sql(): 48 | connection = connect() 49 | cursor = connection.cursor() 50 | return connection, cursor 51 | 52 | 53 | # def opWrite(user, user_prop:str, op: str, table = "user"): # 根據op傳入運算式做+=/-=等以自己原本的值為基準的運算 54 | # 建立連線 55 | # connection = connect() 56 | # cursor = connection.cursor() 57 | # cursor.execute(f"UPDATE {table} SET {user_prop} = {user_prop}{op} ;") 58 | # end(connection.cursor) 59 | 60 | 61 | def fetchone_by_primary_key(table: str, key_name: str, value: MySQLConvertibleType): 62 | with mysql_connection() as c: 63 | _, cursor = c 64 | query = f"SELECT * FROM `{table}` WHERE `{key_name}` = %s" 65 | cursor.execute(query, (value,)) 66 | result = cursor.fetchall() 67 | 68 | if len(result) == 0: 69 | return None 70 | 71 | if len(result) != 1: 72 | raise ValueError("Result have multiple rows.") 73 | 74 | row = result[0] 75 | field_names = cursor.column_names 76 | 77 | return dict(zip(field_names, row)) 78 | 79 | 80 | # XXX: this implement have the risk about SQL injection 81 | def write(user_id, user_prop: str, value, cursor, table: str = "user") -> None: 82 | """ 83 | 欲變更的使用者、屬性、修改值、欲修改資料表(預設user, option) 84 | """ 85 | # 建立連線 86 | 87 | cursor.execute(f'SELECT `uid` FROM `{table}` WHERE `uid`="{user_id}"') 88 | ret = cursor.fetchall() 89 | if len(ret) == 0: # 找不到 新增一份 90 | cursor.execute(f"INSERT INTO `{table}`(uid) VALUE({user_id})") 91 | cursor.execute(f'UPDATE `{table}` SET {user_prop}="{value}" WHERE `uid`={user_id}') 92 | # print(f"write {ret} to ({user_prop},{value})") 93 | 94 | 95 | def read(user_id, user_prop, cursor, table="user"): 96 | # 建立連線 97 | 98 | cursor.execute(f"SELECT {user_prop} FROM `{table}` WHERE `uid`={user_id}") 99 | ret = cursor.fetchall() 100 | if len(ret) == 0: # 找不到 新增一份 101 | cursor.execute(f"INSERT INTO `{table}`(uid) VALUE({user_id})") 102 | cursor.execute(f"SELECT {user_prop} FROM `{table}` WHERE `uid`={user_id}") 103 | ret = cursor.fetchall() 104 | return ret[0][0] 105 | 106 | 107 | def user_id_exists(user_id, table, cursor): 108 | cursor.execute(f'SELECT `uid` FROM {table} WHERE `uid`="{user_id}"') 109 | ret = cursor.fetchall() 110 | if len(ret) == 0: # 不存在 111 | return False 112 | 113 | return True 114 | -------------------------------------------------------------------------------- /cog/version_info.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | import datetime 3 | 4 | # import subprocess 5 | import sys 6 | from typing import cast 7 | 8 | # Third-party imports 9 | import discord 10 | from discord.ext import commands 11 | 12 | 13 | class VersionInfo(commands.Cog): 14 | _SCAICT_UWU_VERSION_NUMBER: str = "0.1.13" 15 | """ 16 | Current hardcoded workaround 17 | """ 18 | 19 | _SCAICT_UWU_VERSION_DATE: str = "2025-12-05 (UTC)" 20 | """ 21 | Current hardcoded workaround 22 | """ 23 | 24 | _SCAICT_UWU_VERSION: str = ( 25 | f"{_SCAICT_UWU_VERSION_NUMBER}\n{_SCAICT_UWU_VERSION_DATE}" 26 | ) 27 | """ 28 | Current hardcoded workaround 29 | """ 30 | 31 | _SCAICT_UWU_IMAGE = ( 32 | "https://github.com/SCAICT/SCAICT-uwu/blob/851186b/uwu.png?raw=true" 33 | ) 34 | """ 35 | Current hardcoded workaround 36 | """ 37 | 38 | def __init__(self, bot: discord.Bot) -> None: 39 | self.bot = bot 40 | 41 | def _embed_version_info(self) -> discord.Embed: 42 | """ 43 | The version information embed. 44 | """ 45 | 46 | # TODO: Git hash 47 | # process = subprocess.Popen( 48 | # ['git', 'rev-parse', 'HEAD'], 49 | # shell=False, 50 | # stdout=subprocess.PIPE 51 | # ) 52 | # git_head_hash = process.communicate()[0].strip() 53 | 54 | # == Installed software == 55 | # 56 | # '''scaict_uwu''' 57 | # version_num (git_hash_to-do) 58 | # date 59 | # 60 | # '''python''' 61 | # version 62 | # 63 | # '''pip''' 64 | # version 65 | # 66 | # '''mysql''' 67 | # version 68 | # 69 | # == Installed packages == 70 | description = ( 71 | "### 已安裝的軟體\n\n" 72 | + "**中電喵**\n" 73 | + self._SCAICT_UWU_VERSION 74 | + "\n\n" 75 | + "**Python**\n" 76 | + sys.version 77 | # + "\n\n" 78 | # + "### 已安裝的套件" 79 | ) 80 | 81 | return discord.Embed( 82 | color=0xFF24CF, 83 | title="版本資訊", 84 | description=description, 85 | fields=self._embed_fields_installed_packages(), 86 | author=discord.EmbedAuthor( 87 | name="中電喵", 88 | icon_url=self.bot.user.display_avatar.url, 89 | ), 90 | thumbnail=self._SCAICT_UWU_IMAGE, 91 | footer=discord.EmbedFooter( 92 | text=datetime.datetime.now(tz=datetime.timezone.utc).strftime( 93 | format="%Y-%m-%d %H:%M:%S (UTC)" 94 | ) 95 | ), 96 | ) 97 | 98 | def _embed_fields_installed_packages(self) -> list[discord.EmbedField] | None: 99 | """ 100 | flask: version, mysql-connector-python: version, 101 | py-cord: version, ... 102 | """ 103 | 104 | # fields = [] 105 | 106 | # TODO: 107 | # fields.append( 108 | # discord.EmbedField( 109 | # name="package_name".lower, 110 | # value="package_version", 111 | # inline=True, 112 | # ) 113 | # ) 114 | 115 | # return fields 116 | return None 117 | 118 | @discord.slash_command(name="version_info", description="版本資訊") 119 | async def version_info(self, interaction) -> None: 120 | interaction = cast(discord.Interaction, interaction) 121 | 122 | assert ( 123 | interaction.user 124 | ), "Interaction may be in PING interactions, so that interaction.user is invalid." 125 | assert interaction.channel, "There are no channel returned from interation." 126 | 127 | await interaction.response.send_message(embed=self._embed_version_info()) 128 | 129 | 130 | def setup(bot: discord.Bot): 131 | bot.add_cog(VersionInfo(bot)) 132 | -------------------------------------------------------------------------------- /cog/game.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | import json 3 | import os 4 | import random 5 | 6 | # Third-party imports 7 | import discord 8 | from discord.ext import commands 9 | 10 | # Local imports 11 | from cog.core.sql import write 12 | from cog.core.sql import read 13 | from cog.core.sql import link_sql 14 | from cog.core.sql import end 15 | 16 | 17 | def get_channels(): 18 | """ 19 | 取得特殊用途頻道的列表,這裡會用來判斷是否在簽到頻道簽到,否則不予受理 20 | """ 21 | try: 22 | with open( 23 | f"{os.getcwd()}/database/server.config.json", "r", encoding="utf-8" 24 | ) as file: 25 | return json.load(file)["SCAICT-alpha"] 26 | except FileNotFoundError: 27 | print("Configuration file not found.") 28 | return {} 29 | except json.JSONDecodeError: 30 | print("Error decoding JSON.") 31 | return {} 32 | 33 | 34 | stickers = get_channels()["stickers"]["zap"] 35 | 36 | 37 | class Game(commands.Cog): 38 | # User can use this command to play ✊-🤚-✌️ with the bot in the command channel 39 | @discord.slash_command(name="rock_paper_scissors", description="玩剪刀石頭布") 40 | # user can choose ✊, 🤚, or ✌️ in their command 41 | async def rock_paper_scissors( 42 | self, interaction, choice: discord.Option(str, choices=["✊", "🤚", "✌️"]) 43 | ): 44 | if interaction.channel.id != get_channels()["channel"]["commandChannel"]: 45 | await interaction.response.send_message("這裡不是指令區喔", ephemeral=True) 46 | return 47 | user_id = interaction.user.id 48 | user_display_name = interaction.user 49 | try: 50 | connection, cursor = link_sql() # SQL 會話 51 | 52 | point = read(user_id, "point", cursor) 53 | if point < 5: 54 | await interaction.response.send_message( 55 | "你的電電點不足以玩這個遊戲", ephemeral=True 56 | ) 57 | end(connection, cursor) 58 | return 59 | if choice not in ["✊", "🤚", "✌️"]: 60 | await interaction.response.send_message( 61 | "請輸入正確的選擇", ephemeral=True 62 | ) 63 | end(connection, cursor) 64 | return 65 | 66 | bot_choice = random.choice(["✊", "🤚", "✌️"]) 67 | game_outcomes = { 68 | ("✌️", "✊"): 5, 69 | ("✌️", "🤚"): -5, 70 | ("✊", "✌️"): -5, 71 | ("✊", "🤚"): 5, 72 | ("🤚", "✌️"): 5, 73 | ("🤚", "✊"): -5, 74 | } 75 | 76 | if bot_choice == choice: 77 | await interaction.response.send_message( 78 | content=f"我出{bot_choice},平手。你還有{point}{stickers}" 79 | ) 80 | else: 81 | point += game_outcomes[(bot_choice, choice)] 82 | result = ( 83 | "你贏了" if game_outcomes[(bot_choice, choice)] > 0 else "你輸了" 84 | ) 85 | await interaction.response.send_message( 86 | content=f"我出{bot_choice},{result},你還有{point}{stickers}" 87 | ) 88 | # pylint: disable-next = line-too-long 89 | print( 90 | f"{user_id}, {user_display_name} Get {game_outcomes[(bot_choice, choice)]} point by playing rock-paper-scissors" 91 | ) 92 | write(user_id, "point", point, cursor) 93 | # pylint: disable-next = broad-exception-caught 94 | except Exception as exception: 95 | print(f"Error: {exception}") 96 | 97 | end(connection, cursor) 98 | 99 | @discord.slash_command(name="number_status", description="數數狀態") 100 | async def number_status(self, interaction): 101 | try: 102 | connection, cursor = link_sql() # SQL 會話 103 | cursor.execute("SELECT seq FROM game") 104 | current_sequence = cursor.fetchone()[0] 105 | # pylint: disable-next = broad-exception-caught 106 | except Exception as exception: 107 | print(f"Error: {exception}") 108 | 109 | end(connection, cursor) 110 | embed = discord.Embed( 111 | title="現在數到", 112 | description=f"{current_sequence} (dec) 了,接下去吧!", 113 | color=0xFF24CF, 114 | ) 115 | await interaction.response.send_message(embed=embed) 116 | 117 | 118 | def setup(bot): 119 | bot.add_cog(Game(bot)) 120 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you wish to contribute to SCAICT-uwu, feel free to fork the repository and 4 | submit a pull request. 5 | 6 | All development happens on the `dev` branch. Make sure to submit pull requests 7 | in the correct branch. 8 | 9 | ## Coding conventions 10 | 11 | ### File formatting 12 | 13 | For Python files, we currently use the Black formatter. 14 | 15 | #### Indentation 16 | 17 | For TOML files, lines should be indented with 1 tab character per indenting 18 | level. 19 | 20 | For JSON and Python files, lines should be indented with 4 whitespace characters 21 | per indenting level. 22 | 23 | For YAML files, lines should be indented with 2 whitespace characters per 24 | indenting level. 25 | 26 | #### Newlines 27 | 28 | * All files should use Unix-style newlines (single LF character, not a CR+LF 29 | combination). 30 | * All files should have a newline at the end. 31 | 32 | #### Encoding 33 | 34 | All text files must be encoded with UTF-8 without a 35 | [Byte Order Mark](https://en.wikipedia.org/wiki/Byte_order_mark). 36 | 37 | Do not use Microsoft Notepad to edit files, as it always inserts a BOM. 38 | 39 | #### Trailing whitespace 40 | 41 | Developers should avoid adding trailing whitespace. 42 | 43 | #### Line width 44 | 45 | Lines should be broken with a line break at maximum 80 characters. 46 | 47 | #### Import order 48 | 49 | Imports should use the following order first, then the alphabetical order: 50 | 51 | ```py 52 | # Standard imports 53 | # Third-party imports 54 | # Local imports 55 | ``` 56 | 57 | See 58 | [wrong-import-order / C0411](https://pylint.readthedocs.io/en/latest/user_guide/messages/convention/wrong-import-order.html) 59 | for further information. 60 | 61 | ### Naming conventions 62 | 63 | Naming cases: 64 | 65 | * `snake_case` 66 | * `camelCase` 67 | * `PascalCase` 68 | * `UPPER_CASE` 69 | 70 | #### Python 71 | 72 | | Name Type | Case | 73 | | ------------------- | ------------ | 74 | | module (file names) | `snake_case` | 75 | | const | `UPPER_CASE` | 76 | | class | `PascalCase` | 77 | | function | `snake_case` | 78 | | method | `snake_case` | 79 | | variable | `snake_case` | 80 | | attribute | `snake_case` | 81 | | argument | `snake_case` | 82 | 83 | Example: 84 | 85 | ```txt 86 | python_module.py 87 | ``` 88 | 89 | ```py 90 | """ 91 | Summary of module. 92 | 93 | Extended description. 94 | """ 95 | 96 | # Standard imports 97 | import standard_import 98 | 99 | # Third-party imports 100 | import third_party_import 101 | 102 | # Local imports 103 | import local_import 104 | 105 | class ClassName: 106 | """ 107 | Summary of class. 108 | 109 | Extended description. 110 | 111 | Attributes: 112 | CONST_NAME (int): Description of constant. 113 | attr_name (int): Description of attribute. 114 | """ 115 | 116 | CONST_NAME = 1 117 | """ 118 | CONST_NAME (int): Description of constant. 119 | """ 120 | 121 | attr_name = 1 122 | """ 123 | attr_name (int): Description of attribute. 124 | """ 125 | 126 | def method_name(self, param_name: int) -> int: 127 | """ 128 | Summary of method. 129 | 130 | Extended description. 131 | 132 | Parameters: 133 | param_name (int): Description of parameter. 134 | 135 | Returns: 136 | int: Description of return value. 137 | 138 | Raises: 139 | KeyError: Raises an exception. 140 | """ 141 | 142 | print(param_name) 143 | 144 | return param_name 145 | ``` 146 | 147 | See 148 | [invalid-name / C0103](https://pylint.readthedocs.io/en/latest/user_guide/messages/convention/invalid-name.html) 149 | for further information. 150 | 151 | #### Database 152 | 153 | * ALWAYS and ONLY capitalize SQL reserved words in SQL queries. 154 | * See the official documentations of SQL and the 155 | [complete list on English Wikipedia](https://en.wikipedia.org/wiki/List_of_SQL_reserved_words) 156 | as references. 157 | * ALWAYS use `snake_case` for database, table, column, trigger names. 158 | * Table names and column names may NOT be case-sensitive in SQLite. 159 | * Database, table, and trigger names may NOT be case-sensitive in 160 | MySQL/MariaDB. 161 | * Column names should be unique, i.e., same column name should not exist in 162 | different tables. 163 | * Column names should be prefixed with table names or abbreviations. 164 | * For example, `user_id` column in `user` table, `ug_user` column in 165 | `user_groups` table. 166 | 167 | Examples: 168 | 169 | ```sql 170 | INSERT INTO user (uid) VALUE (6856) 171 | ``` 172 | 173 | ```sql 174 | UPDATE game SET game_seq = game_seq + 1 175 | ``` 176 | 177 | ## Documentation of external packages 178 | 179 | * flask: 180 | * mysql-connector-python: 181 | * py-cord: 182 | * python-dotenv: 183 | * requests: 184 | 185 | ### Development dependencies 186 | 187 | * black: 188 | * pylint: 189 | * pytest: 190 | -------------------------------------------------------------------------------- /cog/tests/test_daily_charge.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime 3 | 4 | from cog.daily_charge import Charge 5 | from cog.core.sql_abstract import UserRecord 6 | 7 | YUEVUWU = 1 8 | PANBOYU = 2 9 | WALNUT = 3 10 | 11 | 12 | # TODO: specify downtime_list with argument 13 | class TestIsForgivable(unittest.TestCase): 14 | # downtime_list = [ 15 | # { 16 | # "start": "2025-03-14 09:03:00+0800", 17 | # "end": "2025-04-12 16:26:35+0800", 18 | # "is_restored": true 19 | # } 20 | # ] 21 | 22 | def test_is_forgivable(self): 23 | test_cases_true = [ 24 | "2025-03-13 00:00:00", 25 | "2025-03-13 23:59:59", 26 | "2025-03-14 00:00:00", 27 | "2025-03-14 23:59:59", 28 | ] 29 | 30 | test_cases_false = [ 31 | "2025-03-12 23:59:59", 32 | "2025-03-15 00:00:00", 33 | ] 34 | 35 | for last_charge in test_cases_true: 36 | with self.subTest(last_charge=last_charge): 37 | self.assertTrue( 38 | Charge.is_forgivable( 39 | datetime.strptime(last_charge, "%Y-%m-%d %H:%M:%S") 40 | ) 41 | ) 42 | for last_charge in test_cases_false: 43 | with self.subTest(last_charge=last_charge): 44 | self.assertFalse( 45 | Charge.is_forgivable( 46 | datetime.strptime(last_charge, "%Y-%m-%d %H:%M:%S") 47 | ) 48 | ) 49 | 50 | 51 | class TestIsCrossDay(unittest.TestCase): 52 | def test_cross_day(self): 53 | test_cases_true = [ 54 | ("2024-01-01 01:00:00", "2024-01-03 00:00:00"), 55 | ("2024-01-01 01:00:00", "2024-01-03 00:59:59"), 56 | ("2024-01-01 01:00:00", "2024-01-03 01:00:00"), 57 | ] 58 | test_cases_false = [ 59 | ("2024-01-01 01:00:00", "2024-01-02 10:10:00"), 60 | ("2024-01-01 01:00:00", "2024-01-02 01:00:00"), 61 | ("2024-01-01 01:00:00", "2024-01-02 23:59:59"), 62 | ] 63 | 64 | for last_charge, executed_at in test_cases_true: 65 | with self.subTest(last_charge=last_charge, executed_at=executed_at): 66 | self.assertTrue( 67 | Charge.is_cross_day( 68 | datetime.strptime(last_charge, "%Y-%m-%d %H:%M:%S"), 69 | datetime.strptime(executed_at, "%Y-%m-%d %H:%M:%S"), 70 | ) 71 | ) 72 | for last_charge, executed_at in test_cases_false: 73 | with self.subTest(last_charge=last_charge, executed_at=executed_at): 74 | self.assertFalse( 75 | Charge.is_cross_day( 76 | datetime.strptime(last_charge, "%Y-%m-%d %H:%M:%S"), 77 | datetime.strptime(executed_at, "%Y-%m-%d %H:%M:%S"), 78 | ) 79 | ) 80 | 81 | 82 | class TestDailyChargeIntegrationTest(unittest.TestCase): 83 | """ 84 | There will be three Variable: 85 | ``` 86 | is_forgivable: bool # sign before downtime and not loss combo 87 | ``` 88 | """ 89 | 90 | def test_panboyu3980(self): 91 | last_charge = datetime(2025, 3, 13, 9, 5, 0) 92 | is_forgivable = Charge.is_forgivable(last_charge) 93 | self.assertTrue(is_forgivable) 94 | 95 | delta = Charge.reward( 96 | user_or_uid=PANBOYU, 97 | last_charge=last_charge, 98 | executed_at=datetime(2025, 4, 12, 17, 26, 0), 99 | orig_combo=11, 100 | orig_point=1445, 101 | orig_ticket=4, 102 | is_forgivable=is_forgivable, 103 | testing=True, 104 | ) 105 | expected = UserRecord( 106 | uid=PANBOYU, 107 | charge_combo=12, 108 | point=1450, 109 | last_charge=datetime(2025, 4, 12, 17, 26, 0), 110 | ) 111 | self.assertEqual(delta, expected) 112 | 113 | def test_w4lnu7__(self): 114 | last_charge = datetime(2025, 3, 12, 21, 15, 0) 115 | is_forgivable = Charge.is_forgivable(last_charge) 116 | self.assertFalse(is_forgivable) 117 | 118 | delta = Charge.reward( 119 | user_or_uid=WALNUT, 120 | last_charge=last_charge, 121 | executed_at=datetime(2025, 4, 12, 20, 56, 0), 122 | orig_combo=24, 123 | orig_point=2564, 124 | orig_ticket=4, 125 | is_forgivable=is_forgivable, 126 | testing=True, 127 | ) 128 | expected = UserRecord( 129 | uid=WALNUT, 130 | charge_combo=1, 131 | point=2569, 132 | last_charge=datetime(2025, 4, 12, 20, 56, 0), 133 | ) 134 | self.assertEqual(delta, expected) 135 | 136 | def test_hatakutsu_yuevu(self): 137 | last_charge = datetime(2025, 4, 12, 16, 27, 0) 138 | is_forgivable = Charge.is_forgivable(last_charge) 139 | self.assertFalse(is_forgivable) 140 | 141 | delta = Charge.reward( 142 | user_or_uid=YUEVUWU, 143 | last_charge=last_charge, 144 | executed_at=datetime(2025, 4, 13, 2, 47, 0), 145 | orig_combo=2, 146 | orig_point=2751 - 5, 147 | orig_ticket=8, 148 | is_forgivable=is_forgivable, 149 | testing=True, 150 | ) 151 | expected = UserRecord( 152 | uid=YUEVUWU, 153 | charge_combo=3, 154 | point=2751, 155 | last_charge=datetime(2025, 4, 13, 2, 47, 0), 156 | ) 157 | self.assertEqual(delta, expected) 158 | 159 | 160 | if __name__ == "__main__": 161 | unittest.main() 162 | -------------------------------------------------------------------------------- /static/table_struct.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.13 Distrib 8.0.39, for Linux (x86_64) 2 | -- 3 | -- Host: localhost Database: Discord 4 | -- ------------------------------------------------------ 5 | -- Server version 8.0.39-0ubuntu0.20.04.1 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | /*!50503 SET NAMES utf8mb4 */; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | 18 | -- 19 | -- Table structure for table `comment_points` 20 | -- 21 | 22 | DROP TABLE IF EXISTS `comment_points`; 23 | /*!40101 SET @saved_cs_client = @@character_set_client */; 24 | /*!50503 SET character_set_client = utf8mb4 */; 25 | CREATE TABLE `comment_points` ( 26 | `seq` int NOT NULL AUTO_INCREMENT, 27 | `uid` bigint NOT NULL, 28 | `times` int NOT NULL DEFAULT '2', 29 | `next_reward` int NOT NULL DEFAULT '1', 30 | PRIMARY KEY (`seq`), 31 | KEY `uid` (`uid`), 32 | CONSTRAINT `comment_points_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON DELETE CASCADE 33 | ) ENGINE=InnoDB AUTO_INCREMENT=137 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 34 | /*!40101 SET character_set_client = @saved_cs_client */; 35 | 36 | -- 37 | -- Table structure for table `ctf_data` 38 | -- 39 | 40 | DROP TABLE IF EXISTS `ctf_data`; 41 | /*!40101 SET @saved_cs_client = @@character_set_client */; 42 | /*!50503 SET character_set_client = utf8mb4 */; 43 | CREATE TABLE `ctf_data` ( 44 | `id` bigint NOT NULL, 45 | `flags` varchar(255) DEFAULT NULL, 46 | `score` int DEFAULT NULL, 47 | `restrictions` varchar(255) DEFAULT NULL, 48 | `message_id` bigint DEFAULT NULL, 49 | `case_status` tinyint(1) DEFAULT NULL, 50 | `start_time` datetime DEFAULT NULL, 51 | `end_time` varchar(255) DEFAULT NULL, 52 | `title` varchar(255) DEFAULT NULL, 53 | `tried` int DEFAULT NULL, 54 | PRIMARY KEY (`id`) 55 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 56 | /*!40101 SET character_set_client = @saved_cs_client */; 57 | 58 | -- 59 | -- Table structure for table `ctf_history` 60 | -- 61 | 62 | DROP TABLE IF EXISTS `ctf_history`; 63 | /*!40101 SET @saved_cs_client = @@character_set_client */; 64 | /*!50503 SET character_set_client = utf8mb4 */; 65 | CREATE TABLE `ctf_history` ( 66 | `data_id` bigint DEFAULT NULL, 67 | `uid` bigint DEFAULT NULL, 68 | `count` int DEFAULT NULL, 69 | `solved` tinyint(1) NOT NULL DEFAULT '0', 70 | KEY `data_id` (`data_id`), 71 | CONSTRAINT `history_ibfk_1` FOREIGN KEY (`data_id`) REFERENCES `ctf_data` (`id`) ON DELETE CASCADE 72 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 73 | /*!40101 SET character_set_client = @saved_cs_client */; 74 | 75 | -- 76 | -- Table structure for table `game` 77 | -- 78 | 79 | DROP TABLE IF EXISTS `game`; 80 | /*!40101 SET @saved_cs_client = @@character_set_client */; 81 | /*!50503 SET character_set_client = utf8mb4 */; 82 | CREATE TABLE `game` ( 83 | `seq` bigint NOT NULL DEFAULT '0', 84 | `lastID` bigint DEFAULT '0', 85 | `niceColor` varchar(3) NOT NULL DEFAULT 'FFF', 86 | `nicecolorround` int DEFAULT NULL, 87 | `niceColorCount` bigint DEFAULT '0' 88 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 89 | /*!40101 SET character_set_client = @saved_cs_client */; 90 | 91 | -- 92 | -- Table structure for table `gift` 93 | -- 94 | 95 | DROP TABLE IF EXISTS `gift`; 96 | /*!40101 SET @saved_cs_client = @@character_set_client */; 97 | /*!50503 SET character_set_client = utf8mb4 */; 98 | CREATE TABLE `gift` ( 99 | `btnID` bigint NOT NULL, 100 | `type` enum('電電點','抽獎券') DEFAULT NULL, 101 | `count` int DEFAULT NULL, 102 | `recipient` varchar(32) DEFAULT NULL, 103 | `received` tinyint(1) DEFAULT '0', 104 | `sender` varchar(32) DEFAULT 'admin', 105 | PRIMARY KEY (`btnID`) 106 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 107 | /*!40101 SET character_set_client = @saved_cs_client */; 108 | 109 | -- 110 | -- Table structure for table `user` 111 | -- 112 | 113 | DROP TABLE IF EXISTS `user`; 114 | /*!40101 SET @saved_cs_client = @@character_set_client */; 115 | /*!50503 SET character_set_client = utf8mb4 */; 116 | CREATE TABLE `user` ( 117 | `DCname` varchar(32) DEFAULT NULL, 118 | `uid` bigint NOT NULL, 119 | `DCMail` varchar(320) DEFAULT NULL, 120 | `githubName` varchar(39) DEFAULT NULL, 121 | `githubMail` varchar(320) DEFAULT NULL, 122 | `loveuwu` tinyint(1) NOT NULL DEFAULT '0', 123 | `point` int NOT NULL DEFAULT '0', 124 | `ticket` int NOT NULL DEFAULT '1', 125 | `charge_combo` int NOT NULL DEFAULT '0', 126 | `next_lottery` int NOT NULL DEFAULT '0', 127 | `last_charge` datetime NOT NULL DEFAULT '1970-01-01 00:00:00', 128 | `last_comment` date NOT NULL DEFAULT '1970-01-01', 129 | `today_comments` int NOT NULL DEFAULT '0', 130 | PRIMARY KEY (`uid`) 131 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 132 | /*!40101 SET character_set_client = @saved_cs_client */; 133 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 134 | 135 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 136 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 137 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 138 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 139 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 140 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 141 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 142 | 143 | -- Dump completed on 2024-08-20 19:32:15 144 | -------------------------------------------------------------------------------- /cog/class_role.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | import json 3 | import os 4 | 5 | # Third-party imports 6 | import discord 7 | from discord.ext import commands 8 | 9 | # Local imports 10 | from build.build import Build 11 | 12 | 13 | def get_courses(): 14 | try: 15 | with open( 16 | f"{os.getcwd()}/database/courses.json", "r", encoding="utf-8" 17 | ) as file: 18 | data = json.load(file) 19 | return data 20 | except (FileNotFoundError, json.JSONDecodeError): 21 | return {} 22 | 23 | 24 | def search_data(code): 25 | data = get_courses() 26 | if code in data: 27 | return data[code] 28 | 29 | return False 30 | 31 | 32 | def add_data(code, new_data): 33 | data = get_courses() 34 | data[code] = new_data 35 | with open(f"{os.getcwd()}/database/courses.json", "w", encoding="utf-8") as file: 36 | json.dump(data, file, indent=2, ensure_ascii=False) 37 | 38 | 39 | class ClassRole(Build): 40 | @commands.Cog.listener() 41 | async def on_ready(self): 42 | self.bot.add_view(self.TokenVerifyButton()) 43 | 44 | class TokenVerifyButton(discord.ui.View): 45 | def __init__(self): 46 | super().__init__(timeout=None) 47 | 48 | @discord.ui.button( 49 | label="輸入課程代碼", 50 | style=discord.ButtonStyle.blurple, 51 | emoji="🏷️", 52 | custom_id="button", 53 | ) 54 | # pylint: disable-next = unused-argument 55 | async def button_callback(self, button, interaction): 56 | class TokenModal(discord.ui.Modal): 57 | def __init__(self, *args, **kwargs) -> None: 58 | super().__init__(*args, **kwargs) 59 | 60 | self.input_field = discord.ui.InputText(label="請輸入課程代碼") 61 | self.add_item(self.input_field) 62 | 63 | async def callback(self, interaction: discord.Interaction): 64 | user_code = self.input_field.value 65 | 66 | if search_data(user_code): 67 | data = get_courses() 68 | role = discord.utils.get( 69 | interaction.guild.roles, name=data[user_code]["name"] 70 | ) 71 | await interaction.user.add_roles(role) 72 | role_name = data[user_code]["name"] 73 | theme = data[user_code]["theme"] 74 | teacher = data[user_code]["teacher"] 75 | time = data[user_code]["time"] 76 | # embed 77 | embed = discord.Embed(color=0x3DBD46) 78 | # pylint: disable-next = line-too-long 79 | embed.set_thumbnail( 80 | url="https://creazilla-store.fra1.digitaloceanspaces.com/emojis/47298/check-mark-button-emoji-clipart-md.png" 81 | ) 82 | embed.add_field( 83 | name=f"已領取{role_name}身分組", 84 | value=f" 課程主題:{theme}", 85 | inline=False, 86 | ) 87 | embed.add_field( 88 | name=f"使用者:{interaction.user.name}", 89 | value=f" 講師:{teacher}", 90 | inline=False, 91 | ) 92 | embed.set_footer(text=f"課程時間:{time}") 93 | # 94 | await interaction.response.send_message( 95 | embed=embed, ephemeral=True 96 | ) 97 | else: 98 | embed = discord.Embed(color=0xBD3D3D) 99 | # pylint: disable-next = line-too-long 100 | embed.set_thumbnail( 101 | url="https://creazilla-store.fra1.digitaloceanspaces.com/emojis/47329/cross-mark-button-emoji-clipart-md.png" 102 | ) 103 | embed.add_field(name="領取失敗", value="", inline=False) 104 | embed.add_field( 105 | name=f"使用者:{interaction.user.name}", 106 | value="請重新確認課程代碼", 107 | inline=False, 108 | ) 109 | embed.set_footer(text=" ") 110 | await interaction.response.send_message( 111 | embed=embed, ephemeral=True 112 | ) 113 | 114 | await interaction.response.send_modal(TokenModal(title="請輸入課程代碼")) 115 | 116 | @discord.slash_command(description="發送課程代碼兌換鈕") 117 | async def send_modal(self, ctx): 118 | if ctx.author.guild_permissions.administrator: 119 | embed = discord.Embed(color=0x4BE1EC) 120 | # pylint: disable-next = line-too-long 121 | embed.set_thumbnail( 122 | url="https://creazilla-store.fra1.digitaloceanspaces.com/emojis/56531/label-emoji-clipart-md.png" 123 | ) 124 | embed.add_field(name="點下方按鈕輸入token", value="", inline=False) 125 | embed.add_field(name="領取課程身分組!", value="", inline=False) 126 | await ctx.send(embed=embed, view=self.TokenVerifyButton()) 127 | 128 | @discord.slash_command(description="新增主題課程") 129 | # pylint: disable-next = too-many-arguments, too-many-positional-arguments 130 | async def add_class( 131 | self, ctx, class_code: str, name: str, theme: str, teacher: str, time: str 132 | ): 133 | if ctx.author.guild_permissions.administrator: 134 | d = {"name": name, "theme": theme, "teacher": teacher, "time": time} 135 | add_data(class_code, d) 136 | await ctx.respond( 137 | f"已將{name}新增至 JSON;主題:{theme},講師:{teacher},時間:{time}" 138 | ) 139 | 140 | 141 | def setup(bot): 142 | bot.add_cog(ClassRole(bot)) 143 | -------------------------------------------------------------------------------- /cog/admin_gift.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | from datetime import datetime 3 | import traceback 4 | 5 | # Third-party imports 6 | import discord 7 | from discord.ext import commands 8 | 9 | # Local imports 10 | from build.build import Build 11 | from cog.core.sql import link_sql, read, write, end 12 | from cog.core.sendgift import send_gift_button 13 | 14 | 15 | class SendGift(Build): 16 | @commands.Cog.listener() 17 | async def on_ready(self) -> None: 18 | self.bot.add_view(self.Gift()) 19 | 20 | # 禮物按鈕 21 | class Gift(discord.ui.View): 22 | def __init__(self): 23 | super().__init__(timeout=None) # Timeout of the view must be set to None 24 | self.type = None # 存放這個按鈕是送電電點還是抽獎券,預設 None ,在建立按鈕時會設定 see view.type = gift_type 25 | self.count = 0 # 存放這個按鈕是送多少電電點/抽獎券 26 | 27 | # 發送獎勵 28 | @staticmethod 29 | def __reward(uid: int, username: str, bonus_type: str, bonus: int) -> None: 30 | connection, cursor = link_sql() 31 | current_point = read(uid, bonus_type, cursor) 32 | write(uid, bonus_type, current_point + bonus, cursor) 33 | end(connection, cursor) 34 | print(f"{uid} {username} get {bonus} {bonus_type} by Gift {datetime.now()}") 35 | 36 | # 存資料庫存取按鈕屬性(包括獎勵類型、數量) 37 | def __get_btn_attr(self, btn_id: int): 38 | try: 39 | connection, cursor = link_sql() 40 | cursor.execute( 41 | f"SELECT type, count FROM `gift` WHERE `btnID`={btn_id} and `received`=0" 42 | ) 43 | ret = cursor.fetchall() 44 | if len(ret) == 0: 45 | return None, None 46 | cursor.execute(f"UPDATE `gift` SET `received`=1 WHERE `btnID`={btn_id}") 47 | end(connection, cursor) 48 | return ret[0][0], ret[0][1] # type, count 49 | except Exception as e: 50 | print(e) 51 | return None, None 52 | 53 | # 點擊後會觸發的動作 54 | @discord.ui.button( 55 | label="領取獎勵", style=discord.ButtonStyle.success, custom_id="get_gift" 56 | ) 57 | async def get_gift(self, button: discord.ui.Button, ctx) -> None: 58 | self.type, self.count = self.__get_btn_attr( 59 | ctx.message.id 60 | ) # 傳入按鈕的訊息 ID,得到按鈕的屬性 61 | if self.type is None or self.count is None: 62 | button.label = "出問題了" # Change the button's label to "已領取" 63 | button.disabled = True # 關閉按鈕,避免錯誤再被觸發 64 | await ctx.response.edit_message(view=self) 65 | button.disabled = True # 關閉按鈕,避免重複點擊 66 | print( 67 | f"{ctx.user.id},{ctx.user} throw error by get_gift {datetime.now()}" 68 | ) 69 | return await ctx.respond( 70 | "好像出了點問題,你可能已經領過或伺服器內部錯誤。若有異議請在收到此訊息兩天內截圖此畫面提交客服單回報", 71 | ephemeral=False, 72 | ) 73 | self.type = "point" if self.type == "電電點" else "ticket" 74 | self.__reward(ctx.user.id, ctx.user, self.type, self.count) 75 | # log 76 | button.label = "已領取" # Change the button's label to "已領取" 77 | button.disabled = True # 關閉按鈕,避免重複點擊 78 | await ctx.response.edit_message(view=self) 79 | 80 | def cache_users_by_name(self): 81 | # 將所有使用者名稱和對應的使用者物件存入字典 82 | return {user.name: user for user in self.bot.users} 83 | 84 | @discord.slash_command(name="發送禮物", description="dm_gift") 85 | async def send_dm_gift( 86 | self, 87 | ctx, 88 | target_str: discord.Option( 89 | str, "發送對象(用半形逗號分隔多個使用者名稱)", required=True 90 | ), 91 | gift_type: discord.Option(str, "送禮內容", choices=["電電點", "抽獎券"]), 92 | count: discord.Option(int, "數量"), 93 | ) -> None: 94 | if not ctx.author.guild_permissions.administrator: 95 | await ctx.respond("你沒有權限使用這個指令!", ephemeral=True) 96 | return 97 | SendGift.user_cache = self.cache_users_by_name() 98 | try: 99 | await ctx.defer() # 確保機器人請求不會超時 100 | # 不能發送負數 101 | if count <= 0: 102 | await ctx.respond("不能發送 0 以下個禮物!", ephemeral=True) 103 | return 104 | manager = ctx.author # return 105 | target_usernames = target_str.split(",") 106 | target_users = [] 107 | 108 | async def fetch_user_by_name(name): 109 | user_obj = discord.utils.find(lambda u: u.name == name, self.bot.users) 110 | if user_obj: 111 | try: 112 | return await self.bot.fetch_user(user_obj.id) 113 | except Exception as e: 114 | print(f"Failed to fetch user with ID {user_obj.id}: {str(e)}") 115 | return None 116 | 117 | for username in target_usernames: 118 | username = username.strip() 119 | if username not in SendGift.user_cache: 120 | continue 121 | try: 122 | user = await fetch_user_by_name(username) 123 | target_users.append(user) 124 | except (ValueError, Exception) as e: 125 | await ctx.respond(f"找不到使用者 : {username}{e}", ephemeral=True) 126 | return 127 | # DM 一個 Embed 和領取按鈕 128 | for target_user in target_users: 129 | await send_gift_button( 130 | self, target_user, gift_type, count, manager.name 131 | ) 132 | # 管理者介面提示 133 | await ctx.respond( 134 | f"{manager} 已發送 {count} {gift_type} 給 {', '.join([user.name for user in target_users])}" 135 | ) 136 | except Exception as e: 137 | traceback.print_exc() 138 | await ctx.respond(f"伺服器內部出現錯誤:{e}", ephemeral=True) 139 | 140 | 141 | def setup(bot): 142 | bot.add_cog(SendGift(bot)) 143 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | @import url(https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100..900&display=swap); 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | } 10 | body { 11 | font-family: "Noto Sans TC", sans-serif; 12 | font-optical-sizing: auto; 13 | position: relative; 14 | background: #000; 15 | min-height: 100svh; 16 | } 17 | a, 18 | body { 19 | color: #fff; 20 | } 21 | .avatar, 22 | .info { 23 | background: #fff; 24 | } 25 | .logo { 26 | height: 3rem; 27 | } 28 | h1 { 29 | font-size: 3.5rem; 30 | } 31 | h1 + p { 32 | margin-bottom: 2rem; 33 | font-size: 1.3rem; 34 | } 35 | footer, 36 | nav { 37 | display: flex; 38 | align-items: center; 39 | position: fixed; 40 | padding: 2rem; 41 | background-color: #e9aa11; 42 | } 43 | nav { 44 | top: 0; 45 | right: 0; 46 | border-radius: 0 0 0 3rem; 47 | font-size: 1.25rem; 48 | padding: 1.5rem 1.5rem 1.5rem 4rem; 49 | } 50 | .info h3, 51 | footer { 52 | font-size: 1.5rem; 53 | } 54 | nav a { 55 | margin-right: 4rem; 56 | } 57 | nav a.log { 58 | margin-right: 0; 59 | } 60 | .ticket { 61 | margin-right: 1rem; 62 | } 63 | footer { 64 | bottom: 0; 65 | left: 0; 66 | border-radius: 0 3rem 0 0; 67 | flex-direction: column; 68 | gap: 2rem; 69 | padding-block: 4rem; 70 | } 71 | footer a, 72 | nav a { 73 | transition: transform 0.3s ease-out; 74 | } 75 | .product-button:hover, 76 | button:hover, 77 | footer a:hover, 78 | nav a:hover { 79 | transform: scale(1.1); 80 | } 81 | .avatar { 82 | border-radius: 50%; 83 | height: 3.5rem; 84 | width: 3.5rem; 85 | color: transparent; 86 | margin-inline: 1rem; 87 | } 88 | a { 89 | text-decoration: none; 90 | } 91 | .welcome { 92 | margin-top: 1rem; 93 | font-size: 2rem; 94 | } 95 | main { 96 | max-width: 1650px; 97 | margin: auto; 98 | padding: 4rem; 99 | } 100 | section { 101 | display: flex; 102 | gap: 2rem; 103 | justify-content: center; 104 | flex-wrap: wrap; 105 | } 106 | article { 107 | display: flex; 108 | flex-direction: column; 109 | justify-content: space-between; 110 | background: #638971; 111 | width: 20rem; 112 | height: 30rem; 113 | border-radius: 2rem; 114 | padding: 0.5rem; 115 | } 116 | .product-button, 117 | .product-img, 118 | button { 119 | display: flex; 120 | justify-content: center; 121 | } 122 | .product-img { 123 | width: 100%; 124 | align-items: center; 125 | padding: 2rem; 126 | flex-grow: 1; 127 | height: 1rem; 128 | } 129 | .product-img img { 130 | width: calc(100% - 4rem); 131 | height: calc(100% - 4rem); 132 | object-fit: contain; 133 | } 134 | .info { 135 | color: #000; 136 | border-radius: 1.5rem; 137 | padding: 1.5rem; 138 | } 139 | .info p { 140 | font-size: 1rem; 141 | margin-bottom: 0.8rem; 142 | } 143 | .confirm-select button, 144 | .product-button, 145 | button { 146 | color: #fff; 147 | font-size: 1.25rem; 148 | cursor: pointer; 149 | height: 3rem; 150 | padding: 0 1rem; 151 | transition: transform 0.3s ease-out; 152 | } 153 | .info div { 154 | display: flex; 155 | justify-content: space-between; 156 | align-items: flex-end; 157 | } 158 | .product-button, 159 | button { 160 | background: #e9aa11; 161 | border: none; 162 | border-radius: 1.5rem; 163 | align-items: center; 164 | } 165 | .confirm, 166 | .confirm-select { 167 | justify-content: center; 168 | display: flex; 169 | } 170 | .confirm { 171 | position: fixed; 172 | top: 0; 173 | left: 0; 174 | width: 100%; 175 | height: 100%; 176 | background-color: rgba(0, 0, 0, 0); 177 | align-items: center; 178 | transition: background-color 0.3s ease-out; 179 | pointer-events: none; 180 | } 181 | .confirm.active { 182 | background-color: rgba(0, 0, 0, 0.5); 183 | pointer-events: all; 184 | } 185 | .confirm .content { 186 | background: #e9aa11; 187 | padding: 2rem; 188 | border-radius: 1rem; 189 | text-align: center; 190 | scale: 0; 191 | transition: scale 0.3s; 192 | } 193 | .confirm.active .content { 194 | scale: 1; 195 | } 196 | .warning { 197 | color: red; 198 | } 199 | .content span { 200 | font-weight: 700; 201 | } 202 | .confirm-select { 203 | gap: 1rem; 204 | margin-top: 2rem; 205 | } 206 | .confirm-select button { 207 | background: #189f49; 208 | border: none; 209 | border-radius: 1rem; 210 | display: flex; 211 | justify-content: center; 212 | align-items: center; 213 | } 214 | .confirm-select button:first-child { 215 | background: #747474; 216 | } 217 | .outStock { 218 | background: #7b7b7b; 219 | cursor: not-allowed; 220 | } 221 | .footer { 222 | margin-top: 4rem; 223 | text-align: center; 224 | } 225 | .zap { 226 | width: 1em; 227 | vertical-align: middle; 228 | } 229 | @media (max-width: 40em) { 230 | footer, 231 | section { 232 | gap: 1rem; 233 | } 234 | h1 { 235 | font-size: 2.5rem; 236 | } 237 | footer { 238 | padding: 2rem 1rem; 239 | border-radius: 0 1.5rem 0 0; 240 | } 241 | nav a { 242 | margin-right: 2rem; 243 | } 244 | nav { 245 | padding: 1rem 2rem; 246 | font-size: 1rem; 247 | border-radius: 0 0 0 1.5rem; 248 | } 249 | nav > :last-child { 250 | margin-right: 0; 251 | } 252 | .welcome { 253 | font-size: 1.5rem; 254 | } 255 | main { 256 | padding: 5rem 2rem 2rem; 257 | } 258 | article { 259 | width: 15rem; 260 | height: 25rem; 261 | } 262 | .info h3 { 263 | font-size: 1.25rem; 264 | } 265 | .info p { 266 | font-size: 0.9rem; 267 | } 268 | .info div { 269 | flex-direction: column; 270 | align-items: flex-start; 271 | } 272 | .confirm-select button, 273 | .product-button, 274 | button { 275 | font-size: 1rem; 276 | height: 2.5rem; 277 | } 278 | .confirm .content { 279 | padding: 1rem; 280 | } 281 | .footer { 282 | margin-top: 2rem; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 中電喵 SCAICT uwu 6 | 7 | # 中電喵 SCAICT uwu 8 | 9 | [![Sync issues to Notion](https://github.com/SCAICT/SCAICT-uwu/actions/workflows/notion.yml/badge.svg?event=issues)](https://github.com/SCAICT/SCAICT-uwu/actions/workflows/notion.yml) 10 | [![Website](https://img.shields.io/website?label=Website&&url=https%3A%2F%2Fscaict.org%2F)](https://scaict.org/) 11 | [![SCAICT Store](https://img.shields.io/website?label=SCAICT+store&&url=https%3A%2F%2Fstore.scaict.org%2F)](https://store.scaict.org/) 12 | [![Documentation](https://img.shields.io/website?label=Documentation&&url=https%3A%2F%2Fstore.scaict.org%2F)](https://g.scaict.org/doc/) 13 | [![Join Discord server](https://img.shields.io/discord/959823904266944562?label=Discord&logo=discord&)](https://dc.scaict.org) 14 | [![Follow Instagram](https://img.shields.io/badge/Follow-%40scaict.tw-pink?&logo=instagram)](https://www.instagram.com/scaict.tw/) 15 | 16 |
17 | 18 | # SCAICT-uwu 19 | 20 | SCAICT-uwu is a playful and interactive Discord bot that resides in the SCAICT Discord community. Designed to bring fun and utility to its users, SCAICT-uwu loves to sing, dance, code, and do math, making it an engaging companion for the community members. 21 | 22 | ## Overview 23 | 24 | SCAICT-uwu offers a range of interactive features, from daily activities to coding challenges. Whether you want to play games, solve puzzles, or simply chat, SCAICT-uwu is always ready to respond to your commands. 25 | 26 | ### About SCAICT 27 | 28 | SCAICT, Student Club's Association of Information in Central Taiwan, is an electronic engineering club composed of schools from Central Taiwan. By combining the resources of the Central Region, we actively organize educational events, activities, and competitions related to information technology, with the goal of facilitating the flow of technology and knowledge. 29 | 30 | ## Features 31 | 32 | ### Slash Commands 33 | 34 | Interact with SCAICT-uwu using slash commands in any channel where the bot is active. These commands are intuitive and designed to provide instant feedback or initiate specific bot actions. 35 | 36 |
37 | 38 | ![charge demo](https://raw.githubusercontent.com/SCAICT/doc/main/static/img/charge-demo.gif) 39 | 40 |
41 | 42 | ### Dedicated Channels 43 | 44 | Some interactions with SCAICT-uwu occur within dedicated channels, allowing for more focused activities such as guessing colors or counting. 45 | ![color guess](https://raw.githubusercontent.com/SCAICT/doc/main/static/img/color-demo.gif) 46 | 47 | ### Store 48 | 49 | #### Buy something 50 | 51 | You can buy some products like our stickers or USB drives using Electric Points. Note that the products currently can only be exchanged during in-person events. We will soon offer shipping options and more virtual rewards for redemption. 52 | 53 |
54 | 55 | ![store demo](https://raw.githubusercontent.com/SCAICT/doc/main/static/img/store-demo.png) 56 | 57 |
58 | 59 | #### Play slot 60 | 61 | Get some tickets, and you can play the slot machine to earn Electric Points. Just long press to start the slot. 62 |
63 | 64 | ![slot demo](https://raw.githubusercontent.com/SCAICT/doc/main/static/img/slot-demo.gif) 65 | 66 |
67 | 68 | ## Getting Started 69 | 70 | 1. **Join the Server** 71 | Start by joining the SCAICT Discord community using this [link](https://dc.scaict.org). 72 | 2. **Earn Your First Electric Points** 73 | Visit the `#🔌每日充電` (`everyDayCharge`) channel and use the `/charge` command to receive your first set of Electric Points, the primary currency used within the bot's ecosystem. 74 | 3. **Enjoy in Services** 75 | Explore the various commands and interactions SCAICT-uwu offers, and enjoy the space within the server, engaging and connecting with everyone. 76 | 77 | ## Usage Examples 78 | 79 | > For more detailed documentation, please refer to [this link](https://g.scaict.org/doc/docs/SCAICT-uwu/intro). 80 | 81 | ## How to Deploy? 82 | 83 | 1. Clone this repository. 84 | 2. Create an environment in Python 3.11. 85 | 3. Install the required libraries. 86 | 87 | ```bash 88 | pip install -r requirements.txt 89 | ``` 90 | 91 | 4. Configure the channels in `database/server.config.json`. 92 | 5. Start the SQL server. 93 | 6. Configure the SQL server in `cog/core/sql_acc.py` within Breadcrumbs SCAICT-uwu. 94 | 7. Run Flask. 95 | 96 | ```bash 97 | flask run 98 | ``` 99 | 100 | 8. Execute `main.py`. 101 | 102 | ```bash 103 | python main.py 104 | ``` 105 | 106 | ### Files 107 | 108 | * `main.py`: 中電喵. 109 | * `app.py`: 中電商店. 110 | * `generate_secrets.py`: Generates keys for `app.py`. After execution, the keys are stored in `token.json`. 111 | * Database MySQL: Uses an external server. Configuration is in `cog/core/secret.py`. 112 | * `token.json`: 113 | 114 | ```json 115 | { 116 | "discord_token": "", 117 | "secret_key": "", 118 | "discord_client_id": "", 119 | "discord_client_secret": "", 120 | "discord_redirect_uri": "http://127.0.0.1:5000/callback", 121 | "github_client_id": "", 122 | "github_client_secret": "", 123 | "github_redirect_uri": "http://127.0.0.1:5000/github/callback", 124 | "github_discord_redirect_uri": "http://127.0.0.1:5000/github/discord-callback" 125 | } 126 | ``` 127 | 128 | * `database/slot.json`: 129 | 130 | Configures the jackpot probabilities for the slot machine. 131 | 132 | ```json 133 | { 134 | "element": [ percentage, reward ] 135 | } 136 | ``` 137 | 138 | > For more detailed documentation, please refer to [this link](https://g.scaict.org/doc/docs/category/%E9%96%8B%E7%99%BC%E8%80%85%E5%B0%88%E5%8D%80) 139 | 140 | ## Acknowledgements 141 | 142 | SCAICT-uwu is a project jointly developed and maintained by SCAICT and [contributors](https://github.com/SCAICT/SCAICT-uwu/graphs/contributors). The character design was created by [毛哥 EM](https://elvismao.com/) and [瑞樹](https://www.facebook.com/ruishuowo), while some icons were sourced from [Freepik - Flaticon](https://www.flaticon.com/free-icons/slot-machine). 143 | -------------------------------------------------------------------------------- /docs/abstract_schema_table.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/schema#", 3 | "description": "Abstract description of a scaict-uwu database table, derived from MediaWiki database table schema", 4 | "type": "array", 5 | "additionalProperties": false, 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Name of the table" 10 | }, 11 | "comment": { 12 | "type": "string", 13 | "description": "Comment describing the table" 14 | }, 15 | "columns": { 16 | "type": "array", 17 | "additionalItems": false, 18 | "description": "Columns", 19 | "minItems": 1, 20 | "items": { 21 | "type": "object", 22 | "additionalProperties": false, 23 | "properties": { 24 | "name": { 25 | "type": "string", 26 | "description": "Name of the column" 27 | }, 28 | "comment": { 29 | "type": "string", 30 | "description": "Comment describing the column" 31 | }, 32 | "type": { 33 | "type": "string", 34 | "description": "Data type of the column", 35 | "enum": [ 36 | "bigint", 37 | "binary", 38 | "blob", 39 | "boolean", 40 | "date", 41 | "datetime", 42 | "datetimetz", 43 | "decimal", 44 | "enum", 45 | "float", 46 | "integer", 47 | "smallint", 48 | "string", 49 | "text", 50 | "tinyint" 51 | ] 52 | }, 53 | "options": { 54 | "type": "object", 55 | "description": "Additional options", 56 | "additionalProperties": false, 57 | "properties": { 58 | "autoincrement": { 59 | "type": "boolean", 60 | "description": "Indicates if the field should use an autoincremented value if no value was provided", 61 | "default": false 62 | }, 63 | "default": { 64 | "type": [ 65 | "number", 66 | "string", 67 | "null" 68 | ], 69 | "description": "The default value of the column if no value was specified", 70 | "default": null 71 | }, 72 | "fixed": { 73 | "type": "boolean", 74 | "description": "Indicates if the column should have a fixed length", 75 | "default": false 76 | }, 77 | "length": { 78 | "type": "number", 79 | "description": "Length of the field.", 80 | "default": null, 81 | "minimum": 0 82 | }, 83 | "enum": { 84 | "type": "array", 85 | "items": { 86 | "type": "string" 87 | }, 88 | "uniqueItems": true 89 | }, 90 | "notnull": { 91 | "type": "boolean", 92 | "description": "Indicates whether the column is nullable or not", 93 | "default": true 94 | }, 95 | "unsigned": { 96 | "type": "boolean", 97 | "description": "If the column should be an unsigned integer", 98 | "default": false 99 | }, 100 | "scale": { 101 | "type": "number", 102 | "description": "Exact number of decimal digits to be stored in a decimal type column", 103 | "default": 0 104 | }, 105 | "precision": { 106 | "type": "number", 107 | "description": "Precision of a decimal type column that determines the overall maximum number of digits to be stored (including scale)", 108 | "default": 10 109 | }, 110 | "PlatformOptions": { 111 | "type": "object", 112 | "additionalProperties": false, 113 | "properties": { 114 | "version": { 115 | "type": "boolean" 116 | } 117 | } 118 | }, 119 | "CustomSchemaOptions": { 120 | "type": "object", 121 | "description": "Custom schema options", 122 | "additionalProperties": false, 123 | "properties": { 124 | "allowInfinite": { 125 | "type": "boolean" 126 | }, 127 | "doublePrecision": { 128 | "type": "boolean" 129 | }, 130 | "enum_values": { 131 | "type": "array", 132 | "description": "Values to use with type 'enum'", 133 | "additionalItems": false, 134 | "items": { 135 | "type": "string" 136 | }, 137 | "uniqueItems": true 138 | } 139 | } 140 | } 141 | } 142 | } 143 | }, 144 | "required": [ 145 | "name", 146 | "type", 147 | "options" 148 | ] 149 | } 150 | }, 151 | "indexes": { 152 | "type": "array", 153 | "additionalItems": false, 154 | "description": "Indexes", 155 | "items": { 156 | "type": "object", 157 | "additionalProperties": false, 158 | "properties": { 159 | "name": { 160 | "type": "string", 161 | "description": "Index name" 162 | }, 163 | "comment": { 164 | "type": "string", 165 | "description": "Comment describing the index" 166 | }, 167 | "columns": { 168 | "type": "array", 169 | "additionalItems": false, 170 | "description": "Columns used by the index", 171 | "items": { 172 | "type": "string" 173 | }, 174 | "uniqueItems": true 175 | }, 176 | "unique": { 177 | "type": "boolean", 178 | "description": "If the index is unique", 179 | "default": false 180 | }, 181 | "flags": { 182 | "type": "array", 183 | "items": { 184 | "type": "string", 185 | "enum": [ 186 | "fulltext", 187 | "spatial" 188 | ] 189 | }, 190 | "uniqueItems": true 191 | }, 192 | "options": { 193 | "type": "object", 194 | "properties": { 195 | "lengths": { 196 | "type": "array", 197 | "items": { 198 | "type": [ 199 | "number", 200 | "null" 201 | ] 202 | }, 203 | "minItems": 1 204 | } 205 | } 206 | } 207 | }, 208 | "required": [ 209 | "name", 210 | "columns", 211 | "unique" 212 | ] 213 | } 214 | }, 215 | "pk": { 216 | "type": "array", 217 | "additionalItems": false, 218 | "description": "Array of column names used in the primary key", 219 | "items": { 220 | "type": "string" 221 | }, 222 | "uniqueItems": true 223 | }, 224 | "table_options": { 225 | "type": "array", 226 | "items": { 227 | "type": "string" 228 | } 229 | } 230 | }, 231 | "required": [ 232 | "name", 233 | "columns", 234 | "indexes" 235 | ] 236 | } 237 | -------------------------------------------------------------------------------- /cog/ticket.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | import asyncio 3 | 4 | # Third-party imports 5 | import discord 6 | from discord.ext import commands 7 | 8 | # Local imports 9 | from build.build import Build 10 | 11 | 12 | # ticket 頻道 13 | class Ticket(Build): 14 | @commands.Cog.listener() 15 | async def on_ready(self): 16 | self.bot.add_view(self.TicketView()) 17 | self.bot.add_view(self.CloseView()) 18 | self.bot.add_view(self.DelView()) 19 | 20 | ## del cahnnel button 21 | class DelView(discord.ui.View): 22 | def __init__(self): 23 | super().__init__(timeout=None) # timeout of the view must be set to Nones 24 | 25 | @discord.ui.button( 26 | label="刪除頻道", style=discord.ButtonStyle.red, emoji="🗑️", custom_id="del" 27 | ) 28 | # pylint: disable-next = unused-argument 29 | async def button_callback(self, button, interaction): 30 | embed = discord.Embed(color=0xFF0000) 31 | embed.add_field(name="將於幾秒後刪除", value=" ", inline=False) 32 | await interaction.response.send_message(embed=embed) 33 | await asyncio.sleep(3) 34 | await interaction.channel.delete() 35 | 36 | ## close button 37 | class CloseView(discord.ui.View): 38 | def __init__(self): 39 | super().__init__(timeout=None) # timeout of the view must be set to Nones 40 | 41 | @discord.ui.button( 42 | label="關閉表單", 43 | style=discord.ButtonStyle.red, 44 | emoji="🔒", 45 | custom_id="close", 46 | ) 47 | # pylint: disable-next = unused-argument 48 | async def button_callback(self, button, interaction): 49 | user = interaction.user 50 | channel = interaction.channel 51 | 52 | # 這裡可以加入你的權限處理邏輯 53 | # 這裡是一個範例:將使用者的檢視權限設定為 False 54 | embed = discord.Embed(color=0xFF0A0A) 55 | embed.add_field(name="已成功關閉頻道", value=" ", inline=False) 56 | await channel.send(embed=embed) 57 | await channel.set_permissions(user, read_messages=False) 58 | 59 | # 回覆使用者,表示已完成操作 60 | 61 | role = discord.utils.get(interaction.guild.roles, name="root") 62 | embed = discord.Embed(color=0xFFF700) 63 | embed.add_field(name="請確認並刪除頻道", value=" ", inline=False) 64 | await interaction.response.send_message( 65 | role.mention, embed=embed, view=Ticket.DelView() 66 | ) # 修改這裡,使用 Ticket.DelView() 67 | 68 | ## create ticket button 69 | class TicketView(discord.ui.View): 70 | def __init__(self): 71 | super().__init__(timeout=None) # timeout of the view must be set to None 72 | 73 | @discord.ui.button( 74 | label="點擊開單", 75 | style=discord.ButtonStyle.blurple, 76 | emoji="📩", 77 | custom_id="ticket", 78 | ) 79 | # pylint: disable-next = unused-argument 80 | async def button_callback(self, button, interaction): 81 | await self.create_ticket_channel(interaction, "開單") 82 | 83 | # pylint: disable-next = unused-argument 84 | async def create_ticket_channel(self, interaction, button_name): 85 | user = interaction.user 86 | guild = interaction.guild 87 | target_category_name = "開單處" 88 | 89 | existing_channels = [ 90 | channel 91 | for channel in guild.text_channels 92 | if channel.name.startswith(interaction.user.name) 93 | ] 94 | 95 | if existing_channels: 96 | await interaction.response.send_message( 97 | "你已經有建立頻道了!", ephemeral=True 98 | ) 99 | return 100 | 101 | # 建立頻道名稱 102 | channel_name = f"{interaction.user.name}的ticket頻道" 103 | 104 | # 取得或建立目標類別 105 | category = discord.utils.get(guild.categories, name=target_category_name) 106 | if category is None: 107 | category = await guild.create_category(target_category_name) 108 | 109 | # 建立頻道 110 | overwrites = { 111 | guild.default_role: discord.PermissionOverwrite(read_messages=False), 112 | user: discord.PermissionOverwrite(read_messages=True), 113 | } 114 | 115 | channel = await category.create_text_channel( 116 | name=channel_name, overwrites=overwrites 117 | ) 118 | 119 | # 向頻道傳送歡迎訊息 120 | embed = discord.Embed(color=0x4AF750) 121 | embed.add_field(name="請闡述你的問題 並等待回覆!", value="", inline=False) 122 | embed.add_field( 123 | name="若需關閉客服單 可以點擊下方按鈕 🔒 關閉", value="", inline=False 124 | ) 125 | await channel.send( 126 | f"這裡是{user.mention}的頻道", embed=embed, view=Ticket.CloseView() 127 | ) # 修改這裡,使用 Ticket.CloseView() 128 | 129 | await interaction.response.send_message( 130 | f"已建立 {channel.mention}!", ephemeral=True 131 | ) 132 | 133 | @discord.slash_command() 134 | async def create_ticket_button(self, ctx): 135 | if ctx.author.guild_permissions.administrator: 136 | 137 | # 修改這裡,使用 Ticket.TicketView() 138 | embed = discord.Embed(title=" ", color=0xFEFCB6) 139 | embed.set_thumbnail( 140 | url="https://cdn-icons-png.flaticon.com/512/2067/2067179.png" 141 | ) 142 | embed.add_field(name="SCAICT-Discord", value=" ", inline=False) 143 | embed.add_field(name="客服單", value="", inline=False) 144 | embed.add_field(name=" ", value=" ", inline=False) 145 | embed.add_field( 146 | name="----- 什麼時候可以按這個酷酷的按鈕? -----", 147 | value=" ", 148 | inline=False, 149 | ) 150 | embed.add_field(name=" ", value=" ", inline=False) 151 | embed.add_field( 152 | name="各種伺服器內疑難雜症:包括但不限於 不當言行檢舉、領獎、活動轉發、贊助", 153 | value="", 154 | inline=False, 155 | ) 156 | embed.add_field(name=" ", value=" ", inline=False) 157 | embed.add_field( 158 | name="課程問題:當你對中電會課程的報名、上課通知有疑慮時可以點我詢問", 159 | value="", 160 | inline=False, 161 | ) 162 | embed.add_field(name=" ", value=" ", inline=False) 163 | embed.add_field( 164 | name="----------------- 注意事項 --------------------", 165 | value=" ", 166 | inline=False, 167 | ) 168 | embed.add_field(name=" ", value=" ", inline=False) 169 | embed.add_field( 170 | name="請不要隨意開啟客服單,若屢勸不聽將會扣電電點,嚴重者會踢出伺服器", 171 | value="", 172 | inline=False, 173 | ) 174 | embed.add_field(name=" ", value=" ", inline=False) 175 | embed.add_field(name=" ", value=" ", inline=False) 176 | embed.set_footer(text="所有客服單將自動留存,以保障雙方權益。") 177 | await ctx.respond(embed=embed, view=Ticket.TicketView()) 178 | 179 | 180 | def setup(bot): 181 | bot.add_cog(Ticket(bot)) 182 | -------------------------------------------------------------------------------- /cog/core/sql_abstract.py: -------------------------------------------------------------------------------- 1 | # Currently the database column naming is a mess,\ 2 | # let's keep that mess for now and probably fix them later. 3 | # pylint: disable = invalid-name, too-many-instance-attributes, broad-exception-raised 4 | 5 | from __future__ import annotations 6 | from abc import ABC, abstractmethod 7 | from collections import UserDict 8 | from datetime import datetime, date 9 | from dataclasses import dataclass, is_dataclass, fields 10 | from typing import Any, Generic, TypeVar 11 | 12 | from cog.core.sql import fetchone_by_primary_key, write, mysql_connection 13 | from cog.core.singleton import SingletonMeta 14 | 15 | 16 | # TODO: extend if we need to judge the unset value is nullable 17 | class UnsetSentinel(metaclass=SingletonMeta): 18 | def __repr__(self): 19 | return "UNSET" 20 | 21 | 22 | UNSET = UnsetSentinel() 23 | 24 | 25 | class Unsettable: 26 | def __getattribute__(self, name): 27 | value = super().__getattribute__(name) 28 | if value is UNSET: 29 | raise UnsetError(f"Attribute '{name}' is not set") 30 | return value 31 | 32 | def is_unset(self, attr_name): 33 | return super().__getattribute__(attr_name) is UNSET 34 | 35 | 36 | class UnsetError(Exception): 37 | pass 38 | 39 | 40 | DataclassT = TypeVar("DataclassT") 41 | 42 | 43 | # TODO: optimize with attr module 44 | class AttributeKeyedDict(UserDict, Generic[DataclassT]): 45 | def __init__(self, primary_key_name: str, *args, **kwargs): 46 | if not is_dataclass(DataclassT): 47 | raise TypeError( 48 | "KeyedDict should be specified a dataclass as the type of the item." 49 | ) 50 | 51 | if not primary_key_name in DataclassT.__annotations__: 52 | raise AttributeError( 53 | f"The field `{primary_key_name}` is not announced in the dataclass {DataclassT}" 54 | ) 55 | 56 | self.primary_key_name = primary_key_name 57 | super().__init__(*args, **kwargs) 58 | 59 | def __setitem__(self, key, item: DataclassT): 60 | raise RuntimeError("Please use `set()` method to set records") 61 | 62 | def set(self, record: DataclassT): 63 | identifier = getattr(record, self.primary_key_name) 64 | return super().__setitem__(identifier, record) 65 | 66 | 67 | def is_protected_name(name: str) -> bool: 68 | return name.startswith("_") and not name.startswith("__") and not name.endswith("_") 69 | 70 | 71 | class ProtectedAttrReadOnlyMixin: 72 | def __getattribute__(self, name: str) -> Any: 73 | return super().__getattribute__(name) 74 | 75 | def __setattr__(self, name: str, value): 76 | is_protected = is_protected_name(name) 77 | if not is_protected or self.__dict__ is None: 78 | return super().__setattr__(name, value) 79 | 80 | is_unset = self.is_unset(name) if isinstance(self, Unsettable) else False 81 | 82 | is_init = (name in self.__dict__) and not is_unset 83 | 84 | if is_protected and is_init: 85 | raise AttributeError( 86 | f"The protected attribute `{name}` should be read-only after initialization." 87 | ) 88 | return super().__setattr__(name, value) 89 | 90 | 91 | class SQLTable(ABC): 92 | @staticmethod 93 | @abstractmethod 94 | def from_sql(unique_id): ... 95 | 96 | @abstractmethod 97 | def to_sql(self): ... 98 | 99 | 100 | @dataclass 101 | class UserRecord(SQLTable, Unsettable, ProtectedAttrReadOnlyMixin): 102 | uid: int # required 103 | 104 | DCname: str | None = UNSET # pyright: ignore[reportAssignmentType] 105 | DCMail: str | None = UNSET # pyright: ignore[reportAssignmentType] 106 | githubName: str | None = UNSET # pyright: ignore[reportAssignmentType] 107 | githubMail: str | None = UNSET # pyright: ignore[reportAssignmentType] 108 | loveuwu: bool = UNSET # pyright: ignore[reportAssignmentType] 109 | point: int = UNSET # pyright: ignore[reportAssignmentType] 110 | ticket: int = UNSET # pyright: ignore[reportAssignmentType] 111 | charge_combo: int = UNSET # pyright: ignore[reportAssignmentType] 112 | next_lottery: int = UNSET # pyright: ignore[reportAssignmentType] 113 | last_charge: datetime = UNSET # pyright: ignore[reportAssignmentType] 114 | last_comment: date = UNSET # pyright: ignore[reportAssignmentType] 115 | today_comments: int = UNSET # pyright: ignore[reportAssignmentType] 116 | admkey: str | None = UNSET # pyright: ignore[reportAssignmentType] 117 | 118 | # if true, readonly after init (default: False) 119 | _protected: bool = UNSET # pyright: ignore[reportAssignmentType] 120 | 121 | def __post_init__(self): 122 | if self.is_unset("_protected"): 123 | self._protected = False 124 | 125 | def __eq__(self, value: object) -> bool: 126 | if not isinstance(value, UserRecord): 127 | return False 128 | 129 | for field in fields(UserRecord): 130 | is_unset_a = self.is_unset(field.name) 131 | is_unset_b = value.is_unset(field.name) 132 | if is_unset_a != is_unset_b: 133 | return False 134 | 135 | if (is_unset_a == is_unset_b == False) and ( 136 | getattr(self, field.name) != getattr(value, field.name) 137 | ): 138 | return False 139 | 140 | return True 141 | 142 | # won't place default value unless use default() to ensure safety 143 | # TODO: UserRecord.from_sql(uid).or_default() or UserRecord.from_sql_or_default(uid) 144 | @staticmethod 145 | def default(uid): 146 | return UserRecord( 147 | uid=uid, 148 | DCname=None, 149 | DCMail=None, 150 | githubName=None, 151 | githubMail=None, 152 | loveuwu=False, 153 | point=0, 154 | ticket=1, 155 | charge_combo=0, 156 | next_lottery=0, 157 | last_charge=datetime(1970, 1, 1, 0, 0, 0), 158 | last_comment=date(1970, 1, 1), 159 | today_comments=0, 160 | admkey=None, 161 | ) 162 | 163 | @staticmethod 164 | def from_sql( # pylint: disable=arguments-renamed # pyright: ignore[reportIncompatibleMethodOverride] 165 | uid: int, 166 | ): 167 | 168 | data = fetchone_by_primary_key("user", "uid", uid) 169 | if data is None: 170 | return None 171 | 172 | record = UserRecord( 173 | _protected=True, **data # pyright: ignore[reportArgumentType] 174 | ) 175 | 176 | for field in fields(record): 177 | if record.is_unset(field.name): 178 | raise Exception( 179 | f"SQL is not return all fields. (`{field.name}`=`{getattr(record, field.name)}`)" 180 | ) 181 | 182 | return record 183 | 184 | def to_sql(self): 185 | if self._protected: 186 | raise ValueError("You should new a object to modify.") 187 | self.to_sql_unsafe() 188 | 189 | def to_sql_unsafe(self): 190 | with mysql_connection() as c: 191 | _, cursor = c 192 | for field in fields(self): 193 | if is_protected_name(field.name): # _protected 194 | continue 195 | 196 | try: 197 | # only write changed value by check if value is unset or not 198 | value = getattr(self, field.name) 199 | write(self.uid, field.name, value, cursor) 200 | except UnsetError: 201 | continue 202 | # except MySQLError: 203 | # False 204 | 205 | # return True 206 | 207 | 208 | class UserRecordDict(AttributeKeyedDict[UserRecord]): 209 | def __init__(self): 210 | super().__init__("uid") 211 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 中電商店 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 42 | 43 | 44 | 45 | 59 |
60 | 61 |
嗨 {{ username }}!
62 |

歡迎來到中電商店

63 |

以下為目前電電點可兌換的內容

64 |
65 | 66 | 載入中... 67 |
68 | 69 |
70 |
71 | 72 | 73 | 74 |
75 |
76 |
77 |

確認兌換

78 |

79 | 你確定要花購買嗎? 80 |

81 |
82 | 此為實體周邊,請於實體活動拿到獎品後再點擊兌換,否則不補發獎品。 83 |
84 |
85 | 88 | 89 |
90 |
91 |
92 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /cog/daily_charge.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | # import csv 3 | from datetime import datetime, timedelta 4 | import json 5 | import os 6 | from typing import cast 7 | 8 | # Third-party imports 9 | import discord 10 | from discord.ext import commands 11 | from discord.user import _UserTag 12 | from mysql.connector.abstracts import MySQLCursorAbstract 13 | 14 | # Local imports 15 | from cog.core.downtime import get_downtime_list, write_downtime_list, get_history 16 | from cog.core.sql import write, read, mysql_connection 17 | from cog.core.sql_abstract import UserRecord 18 | 19 | 20 | def get_channels(): # 取得特殊用途頻道的清單,這裡會用來判斷是否在簽到頻道簽到,否則不予受理 21 | with open( 22 | f"{os.getcwd()}/database/server.config.json", "r", encoding="utf-8" 23 | ) as file: 24 | return json.load(file)["SCAICT-alpha"]["channel"] 25 | 26 | 27 | class Charge(commands.Cog): 28 | def __init__(self, bot: discord.Bot): 29 | self.bot = bot 30 | 31 | async def restore_downtime_point(self): 32 | downtime_list = get_downtime_list() 33 | unprocessed_downtime_list = list( 34 | filter(lambda x: not x.is_restored, downtime_list) 35 | ) 36 | 37 | if not unprocessed_downtime_list: 38 | return 39 | 40 | earliest_downtime_start = min( 41 | unprocessed_downtime_list, key=lambda downtime: downtime.start 42 | ).start 43 | # TODO: now() or end but with some condition 44 | latest_downtime_end = datetime.now() 45 | 46 | charge_channel_id = get_channels()["everyDayCharge"] 47 | messages = await get_history( 48 | self.bot, 49 | charge_channel_id, 50 | after=earliest_downtime_start, 51 | before=latest_downtime_end, 52 | ) 53 | 54 | # TODO: cache the last_charged date 55 | # cached_change: UserDict = UserDict() 56 | with mysql_connection() as c: 57 | connection, cursor = c 58 | # XXX: check with a better method, because the module on the running machine have no `_connection` attribute :skull: 59 | assert ( 60 | connection.autocommit is False 61 | ), "Unsafe operation at restoring downtime point due to commit automatically." 62 | 63 | for message in messages: 64 | created_time: datetime = message.created_at.astimezone() 65 | author = message.author 66 | 67 | assert self.bot.user, "Bot was not logged in." 68 | if author.id == self.bot.user.id: 69 | continue 70 | 71 | last_charge = self.get_last_charged(author, cursor) 72 | already_charged = created_time.date() == last_charge.date() 73 | if already_charged: 74 | continue 75 | 76 | if any( 77 | message.created_at in downtime 78 | for downtime in unprocessed_downtime_list 79 | ): 80 | user_data = UserRecord.from_sql(author.id) or UserRecord.default( 81 | author.id 82 | ) 83 | 84 | delta_record = self.reward( 85 | author, 86 | last_charge, 87 | created_time, 88 | user_data.charge_combo, 89 | user_data.point, 90 | user_data.ticket, 91 | cursor, 92 | is_forgivable=True, 93 | ) 94 | 95 | # TODO: send the message together, or there may have problem about send but not modify 96 | embed = self.embed_successful( 97 | delta_record.point, delta_record.charge_combo, author 98 | ) 99 | await message.reply(embed=embed, silent=True) 100 | 101 | connection.commit() 102 | # end loop 103 | # modify all "is_restored" of each data from datelist 104 | restored_downtime_list = [ 105 | downtime.marked_as_restored() for downtime in downtime_list 106 | ] 107 | write_downtime_list(restored_downtime_list) 108 | 109 | # commit and close the connection 110 | 111 | def embed_channel_error(self): 112 | embed = discord.Embed(color=0xFF0000) 113 | embed.set_thumbnail(url="https://http.cat/images/404.jpg") 114 | embed.add_field(name="這裡似乎沒有打雷…", value=" ⛱️", inline=False) 115 | embed.add_field(name="到「每日充電」頻道試試吧!", value="", inline=False) 116 | 117 | return embed 118 | 119 | def embed_already_charged(self, user: discord.User | discord.Member): 120 | embed = discord.Embed(color=0xFF0000) 121 | if user.avatar is not None: # 預設頭像沒有這個 122 | embed.set_thumbnail(url=str(user.avatar)) 123 | embed.add_field( 124 | name="您夠電了,明天再來!", value="⚡⚡⚡🛐🛐🛐", inline=False 125 | ) 126 | return embed 127 | 128 | def embed_successful(self, point, combo, user: discord.User | discord.Member): 129 | # 讀表符ID 130 | with open( 131 | f"{os.getcwd()}/database/server.config.json", "r", encoding="utf-8" 132 | ) as file: 133 | stickers = json.load(file)["SCAICT-alpha"]["stickers"] 134 | 135 | embed = discord.Embed( 136 | title=f"{user.name}剛剛充電了!", 137 | description="", 138 | color=0x14E15C, 139 | ) 140 | 141 | if user.avatar is not None: # 預設頭像沒有這個 142 | embed.set_thumbnail(url=str(user.avatar)) 143 | 144 | embed.add_field( 145 | name="", 146 | value=f":battery:+5{stickers['zap']}= " + str(point) + f"{stickers['zap']}", 147 | inline=False, 148 | ) 149 | embed.add_field( 150 | name="連續登入獎勵: " + str(combo) + "/" + str(combo + 7 - combo % 7), 151 | value="\n", 152 | inline=False, 153 | ) 154 | embed.set_footer(text=f"{user.name}充電成功!") 155 | 156 | return embed 157 | 158 | @staticmethod 159 | def is_forgivable(last_charge: datetime) -> bool: 160 | # TODO: implement by add a table column called `is_forgivable` to control 161 | """Return if user cannot charge due to downtime 162 | 163 | For example, if downtime is from 2025-03-14 09:03:00 to 2025-04-11 21:00:00, 164 | return True for all users who have charged between 2025-03-13(yesterday) to 2025-03-14(downtime.start), 165 | if is_forgivable is True, you won't loss combo. 166 | but after next charge, is_forgivable will be False, 167 | because user will execute not at downtime, if downtime is correct 168 | """ 169 | downtime_list = get_downtime_list() 170 | return any( 171 | downtime.start.date() - timedelta(days=1) 172 | <= last_charge.date() 173 | <= downtime.start.date() 174 | for downtime in downtime_list 175 | ) 176 | 177 | @staticmethod 178 | def is_cross_day(last_charge: datetime, executed_at: datetime): 179 | assert executed_at >= last_charge 180 | return executed_at.date() - last_charge.date() > timedelta(days=1) 181 | 182 | @staticmethod 183 | def is_already_charged(last_charge: datetime, executed_at: datetime): 184 | assert executed_at >= last_charge 185 | return executed_at.date() == last_charge.date() 186 | 187 | # TODO: inherit a MySQLCursorAbstract to add method about these or consider to add self.cursor 188 | @staticmethod 189 | # pylint: disable-next = too-many-positional-arguments 190 | def reward( 191 | user_or_uid: _UserTag | int, 192 | last_charge: datetime, 193 | executed_at: datetime, 194 | orig_combo: int, 195 | orig_point: int, 196 | orig_ticket: int, 197 | cursor: MySQLCursorAbstract | None = None, 198 | is_forgivable: bool = False, 199 | testing: bool = False, 200 | ) -> UserRecord: 201 | 202 | if testing: 203 | if not isinstance(user_or_uid, int): 204 | raise ValueError("You should give a UID during test.") 205 | uid = user_or_uid 206 | # TODO: emulate cursor 207 | if cursor is not None: 208 | raise ValueError("Database should not be changed during test.") 209 | else: 210 | if not isinstance(user_or_uid, _UserTag): 211 | raise ValueError( 212 | "You should give a User or Member object, or other object inherit _UserTag, to get id." 213 | ) 214 | uid = user_or_uid.id 215 | 216 | if cursor is None: 217 | raise ValueError("You should give a cursor for writing data to sql.") 218 | 219 | delta_record = UserRecord(uid) 220 | 221 | combo = ( 222 | 1 223 | if not is_forgivable and Charge.is_cross_day(last_charge, executed_at) 224 | else orig_combo + 1 225 | ) 226 | point = orig_point + 5 227 | 228 | ticket = orig_ticket 229 | if combo % 7 == 0: 230 | ticket += 4 231 | delta_record.ticket = ticket 232 | # refactor with UserRecord.to_sql 233 | if not testing: 234 | write(uid, "ticket", ticket, cursor) 235 | 236 | delta_record.last_charge = executed_at 237 | delta_record.charge_combo = combo 238 | delta_record.point = point 239 | 240 | if not testing: 241 | write(uid, "last_charge", executed_at, cursor) 242 | write(uid, "charge_combo", combo, cursor) 243 | write(uid, "point", point, cursor) 244 | 245 | # 紀錄log 246 | # TODO: record both executed time and datetime.now() after logger is implemented 247 | # pylint: disable-next = line-too-long 248 | if not testing: 249 | user = user_or_uid 250 | print(f"{uid},{user} Get 5 point by daily_charge {datetime.now()}") 251 | 252 | return delta_record 253 | 254 | # TODO: inherit a MySQLCursorAbstract to add method about these or consider to add self.cursor 255 | def get_last_charged(self, user: discord.User | discord.Member, cursor): 256 | last_charge = read( 257 | user.id, "last_charge", cursor 258 | ) # SQL回傳型態: 259 | # strptime轉型後: 260 | last_charge = datetime.strptime(str(last_charge), "%Y-%m-%d %H:%M:%S") 261 | 262 | return last_charge 263 | 264 | @discord.slash_command(name="charge", description="每日充電") 265 | async def charge(self, interaction): 266 | interaction = cast(discord.Interaction, interaction) 267 | 268 | assert ( 269 | interaction.user 270 | ), "Interaction may be in PING interactions, so that interaction.user is invalid." 271 | assert interaction.channel, "There are no channel returned from interation." 272 | 273 | # TODO: Use UserRecord or its extension to manage user data uniformly 274 | if interaction.channel.id != get_channels()["everyDayCharge"]: 275 | embed = self.embed_channel_error() 276 | # 其他文案:這裡似乎離無線充電座太遠了,到「每日充電」頻道試試吧! 待商議 277 | await interaction.response.send_message(embed=embed, ephemeral=True) 278 | # End connection instead of return 279 | return 280 | 281 | with mysql_connection() as c: # SQL 會話 282 | _, cursor = c 283 | user = interaction.user 284 | 285 | # get now time and combo 286 | now = datetime.now().replace(microsecond=0) 287 | 288 | last_charge = self.get_last_charged(user, cursor) 289 | already_charged = self.is_already_charged(last_charge, now) 290 | 291 | if already_charged: 292 | embed = self.embed_already_charged(user) 293 | await interaction.response.send_message(embed=embed, ephemeral=True) 294 | 295 | return 296 | 297 | combo: int = read( 298 | user.id, "charge_combo", cursor 299 | ) # 連續登入 # pyright: ignore[reportAssignmentType] 300 | point: int = read( 301 | user.id, "point", cursor 302 | ) # pyright: ignore[reportAssignmentType] 303 | ticket: int = read( 304 | user.id, "ticket", cursor 305 | ) # pyright: ignore[reportAssignmentType] 306 | 307 | is_forgivable = self.is_forgivable(last_charge) 308 | 309 | changed = self.reward( 310 | user, last_charge, now, combo, point, ticket, cursor, is_forgivable 311 | ) 312 | 313 | embed = self.embed_successful(changed.point, changed.charge_combo, user) 314 | await interaction.response.send_message(embed=embed) 315 | 316 | 317 | def setup(bot: discord.Bot): 318 | bot.add_cog(Charge(bot)) 319 | -------------------------------------------------------------------------------- /docs/database_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SCAICT-uwu database table layout diagram 6 | 168 | 169 | 170 |
171 |
172 |

Database schema of SCAICT-uwu.

173 |
174 |
175 |
176 |
177 |

User

178 |
179 | 180 | 181 | 182 | 204 | 205 | 206 |
183 |
184 |
185 |

user

186 |
187 |
    188 |
  • DCname VARCHAR(32)
  • 189 |
  • uid BIGINT
  • 190 |
  • DCMail VARCHAR(320)
  • 191 |
  • githubName VARCHAR(39)
  • 192 |
  • githubMail VARCHAR(320)
  • 193 |
  • loveuwu TINYINT(1)
  • 194 |
  • point INT
  • 195 |
  • ticket INT
  • 196 |
  • charge_combo INT
  • 197 |
  • next_lottery INT
  • 198 |
  • last_charge DATETIME
  • 199 |
  • last_comment DATE
  • 200 |
  • today_comments INT
  • 201 |
202 |
203 |
207 |
208 |
209 |
210 |

Comment points

211 |
212 | 213 | 214 | 215 | 229 | 230 | 231 |
216 |
217 |
218 |

comment_points

219 |
220 |
    221 |
  • 222 | seq INT
  • 223 |
  • uid BIGINT
  • 224 |
  • times INT
  • 225 |
  • next_reward INT
  • 226 |
227 |
228 |
232 |
233 |
234 |
235 |

Game

236 |
237 | 238 | 239 | 240 | 255 | 256 | 257 |
241 |
242 |
243 |

game

244 |
245 |
    246 |
  • 247 | seq BIGINT
  • 248 |
  • lastID BIGINT
  • 249 |
  • niceColor VARCHAR(3)
  • 250 |
  • nicecolorround INT
  • 251 |
  • niceColorCount BIGINT
  • 252 |
253 |
254 |
258 |
259 |
260 |
261 |

Gift

262 |
263 | 264 | 265 | 266 | 282 | 283 | 284 |
267 |
268 |
269 |

gift

270 |
271 |
    272 |
  • 273 | btnID BIGINT
  • 274 |
  • type ENUM(…)
  • 275 |
  • count INT
  • 276 |
  • recipient VARCHAR(32)
  • 277 |
  • received TINYINT(1)
  • 278 |
  • sender VARCHAR(32)
  • 279 |
280 |
281 |
285 |
286 |
287 |
288 |

CTF

289 |
290 | 291 | 292 | 293 | 313 | 327 | 328 | 329 |
294 |
295 |
296 |

ctf_data

297 |
298 |
    299 |
  • 300 | id BIGINT
  • 301 |
  • flags VARCHAR(255)
  • 302 |
  • score INT
  • 303 |
  • restrictions VARCHAR(255)
  • 304 |
  • message_id BIGINT
  • 305 |
  • case_status TINYINT(1)
  • 306 |
  • start_time DATETIME
  • 307 |
  • end_time VARCHAR(255)
  • 308 |
  • title VARCHAR(255)
  • 309 |
  • tried INT
  • 310 |
311 |
312 |
314 |
315 |
316 |

ctf_history

317 |
318 |
    319 |
  • 320 | data_id BIGINT
  • 321 |
  • uid BIGINT
  • 322 |
  • count INT
  • 323 |
  • solved TINYINT(1)
  • 324 |
325 |
326 |
330 |
331 |
332 |

333 |
334 | 335 | 336 | -------------------------------------------------------------------------------- /cog/comment.py: -------------------------------------------------------------------------------- 1 | # Standard imports 2 | from datetime import datetime 3 | from datetime import date 4 | from datetime import timedelta 5 | import json 6 | import os 7 | import random 8 | import re 9 | 10 | # Third-party imports 11 | import discord 12 | from discord.ext import commands 13 | 14 | # Local imports 15 | from cog.core.sql import read 16 | from cog.core.sql import write 17 | from cog.core.sql import user_id_exists 18 | from cog.core.sql import end # 用於結束和SQL資料庫的會話 19 | from cog.core.sql import link_sql 20 | 21 | try: 22 | with open( 23 | f"{os.getcwd()}/database/server.config.json", "r", encoding="utf-8" 24 | ) as file: 25 | stickers = json.load(file)["SCAICT-alpha"]["stickers"] 26 | except FileNotFoundError: 27 | print("Configuration file not found.") 28 | stickers = {} 29 | except json.JSONDecodeError: 30 | print("Error decoding JSON.") 31 | stickers = {} 32 | 33 | 34 | def insert_user(user_id, table, cursor): 35 | """ 36 | 初始化(新增)傳入該ID的資料表 37 | """ 38 | try: 39 | cursor.execute( 40 | f"INSERT INTO {table} (uid) VALUE({user_id})" 41 | ) # 其他屬性在新增時MySQL會給預設值 42 | # pylint: disable-next = broad-exception-caught 43 | except Exception as exception: 44 | print(f"Error inserting user {user_id} into {table}: {exception}") 45 | 46 | 47 | def get_channels(): # 要特殊用途頻道的列表,這裡會用來判斷是否在簽到頻道簽到,否則不予受理 48 | # os.chdir("./") 49 | try: 50 | with open( 51 | f"{os.getcwd()}/database/server.config.json", "r", encoding="utf-8" 52 | ) as config_file: 53 | return json.load(config_file)["SCAICT-alpha"]["channel"] 54 | except FileNotFoundError: 55 | print("Configuration file not found.") 56 | except json.JSONDecodeError: 57 | print("Error decoding JSON.") 58 | except KeyError as exception: 59 | print(f"Key error in configuration file: {exception}") 60 | 61 | return {} 62 | 63 | 64 | def reset(message, now, cursor): 65 | user_id = message.author.id 66 | try: 67 | write(user_id, "today_comments", 0, cursor) # 歸零發言次數 68 | write(user_id, "last_comment", str(now), cursor) 69 | write( 70 | user_id, "times", 2, cursor, table="comment_points" 71 | ) # 初始化達標後能獲得的電電點 72 | write(user_id, "next_reward", 1, cursor, table="comment_points") 73 | # pylint: disable-next = broad-exception-caught 74 | except Exception as exception: 75 | print(f"Error resetting user {user_id}: {exception}") 76 | 77 | 78 | def reward(message, cursor): 79 | user_id = message.author.id 80 | user_display_name = message.author 81 | try: 82 | # 讀user資料表的東西 83 | today_comments = read(user_id, "today_comments", cursor) 84 | point = read(user_id, "point", cursor) 85 | # 讀comment_points 資料表裡面的東西,這個表格記錄有關發言次數非線性加分的資料 86 | next_reward = read(user_id, "next_reward", cursor, table="comment_points") 87 | times = read(user_id, "times", cursor, table="comment_points") 88 | 89 | today_comments += 1 90 | 91 | if today_comments == next_reward: 92 | point += 2 93 | next_reward += times**2 94 | times += 1 95 | write(user_id, "point", point, cursor) 96 | write(user_id, "next_reward", next_reward, cursor, table="comment_points") 97 | write(user_id, "times", times, cursor, table="comment_points") 98 | 99 | # 紀錄log 100 | print( 101 | f"{user_id}, {user_display_name} Get 2 point by comment {datetime.now()}" 102 | ) 103 | write(user_id, "today_comments", today_comments, cursor) 104 | # pylint: disable-next = broad-exception-caught 105 | except Exception as exception: 106 | print(f"Error rewarding user {user_id}: {exception}") 107 | 108 | 109 | class Comment(commands.Cog): 110 | 111 | def __init__(self, bot): 112 | self.bot = bot 113 | self.sp_channel = get_channels() # 特殊用途的channel 114 | 115 | self.sp_channel_handler = { 116 | self.sp_channel["countChannel"]: self.count, 117 | self.sp_channel["colorChannel"]: self.nice_color, 118 | } 119 | 120 | # 數數判定 121 | @commands.Cog.listener() 122 | async def on_message(self, message): 123 | user_id = message.author.id 124 | try: 125 | if user_id != self.bot.user.id: # 機器人發言不可當成觸發條件,必須排除 126 | handler = self.sp_channel_handler.get(message.channel.id) 127 | # 根據訊息頻道 ID 切換要呼叫的函數 128 | if handler: 129 | await handler(message) 130 | if message.channel.id not in self.sp_channel["exclude_point"]: 131 | # 平方發言加電電點,列表中頻道不算發言次數 132 | connection, cursor = link_sql() # SQL 會話 133 | self.today_comment(user_id, message, cursor) 134 | end(connection, cursor) 135 | # pylint: disable-next = broad-exception-caught 136 | except Exception as exception: 137 | print(f"Error in comment for user {user_id}: {exception}") 138 | 139 | @staticmethod 140 | def today_comment(user_id, message, cursor): 141 | try: 142 | # 新增該user的資料表 143 | if not user_id_exists(user_id, "user", cursor): 144 | # 該 user id 不在user資料表內,插入該筆使用者資料 145 | insert_user(user_id, "user", cursor) 146 | if not user_id_exists(user_id, "comment_points", cursor): 147 | insert_user(user_id, "comment_points", cursor) 148 | # pylint: disable-next = broad-exception-caught 149 | except Exception as exception: 150 | print(f"Error: {exception}") 151 | 152 | now = date.today() 153 | delta = timedelta(days=1) 154 | # SQL回傳型態: 155 | last_comment = read(user_id, "last_comment", cursor) 156 | # 今天第一次發言,重設發言次數 157 | if now - last_comment >= delta: 158 | reset(message, now, cursor) 159 | # 變更今天發言狀態 160 | reward(message, cursor) 161 | 162 | @staticmethod 163 | async def count(message): 164 | try: 165 | 166 | connection, cursor = link_sql() 167 | 168 | raw_content = message.content 169 | # 每月更新的數數 170 | # emoji 數數(把emoji轉換成binary) 171 | elements = raw_content.split() 172 | unique_elements = set(elements) 173 | if len(unique_elements) > 2: 174 | await message.add_reaction("❔") 175 | return 176 | 177 | # 轉換元素為0和1 178 | element_map = {element: idx for idx, element in enumerate(unique_elements)} 179 | transformed_elements = [str(element_map[element]) for element in elements] 180 | 181 | # 將轉換後的元素拼接成字串 182 | raw_content = "".join(transformed_elements) 183 | 184 | # emoji 數數(把emoji轉換成binary) 185 | counting_base = 2 186 | 187 | # Allow both plain and monospace formatting 188 | based_number = re.sub("^`([^\n]+)`$", "\\1", raw_content) 189 | 190 | # If is valid 4-digit whitespace delimiter format 191 | # (with/without base), then strip whitespace characters. 192 | # 193 | # Test cases: 194 | # - "0" 195 | # - "0000" 196 | # - "000000" 197 | # - "00 0000" 198 | # - "0b0" 199 | # - "0b0000" 200 | # - "0b 0000" 201 | # - "0b0 0000" 202 | # - "0b 0 0000" 203 | # - "0 b 0000" 204 | # - "0 b 0 0000" 205 | if re.match( 206 | "^(0[bdox]|0[bdox] |0 [bdox] |)" 207 | + "([0-9A-Fa-f]{1,4})" 208 | + "(([0-9A-Fa-f]{4})*|( [0-9A-Fa-f]{4})*)$", 209 | based_number, 210 | ): 211 | based_number = based_number.replace(" ", "") 212 | # If is valid 3-digit comma delimiter format 213 | # (10-based, without base) 214 | elif counting_base == 10 and re.match( 215 | "^([0-9]{1,3}(,[0-9]{3})*)$", based_number 216 | ): 217 | based_number = based_number.replace(",", "") 218 | # 若based_number字串轉換至整數失敗,會直接跳到except 219 | decimal_number = int(based_number, counting_base) 220 | # 補數 221 | decimal_complement = ~decimal_number & ((1 << len(based_number)) - 1) 222 | cursor.execute("select seq from game") 223 | now_seq = cursor.fetchone()[0] 224 | cursor.execute("select lastid from game") 225 | latest_user = cursor.fetchone()[0] 226 | if message.author.id == latest_user: 227 | # 同人疊數數 228 | await message.add_reaction("🔄") 229 | elif decimal_number == now_seq + 1 or decimal_complement == now_seq + 1: 230 | # 數數成立 231 | cursor.execute("UPDATE game SET seq = seq+1") 232 | cursor.execute("UPDATE game SET lastid = %s", (message.author.id,)) 233 | # add a check emoji to the message 234 | await message.add_reaction("✅") 235 | # 隨機產生 1~100 的數字。若模 11=10 ,九個數字符合,分布於 1~100 ,發生機率 9%。給予 5 點電電點 236 | rand = random.randint(1, 100) 237 | if rand % 11 == 10: 238 | point = read(message.author.id, "point", cursor) + 5 239 | write(message.author.id, "point", point, cursor) 240 | # pylint: disable-next = line-too-long 241 | print( 242 | f"{message.author.id}, {message.author} Get 5 point by count reward {datetime.now()}" 243 | ) 244 | await message.add_reaction("💸") 245 | else: 246 | # 不同人數數,但數字不對 247 | await message.add_reaction("❌") 248 | await message.add_reaction("❓") 249 | except (TypeError, ValueError): 250 | # 在decimal_number賦值因為不是數字(可能聊天或其他文字)產生錯誤產生問號emoji回應 251 | await message.add_reaction("❔") 252 | # pylint: disable-next = broad-exception-caught 253 | except Exception as exception: 254 | print(f"Error: {exception}") 255 | 256 | end(connection, cursor) 257 | 258 | @staticmethod 259 | async def nice_color(message): 260 | # if message.content is three letter 261 | if len(message.content) != 3: 262 | # reply text 263 | await message.channel.send("請輸入三位 HEX 碼顏色") 264 | return 265 | 266 | try: 267 | connection, cursor = link_sql() 268 | cursor.execute("SELECT nicecolor FROM game") 269 | nice_color = cursor.fetchone()[0] 270 | # Convert to upper case before check 271 | hex_color = message.content.upper() 272 | 273 | cursor.execute("SELECT `nicecolorround` FROM game") 274 | guess_round = cursor.fetchone()[0] + 1 275 | if hex_color == nice_color: 276 | # Use embed to send message. Set embed color to hex_color 277 | nice_color = "".join([c * 2 for c in nice_color]) # 格式化成六位數 278 | embed = discord.Embed( 279 | title=f"猜了 {guess_round}次後答對了!", 280 | description=f"#{hex_color}\n恭喜 {message.author.mention} 獲得 2{stickers['zap']}", 281 | color=discord.Colour(int(nice_color, 16)), 282 | ) 283 | await message.channel.send(embed=embed) 284 | # Generate a new color by random three letter 0~F 285 | new_color = "".join( 286 | [random.choice("0123456789ABCDEF") for _ in range(3)] 287 | ) 288 | # 資料庫存 3 位色碼,重設回答次數 289 | cursor.execute( 290 | f"UPDATE game SET nicecolor = '{new_color}', nicecolorround = 0" 291 | ) 292 | # 格式化成六位數,配合 discord.Colour 輸出 293 | new_color = "".join([c * 2 for c in new_color]) 294 | # Send new color to channel 295 | embed = discord.Embed( 296 | title="已產生新題目", 297 | description="請輸入三位數回答", 298 | color=discord.Colour(int(new_color, 16)), 299 | ) 300 | await message.channel.send(embed=embed) 301 | # 猜對的使用者加分 302 | point = read(message.author.id, "point", cursor) + 2 303 | write(message.author.id, "point", point, cursor) 304 | # Log 305 | # pylint: disable-next = line-too-long 306 | print( 307 | f"{message.author.id},{message.author} Get 2 point by nice color reward {datetime.now()}" 308 | ) 309 | else: 310 | cursor.execute("UPDATE game SET nicecolorround = nicecolorround + 1;") 311 | # https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/consider-using-generator.html 312 | # pylint: disable-next = line-too-long 313 | correct = 100 - ( 314 | sum( 315 | (int(hex_color[i], 16) - int(nice_color[i], 16)) ** 2 316 | for i in range(3) 317 | ) 318 | ** 0.5 319 | / 0.2598076211353316 320 | ) 321 | hex_color = "".join([c * 2 for c in hex_color]) # 格式化成六位數 322 | embed = discord.Embed( 323 | title=f"#{hex_color}\n{correct:.2f}%", 324 | color=discord.Colour(int(hex_color, 16)), 325 | ) 326 | await message.channel.send(embed=embed) 327 | nice_color = "".join([c * 2 for c in nice_color]) # 格式化成六位數 328 | embed = discord.Embed( 329 | description=f"答案:左邊顏色\n總共回答次數:{guess_round}", 330 | color=discord.Colour(int(nice_color, 16)), 331 | ) 332 | await message.channel.send(embed=embed) 333 | # except: 334 | # await message.add_reaction("❔") 335 | # print error message 336 | # pylint: disable-next = broad-exception-caught 337 | except Exception as exception: 338 | print(f"Error: {exception}") 339 | 340 | end(connection, cursor) 341 | 342 | 343 | def setup(bot): 344 | bot.add_cog(Comment(bot)) 345 | -------------------------------------------------------------------------------- /RELEASE-NOTES-0.1.md: -------------------------------------------------------------------------------- 1 | # SCAICT-uwu 0.1 2 | 3 | ## SCAICT-uwu 0.1 development branch 4 | 5 | THIS IS NOT A RELEASE YET 6 | 7 | The `development` branch is a beta-quality development branch. Use it at your 8 | own risk! 9 | 10 | ### Configuration changes for system administrators 11 | 12 | #### New configuration 13 | 14 | * … 15 | 16 | #### Changed configuration 17 | 18 | * … 19 | 20 | #### Removed configuration 21 | 22 | * … 23 | 24 | ### New user-facing features 25 | 26 | * … 27 | 28 | ### New features for sysadmins 29 | 30 | * … 31 | 32 | ### New developer features 33 | 34 | * Updated project.classifiers in `pyproject.toml`. 35 | * … 36 | 37 | ### External dependency changes 38 | 39 | #### New external dependencies 40 | 41 | * Added py-cord dependency. 42 | * Added propcache 0.3.1. 43 | 44 | #### New development-only external dependencies 45 | 46 | * … 47 | 48 | #### Changed external dependencies 49 | 50 | * Updated flask from 3.0.3 to 3.1.0. 51 | * Updated blinker from 1.8.2 to 1.9.0 52 | * Updated click from 8.1.7 to 8.1.8. 53 | * Updated jinja2 from 3.1.4 to 3.1.6. 54 | * Updated markupsafe from 2.1.5 to 3.0.2. 55 | * Updated werkzeug from 3.0.4 to 3.1.3. 56 | * Updated mysql-connector-python from 8.4.0 to 9.2.0. 57 | * Updated py-cord from 2.6.0 to 2.6.1. 58 | * Updated aiohappyeyeballs from 2.4.0 to 2.6.1. 59 | * Updated aiohttp from 3.10.5 to 3.11.14. 60 | * Updated aiosignal from 1.3.1 to 1.3.2. 61 | * Updated attrs from 24.2.0 to 25.3.0. 62 | * Updated frozenlist from 1.4.1 to 1.5.0. 63 | * Updated idna from 3.7 to 3.10. 64 | * Updated multidict from 6.0.5 to 6.3.1. 65 | * Updated yarl from 1.9.4 to 1.18.3. 66 | * Updated requests dependencies. 67 | * Updated certifi from 2024.7.4 to 2025.1.31. 68 | * Updated charset-normalizer from 3.3.2 to 3.4.1. 69 | * Updated idna from 3.7 to 3.10. 70 | * Updated urllib3 from 2.2.2 to 2.3.0. 71 | 72 | #### Changed development-only external dependencies 73 | 74 | * Updated black from 24.8.0 to 25.1.0. 75 | * Updated click from 8.1.7 to 8.1.8. 76 | * Updated packaging from 24.1 to 24.2. 77 | * Updated platformdirs from 4.2.2 to 4.3.7. 78 | * Updated pylint from 3.2.6 to 3.3.6. 79 | * Updated astroid from 3.2.4 to 3.3.9. 80 | * Updated dill from 0.3.8 to 0.3.9. 81 | * Updated isort from 5.13.2 to 6.0.1. 82 | * Updated platformdirs from 4.2.2 to 4.3.7. 83 | * Updated pytest from 8.3.2 to 8.3.5. 84 | * Updated iniconfig from 2.0.0 to 2.1.0. 85 | * Updated packaging from 24.1 to 24.2. 86 | 87 | #### Removed external dependencies 88 | 89 | * … 90 | 91 | ### Bug fixes 92 | 93 | * … 94 | 95 | ### API changes 96 | 97 | * … 98 | 99 | ### API internal changes 100 | 101 | * … 102 | 103 | ### Languages updated 104 | 105 | SCAICT-uwu now supports 1 language. Localisations are updated regularly. 106 | 107 | Below only new and removed languages are listed. 108 | 109 | * … 110 | 111 | ### Breaking changes 112 | 113 | * … 114 | 115 | ### Deprecations 116 | 117 | * … 118 | 119 | ### Other changes 120 | 121 | * … 122 | 123 | ## SCAICT-uwu 0.1.8 124 | 125 | This is a maintenance release of SCAICT-uwu 0.1 version. 126 | 127 | ### Configuration changes for system administrators in 0.1.8 128 | 129 | #### New configuration in 0.1.8 130 | 131 | * … 132 | 133 | #### Changed configuration in 0.1.8 134 | 135 | * … 136 | 137 | #### Removed configuration in 0.1.8 138 | 139 | * … 140 | 141 | ### New user-facing features in 0.1.8 142 | 143 | * … 144 | 145 | ### New features for sysadmins in 0.1.8 146 | 147 | * … 148 | 149 | ### New developer features in 0.1.8 150 | 151 | * … 152 | 153 | ### External dependency changes in 0.1.8 154 | 155 | #### New external dependencies in 0.1.8 156 | 157 | * … 158 | 159 | #### New development-only external dependencies in 0.1.8 160 | 161 | * … 162 | 163 | #### Changed external dependencies in 0.1.8 164 | 165 | * … 166 | 167 | #### Changed development-only external dependencies in 0.1.8 168 | 169 | * … 170 | 171 | #### Removed external dependencies in 0.1.8 172 | 173 | * … 174 | 175 | ### Bug fixes in 0.1.8 176 | 177 | * … 178 | 179 | ### API changes in 0.1.8 180 | 181 | * … 182 | 183 | ### API internal changes in 0.1.8 184 | 185 | * … 186 | 187 | ### Languages updated in 0.1.8 188 | 189 | SCAICT-uwu now supports 1 language. Localisations are updated regularly. 190 | 191 | Below only new and removed languages are listed. 192 | 193 | * … 194 | 195 | ### Breaking changes in 0.1.8 196 | 197 | * … 198 | 199 | ### Deprecations in 0.1.8 200 | 201 | * … 202 | 203 | ### Other changes in 0.1.8 204 | 205 | * … 206 | 207 | ## SCAICT-uwu 0.1.7 208 | 209 | This is a maintenance release of SCAICT-uwu 0.1 version. 210 | 211 | ### Configuration changes for system administrators in 0.1.7 212 | 213 | #### New configuration in 0.1.7 214 | 215 | * … 216 | 217 | #### Changed configuration in 0.1.7 218 | 219 | * … 220 | 221 | #### Removed configuration in 0.1.7 222 | 223 | * … 224 | 225 | ### New user-facing features in 0.1.7 226 | 227 | * … 228 | 229 | ### New features for sysadmins in 0.1.7 230 | 231 | * … 232 | 233 | ### New developer features in 0.1.7 234 | 235 | * … 236 | 237 | ### External dependency changes in 0.1.7 238 | 239 | #### New external dependencies in 0.1.7 240 | 241 | * … 242 | 243 | #### New development-only external dependencies in 0.1.7 244 | 245 | * … 246 | 247 | #### Changed external dependencies in 0.1.7 248 | 249 | * … 250 | 251 | #### Changed development-only external dependencies in 0.1.7 252 | 253 | * … 254 | 255 | #### Removed external dependencies in 0.1.7 256 | 257 | * … 258 | 259 | ### Bug fixes in 0.1.7 260 | 261 | * … 262 | 263 | ### API changes in 0.1.7 264 | 265 | * … 266 | 267 | ### API internal changes in 0.1.7 268 | 269 | * … 270 | 271 | ### Languages updated in 0.1.7 272 | 273 | SCAICT-uwu now supports 1 language. 274 | 275 | ### Breaking changes in 0.1.7 276 | 277 | * … 278 | 279 | ### Deprecations in 0.1.7 280 | 281 | * … 282 | 283 | ### Other changes in 0.1.7 284 | 285 | * … 286 | 287 | ## SCAICT-uwu 0.1.6 288 | 289 | This is a maintenance release of SCAICT-uwu 0.1 version. 290 | 291 | ### Configuration changes for system administrators in 0.1.6 292 | 293 | #### New configuration in 0.1.6 294 | 295 | * … 296 | 297 | #### Changed configuration in 0.1.6 298 | 299 | * … 300 | 301 | #### Removed configuration in 0.1.6 302 | 303 | * … 304 | 305 | ### New user-facing features in 0.1.6 306 | 307 | * … 308 | 309 | ### New features for sysadmins in 0.1.6 310 | 311 | * … 312 | 313 | ### New developer features in 0.1.6 314 | 315 | * … 316 | 317 | ### External dependency changes in 0.1.6 318 | 319 | #### New external dependencies in 0.1.6 320 | 321 | * … 322 | 323 | #### New development-only external dependencies in 0.1.6 324 | 325 | * … 326 | 327 | #### Changed external dependencies in 0.1.6 328 | 329 | * … 330 | 331 | #### Changed development-only external dependencies in 0.1.6 332 | 333 | * … 334 | 335 | #### Removed external dependencies in 0.1.6 336 | 337 | * … 338 | 339 | ### Bug fixes in 0.1.6 340 | 341 | * … 342 | 343 | ### API changes in 0.1.6 344 | 345 | * … 346 | 347 | ### API internal changes in 0.1.6 348 | 349 | * … 350 | 351 | ### Languages updated in 0.1.6 352 | 353 | SCAICT-uwu now supports 1 language. 354 | 355 | ### Breaking changes in 0.1.6 356 | 357 | * … 358 | 359 | ### Deprecations in 0.1.6 360 | 361 | * … 362 | 363 | ### Other changes in 0.1.6 364 | 365 | * … 366 | 367 | ## SCAICT-uwu 0.1.5 368 | 369 | This is a maintenance release of SCAICT-uwu 0.1 version. 370 | 371 | ### Configuration changes for system administrators in 0.1.5 372 | 373 | #### New configuration in 0.1.5 374 | 375 | * … 376 | 377 | #### Changed configuration in 0.1.5 378 | 379 | * … 380 | 381 | #### Removed configuration in 0.1.5 382 | 383 | * … 384 | 385 | ### New user-facing features in 0.1.5 386 | 387 | * … 388 | 389 | ### New features for sysadmins in 0.1.5 390 | 391 | * … 392 | 393 | ### New developer features in 0.1.5 394 | 395 | * … 396 | 397 | ### External dependency changes in 0.1.5 398 | 399 | #### New external dependencies in 0.1.5 400 | 401 | * … 402 | 403 | #### New development-only external dependencies in 0.1.5 404 | 405 | * … 406 | 407 | #### Changed external dependencies in 0.1.5 408 | 409 | * … 410 | 411 | #### Changed development-only external dependencies in 0.1.5 412 | 413 | * … 414 | 415 | #### Removed external dependencies in 0.1.5 416 | 417 | * … 418 | 419 | ### Bug fixes in 0.1.5 420 | 421 | * … 422 | 423 | ### API changes in 0.1.5 424 | 425 | * … 426 | 427 | ### API internal changes in 0.1.5 428 | 429 | * … 430 | 431 | ### Languages updated in 0.1.5 432 | 433 | SCAICT-uwu now supports 1 language. 434 | 435 | ### Breaking changes in 0.1.5 436 | 437 | * … 438 | 439 | ### Deprecations in 0.1.5 440 | 441 | * … 442 | 443 | ### Other changes in 0.1.5 444 | 445 | * … 446 | 447 | ## SCAICT-uwu 0.1.4 448 | 449 | This is a maintenance release of SCAICT-uwu 0.1 version. 450 | 451 | ### Configuration changes for system administrators in 0.1.4 452 | 453 | #### New configuration in 0.1.4 454 | 455 | * … 456 | 457 | #### Changed configuration in 0.1.4 458 | 459 | * … 460 | 461 | #### Removed configuration in 0.1.4 462 | 463 | * … 464 | 465 | ### New user-facing features in 0.1.4 466 | 467 | * … 468 | 469 | ### New features for sysadmins in 0.1.4 470 | 471 | * … 472 | 473 | ### New developer features in 0.1.4 474 | 475 | * … 476 | 477 | ### External dependency changes in 0.1.4 478 | 479 | #### New external dependencies in 0.1.4 480 | 481 | * … 482 | 483 | #### New development-only external dependencies in 0.1.4 484 | 485 | * … 486 | 487 | #### Changed external dependencies in 0.1.4 488 | 489 | * … 490 | 491 | #### Changed development-only external dependencies in 0.1.4 492 | 493 | * … 494 | 495 | #### Removed external dependencies in 0.1.4 496 | 497 | * … 498 | 499 | ### Bug fixes in 0.1.4 500 | 501 | * … 502 | 503 | ### API changes in 0.1.4 504 | 505 | * … 506 | 507 | ### API internal changes in 0.1.4 508 | 509 | * … 510 | 511 | ### Languages updated in 0.1.4 512 | 513 | SCAICT-uwu now supports 1 language. 514 | 515 | ### Breaking changes in 0.1.4 516 | 517 | * … 518 | 519 | ### Deprecations in 0.1.4 520 | 521 | * … 522 | 523 | ### Other changes in 0.1.4 524 | 525 | * … 526 | 527 | ## SCAICT-uwu 0.1.3 528 | 529 | This is a maintenance release of SCAICT-uwu 0.1 version. 530 | 531 | ### Configuration changes for system administrators in 0.1.3 532 | 533 | #### New configuration in 0.1.3 534 | 535 | * … 536 | 537 | #### Changed configuration in 0.1.3 538 | 539 | * … 540 | 541 | #### Removed configuration in 0.1.3 542 | 543 | * … 544 | 545 | ### New user-facing features in 0.1.3 546 | 547 | * … 548 | 549 | ### New features for sysadmins in 0.1.3 550 | 551 | * … 552 | 553 | ### New developer features in 0.1.3 554 | 555 | * … 556 | 557 | ### External dependency changes in 0.1.3 558 | 559 | #### New external dependencies in 0.1.3 560 | 561 | * … 562 | 563 | #### New development-only external dependencies in 0.1.3 564 | 565 | * … 566 | 567 | #### Changed external dependencies in 0.1.3 568 | 569 | * … 570 | 571 | #### Changed development-only external dependencies in 0.1.3 572 | 573 | * … 574 | 575 | #### Removed external dependencies in 0.1.3 576 | 577 | * … 578 | 579 | ### Bug fixes in 0.1.3 580 | 581 | * … 582 | 583 | ### API changes in 0.1.3 584 | 585 | * … 586 | 587 | ### API internal changes in 0.1.3 588 | 589 | * … 590 | 591 | ### Languages updated in 0.1.3 592 | 593 | SCAICT-uwu now supports 1 language. 594 | 595 | ### Breaking changes in 0.1.3 596 | 597 | * … 598 | 599 | ### Deprecations in 0.1.3 600 | 601 | * … 602 | 603 | ### Other changes in 0.1.3 604 | 605 | * … 606 | 607 | ## SCAICT-uwu 0.1.2 608 | 609 | This is a maintenance release of SCAICT-uwu 0.1 version. 610 | 611 | ### Configuration changes for system administrators in 0.1.2 612 | 613 | #### New configuration in 0.1.2 614 | 615 | * … 616 | 617 | #### Changed configuration in 0.1.2 618 | 619 | * … 620 | 621 | #### Removed configuration in 0.1.2 622 | 623 | * … 624 | 625 | ### New user-facing features in 0.1.2 626 | 627 | * … 628 | 629 | ### New features for sysadmins in 0.1.2 630 | 631 | * … 632 | 633 | ### New developer features in 0.1.2 634 | 635 | * … 636 | 637 | ### External dependency changes in 0.1.2 638 | 639 | #### New external dependencies in 0.1.2 640 | 641 | * … 642 | 643 | #### New development-only external dependencies in 0.1.2 644 | 645 | * … 646 | 647 | #### Changed external dependencies in 0.1.2 648 | 649 | * … 650 | 651 | #### Changed development-only external dependencies in 0.1.2 652 | 653 | * … 654 | 655 | #### Removed external dependencies in 0.1.2 656 | 657 | * … 658 | 659 | ### Bug fixes in 0.1.2 660 | 661 | * … 662 | 663 | ### API changes in 0.1.2 664 | 665 | * … 666 | 667 | ### API internal changes in 0.1.2 668 | 669 | * … 670 | 671 | ### Languages updated in 0.1.2 672 | 673 | SCAICT-uwu now supports 1 language. 674 | 675 | ### Breaking changes in 0.1.2 676 | 677 | * … 678 | 679 | ### Deprecations in 0.1.2 680 | 681 | * … 682 | 683 | ### Other changes in 0.1.2 684 | 685 | * … 686 | 687 | ## SCAICT-uwu 0.1.1 688 | 689 | This is a maintenance release of SCAICT-uwu 0.1 version. 690 | 691 | ### New user-facing features in 0.1.1 692 | 693 | * Added dynamic voice channel and support ticket feature. 694 | * Added channel member display. 695 | * Added CTF features. 696 | * Updated CTF features (WIP, can create and list). 697 | * Added independent update channels. 698 | * Rewrote the daily charge feature. 699 | * Completed CTF features. 700 | * Added initial website using Flask. 701 | * Completed course role features. 702 | * Updated design of SCAICT Store. 703 | * Updated the support ticket embed description. 704 | * Updated to limit the usage of daily change command to specific channel. 705 | * Completed SCAICT Store. 706 | * Added lottery slot feature in SCAICT Store. 707 | * Added support for bot status/presence. 708 | * Added zap emoji. 709 | * Added total point status display. 710 | * Updated CTF features. 711 | 712 | ### New features for sysadmins in 0.1.1 713 | 714 | * Added Google Analytics. 715 | 716 | ### Bug fixes in 0.1.1 717 | 718 | * Fixed the problem of bot responding to self messages. 719 | * Fixed counting error, only the user replied would see the message. 720 | * Added exception handling for data not found issues. 721 | * Fixed issue caused by users without avatar set. 722 | 723 | ### Languages in 0.1.1 724 | 725 | SCAICT-uwu now supports 1 language. 726 | 727 | Below only new and removed languages are listed. 728 | 729 | * Added language support for Mandarin - Traditional Han script (`zh-hant`). 730 | 731 | ### Breaking changes and deprecations in 0.1.1 732 | 733 | * Added JSON file as initial database. 734 | * Updated to use JSON file to get CTF maker role ID. 735 | * Added SQL database. 736 | * Migrated user.json to SQL database. 737 | * Renamed SQL column name from `user_id` to `uid`. 738 | * Dropped support for `user.json`, use SQL database instead. 739 | 740 | ### Other changes in 0.1.1 741 | 742 | * Added `.gitignore` to ignore token files. 743 | * Added `.gitignore` to ignore cache files. 744 | * Passing bot attributes. 745 | * Added comment, check_point and daily_charge Python modules. 746 | * Added user Python module. 747 | * Migrated use of user functions to user Python module. 748 | 749 | ## SCAICT-uwu 0.1.0 750 | 751 | This is the initial release of SCAICT-uwu 0.1 version. 752 | 753 | ### Changes in 0.1.0 754 | 755 | * Initial commit. 756 | * Added `LICENSE`, using Apache License Version 2.0, January 2004. 757 | * Added `README.md`. 758 | --------------------------------------------------------------------------------