diff --git a/.env.example b/.env.example index 2f78239..0cff0b3 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,24 @@ # 這是一個範例檔案,請複製一份 .env.example 並命名為 .env # 並將以下的設定值填入 + +# Database configuration MYSQL_USER=資料庫使用者 MYSQL_PASSWORD=資料庫使用者密碼 MYSQL_PORT=資料庫連接埠 MYSQL_DATABASE=資料庫名稱 -HOST=資料庫主機位址 \ No newline at end of file +HOST=資料庫主機位址 + +# Global configuration +DISCORD_TOKEN=Discord機器人token +GUILD_ID=機器人所在伺服器ID + +# Flask configuration +SECRET_KEY=隨機字串,給 flask session 使用 +DISCORD_CLIENT_ID=Discord Client ID +DISCORD_CLIENT_SECRET=Discord Client Secret,到 Discord Developer Portal 取得 +DISCORD_REDIRECT_URI=商店網址/callback +GITHUB_CLIENT_ID=GitHub Client ID,到 GitHub Developer 取得 +GITHUB_CLIENT_SECRET=GitHub Client Secret +GITHUB_REDIRECT_URI=GitHub OAuth Redirect URI +GITHUB_DISCORD_REDIRECT_URI= +SEND_GIFT_ROLE=允許發送禮物的身分組ID,多個身分組ID以逗號分隔 diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 29cd7a0..c11abdc 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -11,25 +11,37 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ "3.11" ] + python-version: ["3.11"] steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements_dev.txt - - name: Formatting the code with Black - run: | - black $(git ls-files '*.py') - - name: Add, commit and push - run: | - git config --local user.name "github-actions[bot]" - git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add * - git diff-index --quiet HEAD || git commit -m "Format \"$(git show -s --format=%s)\" using Black" - git diff-index --quiet HEAD || git push + - if: github.event_name != 'pull_request' + uses: actions/checkout@v4 + - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements_dev.txt + pip install -e .[dev] + - name: Formatting the code with Black + run: | + black $(git ls-files '*.py') + - name: Git config + run: | + git config --local user.name "github-actions[bot]" + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Git diff + run: | + git diff HEAD || true + - name: Git add + run: | + git add * + - name: Git commit & push + run: | + git diff-index --quiet HEAD || ( git commit -m "Format \"$(git show -s --format=%s)\" using Black" && git push ) diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml new file mode 100644 index 0000000..1b910ca --- /dev/null +++ b/.github/workflows/merge.yml @@ -0,0 +1,38 @@ +name: Merge + +on: + pull_request_review: + types: [submitted] + +jobs: + merge: + permissions: + contents: write + pull-requests: write + if: + github.event.review.state == 'approved' && + ( + github.event.review.author_association == 'OWNER' || + github.event.review.author_association == 'MEMBER' || + github.event.review.author_association == 'COLLABORATOR' + ) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set Git config + run: | + git config --local user.name "github-actions[bot]" + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Rebase to target branch when needed in order to fast-forward merge + run: | + git fetch origin ${{ github.event.pull_request.base.ref }} + git rebase origin/${{ github.event.pull_request.base.ref }} + - name: Push back source branch (head) + run: | + git push "${{ github.event.pull_request.head.repo.clone_url }}" HEAD:${{ github.event.pull_request.head.ref }} --force + - name: Push to target branch (base) + run: | + # git push "${{ github.event.pull_request.base.repo.clone_url }}" ${{ github.event.pull_request.base.ref }} + git push origin HEAD:${{ github.event.pull_request.base.ref }} diff --git a/.github/workflows/notion.yml b/.github/workflows/notion.yml index 2b63a87..44fb095 100644 --- a/.github/workflows/notion.yml +++ b/.github/workflows/notion.yml @@ -2,7 +2,7 @@ name: Sync issues to Notion on: issues: - types: [ opened, edited, deleted, reopened, closed ] + types: [opened, edited, deleted, reopened, closed] workflow_dispatch: jobs: diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index dfaab50..18d7a6e 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -7,21 +7,23 @@ on: jobs: build: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == true runs-on: ubuntu-latest strategy: matrix: - python-version: [ "3.11" ] + python-version: ["3.11"] steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements_dev.txt - - name: Analysing the code with Pylint - run: | - pylint $(git ls-files '*.py') + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements_dev.txt + pip install -e .[dev] + - name: Analysing the code with Pylint + run: | + pylint $(git ls-files '*.py') diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..9300f34 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,29 @@ +name: Pytest + +on: + pull_request: + push: + workflow_dispatch: + +jobs: + build: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == true + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements_dev.txt + pip install -e .[dev] + - name: Analysing the code with Pylint + run: | + pytest tests/pytest/ diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml new file mode 100644 index 0000000..5f22567 --- /dev/null +++ b/.github/workflows/unittest.yml @@ -0,0 +1,29 @@ +name: Python unittest + +on: + pull_request: + push: + workflow_dispatch: + +jobs: + build: + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == true) + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements_dev.txt + pip install -e .[dev] + - name: Analysing the code with Pylint + run: | + python -m unittest $(git ls-files 'tests/unittest/*.py') diff --git a/.gitignore b/.gitignore index fa685a3..3d17a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,12 +12,20 @@ ## Python **/__pycache__ *.egg-info/ +build/lib/ ## Linux flakLog.out nohup.out pointLog.out ## Configuration files .env +# python 虛擬環境 +bin/ +envuwu/ +env-uwu/ +lib/ +lib64 +pyvenv.cfg ### Ignore token ### 如果你的token.json已經被追蹤可以執行: ### 1. git rm --cached token.json diff --git a/.pylintrc b/.pylintrc index 764eb8c..e26656c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -448,6 +448,7 @@ disable=raw-checker-failed, line-too-long, duplicate-code, no-member, + too-few-public-methods, unused-argument, too-many-locals, too-many-arguments, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa0d15e..01e55f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -189,8 +189,9 @@ for further information. MySQL/MariaDB. * Column names should be unique, i.e., same column name should not exist in different tables. -* Column names should be prefixed with table names or abbrieviations. - * For example, `user_id` in `user`, `ug_user` in `user_groups`. +* Column names should be prefixed with table names or abbreviations. + * For example, `user_id` column in `user` table, `ug_user` column in + `user_groups` table. Examples: @@ -212,5 +213,6 @@ UPDATE game SET game_seq = game_seq + 1 ### Development dependencies +* black: * pylint: * pytest: diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..c8cf6f9 --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,557 @@ +# History + +Change notes from older releases. For current info, see RELEASE-NOTES-0.2. + +# SCAICT-uwu 0.1 + +## SCAICT-uwu 0.1.7 + +This is a maintenance release of SCAICT-uwu 0.1 version. + +### Configuration changes for system administrators in 0.1.7 + +#### New configuration in 0.1.7 + +* … + +#### Changed configuration in 0.1.7 + +* … + +#### Removed configuration in 0.1.7 + +* … + +### New user-facing features in 0.1.7 + +* … + +### New features for sysadmins in 0.1.7 + +* … + +### New developer features in 0.1.7 + +* … + +### External dependency changes in 0.1.7 + +#### New external dependencies in 0.1.7 + +* … + +#### New development-only external dependencies in 0.1.7 + +* … + +#### Changed external dependencies in 0.1.7 + +* … + +#### Changed development-only external dependencies in 0.1.7 + +* … + +#### Removed external dependencies in 0.1.7 + +* … + +### Bug fixes in 0.1.7 + +* … + +### API changes in 0.1.7 + +* … + +### API internal changes in 0.1.7 + +* … + +### Languages updated in 0.1.7 + +SCAICT-uwu now supports 1 language. + +### Breaking changes in 0.1.7 + +* … + +### Deprecations in 0.1.7 + +* … + +### Other changes in 0.1.7 + +* … + +## SCAICT-uwu 0.1.6 + +This is a maintenance release of SCAICT-uwu 0.1 version. + +### Configuration changes for system administrators in 0.1.6 + +#### New configuration in 0.1.6 + +* … + +#### Changed configuration in 0.1.6 + +* … + +#### Removed configuration in 0.1.6 + +* … + +### New user-facing features in 0.1.6 + +* … + +### New features for sysadmins in 0.1.6 + +* … + +### New developer features in 0.1.6 + +* … + +### External dependency changes in 0.1.6 + +#### New external dependencies in 0.1.6 + +* … + +#### New development-only external dependencies in 0.1.6 + +* … + +#### Changed external dependencies in 0.1.6 + +* … + +#### Changed development-only external dependencies in 0.1.6 + +* … + +#### Removed external dependencies in 0.1.6 + +* … + +### Bug fixes in 0.1.6 + +* … + +### API changes in 0.1.6 + +* … + +### API internal changes in 0.1.6 + +* … + +### Languages updated in 0.1.6 + +SCAICT-uwu now supports 1 language. + +### Breaking changes in 0.1.6 + +* … + +### Deprecations in 0.1.6 + +* … + +### Other changes in 0.1.6 + +* … + +## SCAICT-uwu 0.1.5 + +This is a maintenance release of SCAICT-uwu 0.1 version. + +### Configuration changes for system administrators in 0.1.5 + +#### New configuration in 0.1.5 + +* … + +#### Changed configuration in 0.1.5 + +* … + +#### Removed configuration in 0.1.5 + +* … + +### New user-facing features in 0.1.5 + +* … + +### New features for sysadmins in 0.1.5 + +* … + +### New developer features in 0.1.5 + +* … + +### External dependency changes in 0.1.5 + +#### New external dependencies in 0.1.5 + +* … + +#### New development-only external dependencies in 0.1.5 + +* … + +#### Changed external dependencies in 0.1.5 + +* … + +#### Changed development-only external dependencies in 0.1.5 + +* … + +#### Removed external dependencies in 0.1.5 + +* … + +### Bug fixes in 0.1.5 + +* … + +### API changes in 0.1.5 + +* … + +### API internal changes in 0.1.5 + +* … + +### Languages updated in 0.1.5 + +SCAICT-uwu now supports 1 language. + +### Breaking changes in 0.1.5 + +* … + +### Deprecations in 0.1.5 + +* … + +### Other changes in 0.1.5 + +* … + +## SCAICT-uwu 0.1.4 + +This is a maintenance release of SCAICT-uwu 0.1 version. + +### Configuration changes for system administrators in 0.1.4 + +#### New configuration in 0.1.4 + +* … + +#### Changed configuration in 0.1.4 + +* … + +#### Removed configuration in 0.1.4 + +* … + +### New user-facing features in 0.1.4 + +* … + +### New features for sysadmins in 0.1.4 + +* … + +### New developer features in 0.1.4 + +* … + +### External dependency changes in 0.1.4 + +#### New external dependencies in 0.1.4 + +* … + +#### New development-only external dependencies in 0.1.4 + +* … + +#### Changed external dependencies in 0.1.4 + +* … + +#### Changed development-only external dependencies in 0.1.4 + +* … + +#### Removed external dependencies in 0.1.4 + +* … + +### Bug fixes in 0.1.4 + +* … + +### API changes in 0.1.4 + +* … + +### API internal changes in 0.1.4 + +* … + +### Languages updated in 0.1.4 + +SCAICT-uwu now supports 1 language. + +### Breaking changes in 0.1.4 + +* … + +### Deprecations in 0.1.4 + +* … + +### Other changes in 0.1.4 + +* … + +## SCAICT-uwu 0.1.3 + +This is a maintenance release of SCAICT-uwu 0.1 version. + +### Configuration changes for system administrators in 0.1.3 + +#### New configuration in 0.1.3 + +* … + +#### Changed configuration in 0.1.3 + +* … + +#### Removed configuration in 0.1.3 + +* … + +### New user-facing features in 0.1.3 + +* … + +### New features for sysadmins in 0.1.3 + +* … + +### New developer features in 0.1.3 + +* … + +### External dependency changes in 0.1.3 + +#### New external dependencies in 0.1.3 + +* … + +#### New development-only external dependencies in 0.1.3 + +* … + +#### Changed external dependencies in 0.1.3 + +* … + +#### Changed development-only external dependencies in 0.1.3 + +* … + +#### Removed external dependencies in 0.1.3 + +* … + +### Bug fixes in 0.1.3 + +* … + +### API changes in 0.1.3 + +* … + +### API internal changes in 0.1.3 + +* … + +### Languages updated in 0.1.3 + +SCAICT-uwu now supports 1 language. + +### Breaking changes in 0.1.3 + +* … + +### Deprecations in 0.1.3 + +* … + +### Other changes in 0.1.3 + +* … + +## SCAICT-uwu 0.1.2 + +This is a maintenance release of SCAICT-uwu 0.1 version. + +### Configuration changes for system administrators in 0.1.2 + +#### New configuration in 0.1.2 + +* … + +#### Changed configuration in 0.1.2 + +* … + +#### Removed configuration in 0.1.2 + +* … + +### New user-facing features in 0.1.2 + +* … + +### New features for sysadmins in 0.1.2 + +* … + +### New developer features in 0.1.2 + +* … + +### External dependency changes in 0.1.2 + +#### New external dependencies in 0.1.2 + +* … + +#### New development-only external dependencies in 0.1.2 + +* … + +#### Changed external dependencies in 0.1.2 + +* … + +#### Changed development-only external dependencies in 0.1.2 + +* … + +#### Removed external dependencies in 0.1.2 + +* … + +### Bug fixes in 0.1.2 + +* … + +### API changes in 0.1.2 + +* … + +### API internal changes in 0.1.2 + +* … + +### Languages updated in 0.1.2 + +SCAICT-uwu now supports 1 language. + +### Breaking changes in 0.1.2 + +* … + +### Deprecations in 0.1.2 + +* … + +### Other changes in 0.1.2 + +* … + +## SCAICT-uwu 0.1.1 + +This is a maintenance release of SCAICT-uwu 0.1 version. + +### New user-facing features in 0.1.1 + +* Added dynamic voice channel and support ticket feature. +* Added channel member display. +* Added CTF features. +* Updated CTF features (WIP, can create and list). +* Added independent update channels. +* Rewrote the daily charge feature. +* Completed CTF features. +* Added initial website using Flask. +* Completed course role features. +* Updated design of SCAICT Store. +* Updated the support ticket embed description. +* Updated to limit the usage of daily change command to specific channel. +* Completed SCAICT Store. +* Added lottery slot feature in SCAICT Store. +* Added support for bot status/presence. +* Added zap emoji. +* Added total point status display. +* Updated CTF features. + +### New features for sysadmins in 0.1.1 + +* Added Google Analytics. + +### Bug fixes in 0.1.1 + +* Fixed the problem of bot responding to self messages. +* Fixed counting error, only the user replied would see the message. +* Added exception handling for data not found issues. +* Fixed issue caused by users without avatar set. + +### Languages in 0.1.1 + +SCAICT-uwu now supports 1 language. + +Below only new and removed languages are listed. + +* Added language support for Mandarin - Traditional Han script (`zh-hant`). + +### Breaking changes and deprecations in 0.1.1 + +* Added JSON file as initial database. +* Updated to use JSON file to get CTF maker role ID. +* Added SQL database. +* Migrated user.json to SQL database. +* Renamed SQL column name from `user_id` to `uid`. +* Dropped support for `user.json`, use SQL database instead. + +### Other changes in 0.1.1 + +* Added `.gitignore` to ignore token files. +* Added `.gitignore` to ignore cache files. +* Passing bot attributes. +* Added comment, check_point and daily_charge Python modules. +* Added user Python module. +* Migrated use of user functions to user Python module. + +## SCAICT-uwu 0.1.0 + +This is the initial release of SCAICT-uwu 0.1 version. + +### Changes in 0.1.0 + +* Initial commit. + * Added `LICENSE`, using Apache License Version 2.0, January 2004. + * Added `README.md`. diff --git a/README.md b/README.md index f6fae6b..7289457 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,18 @@ 中電喵 SCAICT uwu # 中電喵 SCAICT uwu -[![同步代辦事項至 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) -[![website](https://img.shields.io/website?label=website&&url=https%3A%2F%2Fscaict.org%2F)](https://scaict.org/) -[![SCAICT store](https://img.shields.io/website?label=SCAICT-store&&url=https%3A%2F%2Fstore.scaict.org%2F)](https://store.scaict.org/) -[![document](https://img.shields.io/website?label=Document&&url=https%3A%2F%2Fstore.scaict.org%2F)](https://g.scaict.org/doc/) -[![Join Discord](https://img.shields.io/discord/959823904266944562?label=Discord&logo=discord&)](https://dc.scaict.org) -[![follow Instagram](https://img.shields.io/badge/follow-%40scaict.tw-pink?&logo=instagram)](https://www.instagram.com/scaict.tw/) - +[![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) +[![Website](https://img.shields.io/website?label=Website&&url=https%3A%2F%2Fscaict.org%2F)](https://scaict.org/) +[![SCAICT Store](https://img.shields.io/website?label=SCAICT+store&&url=https%3A%2F%2Fstore.scaict.org%2F)](https://store.scaict.org/) +[![Documentation](https://img.shields.io/website?label=Documentation&&url=https%3A%2F%2Fstore.scaict.org%2F)](https://g.scaict.org/doc/) +[![Join Discord server](https://img.shields.io/discord/959823904266944562?label=Discord&logo=discord&)](https://dc.scaict.org) +[![Follow Instagram](https://img.shields.io/badge/Follow-%40scaict.tw-pink?&logo=instagram)](https://www.instagram.com/scaict.tw/) + # SCAICT-uwu + 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. ## Overview @@ -23,7 +24,8 @@ SCAICT-uwu is a playful and interactive Discord bot that resides in the SCAICT D 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. ### About SCAICT -SCAICT, the Student Club Information Association of Central Taiwan, is an electronic engineering club composed of schools from central Taiwan. By combining the resources of the Central District, we actively organize educational events, activities, and competitions related to information technology, with the goal of facilitating the flow of technology and knowledge. + +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. ## Features @@ -41,8 +43,11 @@ Interact with SCAICT-uwu using slash commands in any channel where the bot is ac Some interactions with SCAICT-uwu occur within dedicated channels, allowing for more focused activities such as guessing colors or counting. ![color guess](https://raw.githubusercontent.com/SCAICT/doc/main/static/img/color-demo.gif) + ### Store -#### Buy somthing + +#### Buy something + 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,20 +57,20 @@ You can buy some products like our stickers or USB drives using Electric Points.
#### Play slot + Get some tickets, and you can play the slot machine to earn Electric Points. Just long press to start the slot.
-![solt demo](https://raw.githubusercontent.com/SCAICT/doc/main/static/img/slot-demo.gif) +![slot demo](https://raw.githubusercontent.com/SCAICT/doc/main/static/img/slot-demo.gif)
## Getting Started -1. **Join the Server** - Start by joining the SCAICT Discord community using this [link](https://dc.scaict.org). +1. **Join the Server** + Start by joining the SCAICT Discord community using this [link](https://dc.scaict.org). 2. **Earn Your First Electric Points** - Visit the `#daily-charge` channel and use the `/charge` command to receive your first set of Electric Points, the primary currency used within the bot's ecosystem. - + 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. 3. **Enjoy in Services** Explore the various commands and interactions SCAICT-uwu offers, and enjoy the space within the server, engaging and connecting with everyone. @@ -129,10 +134,9 @@ Get some tickets, and you can play the slot machine to earn Electric Points. Jus "element": [ percentage, reward ] } ``` -> 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) - +> 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) ## Acknowledgements -SCAICT-uww 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). +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). diff --git a/READMEtemp b/READMEtemp deleted file mode 100644 index 5dba1b7..0000000 --- a/READMEtemp +++ /dev/null @@ -1,82 +0,0 @@ - - - -
-中電喵 SCAICT uwu - -# 中電喵 SCAICT uwu - -住在中電會 Discord 伺服器的貓咪 - -[![同步代辦事項至 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) -[![官方網站](https://img.shields.io/website?label=官方網站&&url=https%3A%2F%2Fscaict.org%2F)](https://scaict.org/) -[![中電商店](https://img.shields.io/website?label=中電商店&&url=https%3A%2F%2Fstore.scaict.org%2F)](https://store.scaict.org/) -[![加入 Discord 伺服器](https://img.shields.io/discord/959823904266944562?label=Discord&logo=discord&)](https://dc.scaict.org) -[![追蹤 Instagram](https://img.shields.io/badge/follow-%40scaict.tw-pink?&logo=instagram)](https://www.instagram.com/scaict.tw/) - -
- -> 這個專案目前處於開發階段,並且可能會有一些問題。如果您發現了任何問題或有任何建議,請透過提交 issue 來通知我們。 - -## 如何部署? - -1. clone 此儲存庫。 -2. 在 Python 3.11 中建立環境。 -3. 安裝必要的函式庫。 - - ```bash - pip install -r requirements.txt - ``` - -4. 在 `DataBase/server.config.json` 中設定頻道。 -5. 啟動 SQL 伺服器。 -6. 在 Breadcrumbs SCAICT-uwu 的 `cog/core/sql_acc.py` 中設定 SQL 伺服器。 -7. 執行 Flask。 - - ```bash - flask run - ``` - -8. 執行 `main.py`。 - - ```bash - python main.py - ``` - -## 檔案 - -* `main.py`:中電喵。 -* `app.py`:中電商店。 -* `generate_secrets.py`:為 `app.py` 產生密鑰,執行後儲存在 `token.json` 中。 -* 資料庫 MySQL:使用外部伺服器,相關設定在 `cog/core/secret.py` 中。 -* `token.json`: - - ```json - { - "discord_token": "", - "secret_key": "", - "discord_client_id": "", - "discord_client_secret": "", - "discord_redirect_uri": "http://127.0.0.1:5000/callback", - "github_client_id": "", - "github_client_secret": "", - "github_redirect_uri": "http://127.0.0.1:5000/github/callback", - "github_discord_redirect_uri": "http://127.0.0.1:5000/github/discord-callback" - } - ``` - -* `DataBase/slot.json`: - - 設定老虎機的中獎機率。 - - ```json - { - "element": [ percentage, reward ] - } - ``` - -> 更詳細的說明文件敘述可以參考[這裡](https://g.scaict.org/doc/) - -## 鳴謝 - -中電喵是由中電會和[貢獻者們](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) 的設計素材。 diff --git a/RELEASE-NOTES-0.2.md b/RELEASE-NOTES-0.2.md new file mode 100644 index 0000000..d75ad9e --- /dev/null +++ b/RELEASE-NOTES-0.2.md @@ -0,0 +1,118 @@ +# SCAICT-uwu 0.2 + +## SCAICT-uwu 0.2 development branch + +THIS IS NOT A RELEASE YET + +The `development` branch is a beta-quality development branch. Use it at your +own risk! + +### Configuration changes for system administrators + +#### New configuration + +* … + +#### Changed configuration + +* … + +#### Removed configuration + +* … + +### New user-facing features + +* … + +### New features for sysadmins + +* … + +### New developer features + +* … + +### External dependency changes + +#### New external dependencies + +* Added py-cord dependency. + * Added propcache 0.2.1. + +#### New development-only external dependencies + +* … + +#### Changed external dependencies + +* Updated flask from 3.0.3 to 3.1.0. + * Updated blinker from 1.8.2 to 1.9.0 + * Updated click from 8.1.7 to 8.1.8. + * Updated jinja2 from 3.1.4 to 3.1.5. + * Updated markupsafe from 2.1.5 to 3.0.2. + * Updated werkzeug from 3.0.4 to 3.1.3. +* Updated mysql-connector-python from 8.4.0 to 9.1.0. +* Updated py-cord from 2.6.0 to 2.6.1. + * Updated aiohappyeyeballs from 2.4.0 to 2.4.4. + * Updated aiohttp from 3.10.5 to 3.11.11. + * Updated aiosignal from 1.3.1 to 1.3.2. + * Updated attrs from 24.2.0 to 24.3.0. + * Updated frozenlist from 1.4.1 to 1.5.0. + * Updated idna from 3.7 to 3.10. + * Updated multidict from 6.0.5 to 6.1.0. + * Updated yarl from 1.9.4 to 1.18.3. +* Updated requests dependencies. + * Updated certifi from 2024.7.4 to 2024.12.14. + * Updated charset-normalizer from 3.3.2 to 3.4.1. + * Updated idna from 3.7 to 3.10. + * Updated urllib3 from 2.2.2 to 2.3.0. + +#### Changed development-only external dependencies + +* Updated black from 24.8.0 to 24.10.0. + * Updated click from 8.1.7 to 8.1.8. + * Updated packaging from 24.1 to 24.2. + * Updated platformdirs from 4.2.2 to 4.3.6. +* Updated pylint from 3.2.6 to 3.3.3. + * Updated astroid from 3.2.4 to 3.3.8. + * Updated dill from 0.3.8 to 0.3.9. + * Updated platformdirs from 4.2.2 to 4.3.6. +* Updated pytest from 8.3.2 to 8.3.4. + * Updated packaging from 24.1 to 24.2. + +#### Removed external dependencies + +* … + +### Bug fixes + +* … + +### API changes + +* … + +### API internal changes + +* … + +### Languages updated + +SCAICT-uwu now supports 1 language. Localisations are updated regularly. + +Below only new and removed languages are listed. + +* … + +### Breaking changes + +* … + +### Deprecations + +* … + +### Other changes + +* … diff --git a/app.py b/app.py index 53bdc41..299b297 100644 --- a/app.py +++ b/app.py @@ -3,6 +3,7 @@ import os import random from urllib.parse import urlencode +import traceback # Third-party imports from flask import ( @@ -16,6 +17,7 @@ jsonify, ) import requests +from dotenv import load_dotenv # Local imports from cog.core.sql import write @@ -23,23 +25,28 @@ from cog.core.sql import link_sql from cog.core.sql import end from cog.core.sql import user_id_exists +from cog.api.api import Apis +from cog.api.gift import Gift app = Flask(__name__) - -# FILEPATH: /d:/GayHub/SCAICT-Discord-Bot/token.json -with open(f"{os.getcwd()}/token.json", encoding="utf-8") as token_file: - token_data = json.load(token_file) - -app.secret_key = token_data["secret_key"] -discord_client_id = token_data["discord_client_id"] -discord_client_secret = token_data["discord_client_secret"] -discord_redirect_uri = token_data["discord_redirect_uri"] -github_client_id = token_data["github_client_id"] -github_client_secret = token_data["github_client_secret"] -github_redirect_uri = token_data["github_redirect_uri"] -github_discord_redirect_uri = token_data["github_discord_redirect_uri"] -discord_token = token_data["discord_token"] -send_gift_role = token_data["send_gift_role"] +load_dotenv(f"{os.getcwd()}/.env", verbose=True, override=True) + +app.secret_key = os.getenv("SECRET_KEY") +discord_client_id = os.getenv("DISCORD_CLIENT_ID") +discord_client_secret = os.getenv("DISCORD_CLIENT_SECRET") +discord_redirect_uri = os.getenv("DISCORD_REDIRECT_URI") +github_client_id = os.getenv("GITHUB_CLIENT_ID") +github_client_secret = os.getenv("GITHUB_CLIENT_SECRET") +github_redirect_uri = os.getenv("GITHUB_REDIRECT_URI") +github_discord_redirect_uri = os.getenv("GITHUB_DISCORD_REDIRECT_URI") +discord_token = os.getenv("DISCORD_TOKEN") +send_gift_role = os.getenv("SEND_GIFT_ROLE") +guild_ID = os.getenv("GUILD_ID") +# 將字串轉換為列表 +if send_gift_role: + send_gift_role = [str(role_id) for role_id in send_gift_role.split(",")] +else: + send_gift_role_list = [] @app.errorhandler(404) @@ -61,7 +68,6 @@ def login(): params["state"] = redirurl # 將參數進行 URL 編碼並組合成最終的 URL urlencoded = urlencode(params) - print(f"{base_url}?\n\n{urlencoded}") return redirect(f"{base_url}?{urlencoded}") @@ -74,15 +80,13 @@ def logout(): @app.route("/api/mlist") def listt(): if not session: - return jsonify({"resulet": "you must loggin", "status": 403}) + return jsonify({"result": "you must login", "status": 403}) api_admin = session.get( "user" ) # {'avatar': 'https://cdn.discordapp.com/avatars/898141506588770334/a_c81acdd4a925993d053a6fe9ed990c14.png', 'id': '898141506588770334', 'name': 'iach526526'} api_admin_id = api_admin.get("id") headers = {"Authorization": f"Bot {discord_token}"} - url = ( - f"https://discord.com/api/v10/guilds/1203338928535379978/members/{api_admin_id}" - ) + url = f"https://discord.com/api/v10/guilds/{guild_ID}/members/{api_admin_id}" response = requests.get(url, headers=headers, timeout=10) user_data = response.json() if response.status_code != 200: @@ -102,127 +106,101 @@ def listt(): @app.route("/api/send/") +# api/send/{recipient}?gift_type={電電點|抽獎券}count={count} def send(target_user_id): if not session: - return jsonify({"resulet": "you must loggin", "status": 403}) + return jsonify({"result": "you must login", "status": 403}) try: api_admin = session.get( "user" ) # {'avatar': 'https://cdn.discordapp.com/avatars/898141506588770334/a_c81acdd4a925993d053a6fe9ed990c14.png', 'id': '898141506588770334', 'name': 'iach526526'} api_admin_id = api_admin.get("id") api_admin_name = api_admin.get("name") - headers = {"Authorization": f"Bot {discord_token}"} - url = f"https://discord.com/api/v10/guilds/1203338928535379978/members/{api_admin_id}" - response = requests.get(url, headers=headers, timeout=10) - user_data = response.json() - if response.status_code != 200: - return ( - jsonify({"error": "Failed to fetch user information"}), - response.status_code, + discord_api = Apis(discord_token, guild_ID) + request_admin = discord_api.get_user(api_admin_id) + gift_type = request.args.get("gift_type", "電電點") # 預設為"電電點" + gift_amount = request.args.get("count", 1) # 預設數量為1 + if "error" in request_admin: + # 如果有錯誤,返回錯誤訊息和詳細報錯 + return jsonify( + { + "result": "Failed to fetch user information in admin id", + "status": 500, + "error_details": request_admin.get("details"), + } ) - if send_gift_role not in user_data.get("roles", []): + admin_roles = request_admin.get("roles", []) + # 確保發起人有權限發送禮物 + if set(send_gift_role) & set(admin_roles) == set(): return jsonify( {"result": "You do not have permission to use this", "status": 403} ) - gift_type = request.args.get("gift_type", "電電點") # 預設為"電電點" if gift_type not in ["電電點", "抽獎券"]: return jsonify({"result": "Invalid gift type", "status": 400}) - count = request.args.get("count", 1) # 預設數量為1 try: - count = int(count) # 確保 count 是整數 + gift_amount = int(gift_amount) # 確保 count 是整數 except ValueError: return jsonify({"result": "Invalid count value", "status": 400}) - print(gift_type, count) - url = f"https://discord.com/api/v10/guilds/1203338928535379978/members/{target_user_id}" - response = requests.get(url, headers=headers, timeout=10) - user_data = response.json() - if response.status_code != 200: - # 確保 URL 的 target_user_id 在伺服器裡面 - return ( - jsonify({"error": "Failed to fetch user information in tg id"}), - response.status_code, - ) - # 送禮物 - try: - url = "https://discord.com/api/v10/users/@me/channels" - headers = { - "Authorization": f"Bot {discord_token}", - "Content-Type": "application/json", - } - json_data = {"recipient_id": target_user_id} - except requests.RequestException as e: - return jsonify( - {"result": "interal server error", "status": 500, "error": str(e)} - ) - except Exception as e: + # 確保目標用戶存在 + user_data = discord_api.get_user(target_user_id) + if "error" in user_data: + # 如果有錯誤,返回錯誤訊息和詳細信息 return jsonify( - {"result": "interal server error", "status": 500, "error": str(e)} - ) - response = requests.post(url, headers=headers, json=json_data, timeout=10) - dm_channel = response.json() - dm_room = dm_channel["id"] - url = f"https://discord.com/api/v10/channels/{dm_room}/messages" - # 發送按鈕訊息 - headers = { - "Authorization": f"Bot {discord_token}", - "Content-Type": "application/json", - } - embed = { - "title": f"你收到了 {count} {gift_type}!", - "color": 3447003, # (藍色) - "description": ":gift:", - } - button = { - "type": 1, - "components": [ { - "type": 2, - "label": "前往確認", - "style": 5, # `5` 表示 Link Button - "url": "https://store.scaict.org", # 要導向的連結 + "result": "Failed to fetch user information in target id (which is in url path)", + "status": 500, + "error_details": request_admin.get("details"), } - ], - } - json_data = { - "embeds": [embed], - "components": [button], - "tts": False, # Text-to-speech, 默認為 False - } + ) + # 送禮物 + user_name = user_data["user"]["username"] try: - response = requests.post(url, headers=headers, json=json_data, timeout=10) + new_gift = Gift( + discord_token, guild_ID, target_user_id + ) # create a new gift object + if new_gift.dm_room is None: + return jsonify( + { + "result": "Failed to create DM channel", + "status": 500, + "error": new_gift.error_msg, + } + ) + message_id = new_gift.send_gift(gift_type, gift_amount) connect, cursor = link_sql() - message_id = response.json().get("id") - print(message_id) if not user_id_exists(target_user_id, "user", cursor): cursor.execute( "INSERT INTO user (uid) VALUE(%s)", (target_user_id,) ) # 這裡要調用 api 去抓使用者名稱和 Mail cursor.execute( "INSERT into gift (btnID,type,count,recipient,received,sender) VALUE(%s,%s,%s,%s,%s,%s)", - (message_id, gift_type, count, target_user_id, True, api_admin_name), + ( + message_id, + gift_type, + gift_amount, + user_name, + True, + api_admin_name, + ), ) gift_type = "point" if gift_type == "電電點" else "ticket" query = f"update user set {gift_type}={gift_type}+%s where uid=%s" - cursor.execute(query, (count, target_user_id)) + cursor.execute(query, (gift_amount, target_user_id)) end(connect, cursor) + except Exception as e: return jsonify( { - "result": "interal server error(SQL) when insert gift", + "result": "internal server error(SQL) when insert gift", "status": 500, "error": str(e), } ) - # 待辦:用戶端那裏也要提示 - response = requests.post(url, headers=headers, json=json_data, timeout=10) - if response.status_code != 200: - return jsonify( - {"error": "Failed to send message", "status": response.status_code} - ) return jsonify({"result": "success", "status": 200}) except Exception as e: + traceback.print_exc() return jsonify( - {"result": "interal server error", "status": 500, "error": str(e)} + {"result": "internal server error", "status": 500, "error": str(e)} ) @@ -302,7 +280,6 @@ def discord_callback(): response = requests.post( "https://discord.com/api/oauth2/token", data=data, headers=headers ) - print(response.json()) access_token = response.json()["access_token"] headers = {"Authorization": f"Bearer {access_token}"} # pylint: disable-next = missing-timeout @@ -316,7 +293,7 @@ def discord_callback(): return redirect(url_for("star_uwu")) -# make filder static in templates/static static +# make filter static in templates/static static @app.route("/static/") def staticfiles(path): return send_from_directory("static", path) @@ -390,7 +367,7 @@ def product_list(): @app.route("/buyProduct", methods=["POST"]) def buy_product(): - # Recieve POST request, get product id and check if logged in + # Receive POST request, get product id and check if logged in discord_user = session.get("user") if not discord_user: return "請重新登入" @@ -501,7 +478,6 @@ def github_callback(): } # pylint: disable-next = missing-timeout response = requests.post(token_url, headers=headers, data=data) - print(response.json()) session["access_token"] = response.json()["access_token"] return redirect(url_for("star_uwu")) @@ -515,7 +491,7 @@ def insert_user(user_id, table, cursor): # 初始化(新增)傳入該ID的 if "access_token" not in session: print("GitHub access token not found!") return redirect(url_for("github_login")) - # if dc not loggin + # if dc not login discord_user = session.get("user") if not discord_user: # pylint: disable-next = line-too-long @@ -527,7 +503,6 @@ def insert_user(user_id, table, cursor): # 初始化(新增)傳入該ID的 headers = {"Authorization": f"token {session['access_token']}"} # pylint: disable-next = missing-timeout user_response = requests.get(user_url, headers=headers) - print(user_response.json()) github_username = user_response.json()["login"] github_email = user_response.json()["email"] connection, cursor = link_sql() # SQL 會話 @@ -540,20 +515,18 @@ def insert_user(user_id, table, cursor): # 初始化(新增)傳入該ID的 repo_name = "SCAICT-uwu" star_url = f"https://api.github.com/user/starred/{repo_owner}/{repo_name}" headers = {"Authorization": f"token {session['access_token']}"} - print(session["access_token"]) # Sending a PUT request to star the repository # pylint: disable-next = missing-timeout response = requests.put(star_url, headers=headers) - print(response.text) # Checking the response status and returning an appropriate message if response.ok: print(f"Successfully starred {repo_owner}/{repo_name}! {response}") connection, cursor = link_sql() # SQL 會話 if not user_id_exists( discord_user["id"], "user", cursor - ): # 該 uesr id 不在user表格內,插入該筆使用者資料 + ): # 該 user id 不在user表格內,插入該筆使用者資料 insert_user(discord_user["id"], "user", cursor) - # if already starred. liveuwu is 1 + # if already starred. loveuwu is 1 if read(discord_user["id"], "loveuwu", cursor): end(connection, cursor) return render_template("already.html") diff --git a/cog/admin.py b/cog/admin.py new file mode 100644 index 0000000..8833f5a --- /dev/null +++ b/cog/admin.py @@ -0,0 +1,25 @@ +# Standard imports +# import csv +# from datetime import datetime, timedelta +# import json +# import os + +# Third-party imports +import discord +from build.build import Build + +# Local imports + + +class ManagerCommand(Build): + @discord.slash_command(name="reload", description="你是管理員才讓你用") + async def reload(self, ctx, package): + if not ctx.author.guild_permissions.administrator: + await ctx.respond("你沒有權限使用這個指令!", ephemeral=True) + return + self.bot.reload_extension(f"cog.{package}") + await ctx.respond(f"🔄 {package} reloaded") + + +def setup(bot): + bot.add_cog(ManagerCommand(bot)) diff --git a/cog/admin_gift.py b/cog/admin_gift.py index 4ce8907..b784c3f 100644 --- a/cog/admin_gift.py +++ b/cog/admin_gift.py @@ -1,5 +1,6 @@ # Standard imports from datetime import datetime +import traceback # Third-party imports import discord @@ -76,6 +77,10 @@ async def get_gift(self, button: discord.ui.Button, ctx) -> None: button.disabled = True # 關閉按鈕,避免重複點擊 await ctx.response.edit_message(view=self) + def cache_users_by_name(self): + # 將所有使用者名稱和對應的使用者物件存入字典 + return {user.name: user for user in self.bot.users} + @discord.slash_command(name="發送禮物", description="dm_gift") async def send_dm_gift( self, @@ -89,6 +94,7 @@ async def send_dm_gift( if not ctx.author.guild_permissions.administrator: await ctx.respond("你沒有權限使用這個指令!", ephemeral=True) return + SendGift.user_cache = self.cache_users_by_name() try: await ctx.defer() # 確保機器人請求不會超時 # 不能發送負數 @@ -96,24 +102,28 @@ async def send_dm_gift( await ctx.respond("不能發送 0 以下個禮物!", ephemeral=True) return manager = ctx.author # return - print(type(manager)) target_usernames = target_str.split(",") target_users = [] async def fetch_user_by_name(name): user_obj = discord.utils.find(lambda u: u.name == name, self.bot.users) if user_obj: - return await self.bot.fetch_user(user_obj.id) + try: + return await self.bot.fetch_user(user_obj.id) + except Exception as e: + print(f"Failed to fetch user with ID {user_obj.id}: {str(e)}") + return None for username in target_usernames: username = username.strip() + if username not in SendGift.user_cache: + continue try: user = await fetch_user_by_name(username) target_users.append(user) except (ValueError, Exception) as e: await ctx.respond(f"找不到使用者 : {username}{e}", ephemeral=True) return - # DM 一個 Embed 和領取按鈕 for target_user in target_users: await send_gift_button( @@ -124,6 +134,7 @@ async def fetch_user_by_name(name): f"{manager} 已發送 {count} {gift_type} 給 {', '.join([user.name for user in target_users])}" ) except Exception as e: + traceback.print_exc() await ctx.respond(f"伺服器內部出現錯誤:{e}", ephemeral=True) diff --git a/cog/api/api.py b/cog/api/api.py new file mode 100644 index 0000000..cffa02b --- /dev/null +++ b/cog/api/api.py @@ -0,0 +1,87 @@ +# Standard imports +# import json + +# Third-party imports +import requests + + +class Apis: + def __init__(self, api_key: str, guild_id: int): + self.api_key = api_key + self.guild_id = guild_id + self.headers = { + "Authorization": f"Bot {self.api_key}", + "Content-Type": "application/json", + } + + def get_user(self, uid): + """ + API 回傳的資料格式範例,已經把一些敏感資料隱藏掉 + { + "avatar": null, + "banner": null, + "communication_disabled_until": null, + "flags": 0, + "joined_at": "", + "nick": null, + "pending": false, + "premium_since": null, + "roles": [ + "12348763", + "12448763", + "12548763" + ], + "unusual_dm_activity_until": null, + "user": { + "id": "", + "username": "", + "avatar": "", + "discriminator": "0", + "public_flags": 256, + "flags": 256, + "banner": "", + "accent_color": 2054367, + "global_name": "", + "avatar_decoration_data": { + "asset": "a_d3da36040163ee0f9176dfe7ced45cdc", + "sku_id": "1144058522808614923", + "expires_at": null + }, + "banner_color": "#1f58df", + "clan": null + }, + "mute": false, + "deaf": false + } + """ + try: + url = f"https://discord.com/api/v10/guilds/{self.guild_id}/members/{uid}" + usr = requests.get(url, headers=self.headers, timeout=5) + usr.raise_for_status() # 檢查 HTTP 狀態碼 + return usr.json() + except requests.exceptions.RequestException as e: + # 如果發生錯誤,返回一個包含錯誤訊息和詳細報錯的字典 + return {"error": "get_user error", "details": str(e)} + + def create_dm_channel(self, target_user_id: str): + try: + url = "https://discord.com/api/v10/users/@me/channels" + json_data = {"recipient_id": target_user_id} + response = requests.post( + url, headers=self.headers, json=json_data, timeout=10 + ) + response.raise_for_status() # Raise an HTTPError for bad responses + dm_channel = response.json() + return dm_channel["id"] + except requests.RequestException as e: + return { + "result": "internal server error", + "status": 500, + "error": str(e), + } + except Exception as e: + return { + "result": "internal server error", + "status": 500, + "error": str(e), + } diff --git a/cog/api/gift.py b/cog/api/gift.py new file mode 100644 index 0000000..0510f6e --- /dev/null +++ b/cog/api/gift.py @@ -0,0 +1,64 @@ +import requests + + +class Gift: + def __init__(self, api_key: str, guild_id: int, recipient_id: int): + self.api_key = api_key + self.guild_id = guild_id + self.error_msg = None + self.headers = { + "Authorization": f"Bot {self.api_key}", + "Content-Type": "application/json", + } + try: + fetch_usr = self.__new_dm(recipient_id) + if "id" in fetch_usr: + self.dm_room = fetch_usr["id"] + else: + self.dm_room = None + except Exception as e: + self.dm_room = None + self.error_msg = str(e) + + def __new_dm(self, uid: int) -> dict: + try: + url = "https://discord.com/api/v10/users/@me/channels" + payload = {"recipient_id": uid} + # {'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}]} + response = requests.post(url, headers=self.headers, json=payload, timeout=5) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"result": "internal server error", "status": 500, "error": str(e)} + except Exception as e: + return {"result": "internal server error", "status": 500, "error": str(e)} + + def _gen_msg(self, gift_type: str, gift_amount: int) -> str: + embed = { + "title": f"你收到了 {gift_amount} {gift_type}!", + "color": 3447003, # (藍色) + "description": ":gift:", + } + button = { + "type": 1, + "components": [ + { + "type": 2, + "label": "前往確認", + "style": 5, # `5` 表示 Link Button + "url": "https://store.scaict.org", # 要導向的連結 + } + ], + } + json_data = { + "embeds": [embed], + "components": [button], + "tts": False, # Text-to-speech, 默認為 False + } + return json_data + + def send_gift(self, gift_type: str, gift_amount: int) -> str: + url = f"https://discord.com/api/v10/channels/{self.dm_room}/messages" + payload = self._gen_msg(gift_type, gift_amount) + response = requests.post(url, headers=self.headers, json=payload, timeout=5) + return response.json().get("id") diff --git a/cog/class_role.py b/cog/class_role.py index 14de07d..df71b41 100644 --- a/cog/class_role.py +++ b/cog/class_role.py @@ -124,7 +124,7 @@ async def send_modal(self, ctx): await ctx.send(embed=embed, view=self.TokenVerifyButton()) @discord.slash_command(description="新增主題課程") - # pylint: disable-next = too-many-arguments + # pylint: disable-next = too-many-arguments, too-many-positional-arguments async def add_class( self, ctx, class_code: str, name: str, theme: str, teacher: str, time: str ): diff --git a/cog/comment.py b/cog/comment.py index a27956c..acf38c7 100644 --- a/cog/comment.py +++ b/cog/comment.py @@ -141,7 +141,7 @@ def today_comment(user_id, message, cursor): try: # 新增該user的資料表 if not user_id_exists(user_id, "user", cursor): - # 該 uesr id 不在user資料表內,插入該筆使用者資料 + # 該 user id 不在user資料表內,插入該筆使用者資料 insert_user(user_id, "user", cursor) if not user_id_exists(user_id, "comment_points", cursor): insert_user(user_id, "comment_points", cursor) @@ -187,7 +187,7 @@ async def count(message): # Allow both plain and monospace formatting based_number = re.sub("^`([^\n]+)`$", "\\1", raw_content) - # If is valid 4-digit whitespace delimeter format + # If is valid 4-digit whitespace delimiter format # (with/without base), then strip whitespace characters. # # Test cases: @@ -209,7 +209,7 @@ async def count(message): based_number, ): based_number = based_number.replace(" ", "") - # If is valid 3-digit comma delimeter format + # If is valid 3-digit comma delimiter format # (10-based, without base) elif counting_base == 10 and re.match( "^([0-9]{1,3}(,[0-9]{3})*)$", based_number diff --git a/cog/core/secret.py b/cog/core/secret.py index 4b09446..f50b094 100644 --- a/cog/core/secret.py +++ b/cog/core/secret.py @@ -6,6 +6,7 @@ import mysql.connector +load_dotenv(f"{os.getcwd()}/.env") DB_USER = os.getenv("MYSQL_USER") DB_PASSWORD = os.getenv("MYSQL_PASSWORD") DB_NAME = os.getenv("MYSQL_DATABASE") @@ -14,7 +15,6 @@ def connect(): - load_dotenv(f"{os.getcwd()}/.env") return mysql.connector.connect( user=DB_USER, password=DB_PASSWORD, diff --git a/cog/core/sendgift.py b/cog/core/sendgift.py index d98f6cf..a26d714 100644 --- a/cog/core/sendgift.py +++ b/cog/core/sendgift.py @@ -36,9 +36,8 @@ async def record_db( ) end(connection, cursor) except Exception as e: - raise DBError("無法成功插入禮物資料進資料庫") from e - finally: end(connection, cursor) + raise DBError("無法成功插入禮物資料進資料庫") from e try: await target_user.send(embed=embed) diff --git a/cog/ctf.py b/cog/ctf.py index dff0416..1dd2826 100644 --- a/cog/ctf.py +++ b/cog/ctf.py @@ -279,6 +279,7 @@ async def callback(self, interaction: discord.Interaction) -> None: @ctf_commands.command(name="create", description="新題目") # 新增題目 + # pylint: disable-next = too-many-positional-arguments async def create( self, ctx, diff --git a/cog/game.py b/cog/game.py index 9e2dfba..898d9b7 100644 --- a/cog/game.py +++ b/cog/game.py @@ -37,12 +37,12 @@ def get_channels(): class Game(commands.Cog): # User can use this command to play ✊-🤚-✌️ with the bot in the command channel @discord.slash_command(name="rock_paper_scissors", description="玩剪刀石頭布") - # useser can choose ✊, 🤚, or ✌️ in their command + # user can choose ✊, 🤚, or ✌️ in their command async def rock_paper_scissors( self, interaction, choice: discord.Option(str, choices=["✊", "🤚", "✌️"]) ): if interaction.channel.id != get_channels()["channel"]["commandChannel"]: - await interaction.response.send_message("這裡不是指令區喔") + await interaction.response.send_message("這裡不是指令區喔", ephemeral=True) return user_id = interaction.user.id user_display_name = interaction.user @@ -51,11 +51,15 @@ async def rock_paper_scissors( point = read(user_id, "point", cursor) if point < 5: - await interaction.response.send_message("你的電電點不足以玩這個遊戲") + await interaction.response.send_message( + "你的電電點不足以玩這個遊戲", ephemeral=True + ) end(connection, cursor) return if choice not in ["✊", "🤚", "✌️"]: - await interaction.response.send_message("請輸入正確的選擇") + await interaction.response.send_message( + "請輸入正確的選擇", ephemeral=True + ) end(connection, cursor) return diff --git a/docs/abstract_schema_table.json b/docs/abstract_schema_table.json index 0544acb..ea2502f 100644 --- a/docs/abstract_schema_table.json +++ b/docs/abstract_schema_table.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/schema#", "description": "Abstract description of a scaict-uwu database table, derived from MediaWiki database table schema", - "type": "object", + "type": "array", "additionalProperties": false, "properties": { "name": { @@ -41,6 +41,7 @@ "datetime", "datetimetz", "decimal", + "enum", "float", "integer", "smallint", @@ -79,6 +80,13 @@ "default": null, "minimum": 0 }, + "enum": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, "notnull": { "type": "boolean", "description": "Indicates whether the column is nullable or not", @@ -121,7 +129,7 @@ }, "enum_values": { "type": "array", - "description": "Values to use with type 'mwenum'", + "description": "Values to use with type 'enum'", "additionalItems": false, "items": { "type": "string" diff --git a/docs/database_layout.html b/docs/database_layout.html new file mode 100644 index 0000000..5feb469 --- /dev/null +++ b/docs/database_layout.html @@ -0,0 +1,335 @@ + + + + +SCAICT-uwu database table layout diagram + + + +
+
+

Database schema of SCAICT-uwu.

+
+
+
+
+

User

+
+ + + + + + +
+
+
+

user

+
+
    +
  • DCname VARCHAR(32)
  • +
  • uid BIGINT
  • +
  • DCMail VARCHAR(320)
  • +
  • githubName VARCHAR(39)
  • +
  • githubMail VARCHAR(320)
  • +
  • loveuwu TINYINT(1)
  • +
  • point INT
  • +
  • ticket INT
  • +
  • charge_combo INT
  • +
  • next_lottery INT
  • +
  • last_charge DATETIME
  • +
  • last_comment DATE
  • +
  • today_comments INT
  • +
+
+
+
+
+
+

Comment points

+
+ + + + + + +
+
+
+

comment_points

+
+
    +
  • + seq INT
  • +
  • uid BIGINT
  • +
  • times INT
  • +
  • next_reward INT
  • +
+
+
+
+
+
+

Game

+
+ + + + + + +
+
+
+

game

+
+
    +
  • + seq BIGINT
  • +
  • lastID BIGINT
  • +
  • niceColor VARCHAR(3)
  • +
  • nicecolorround INT
  • +
  • niceColorCount BIGINT
  • +
+
+
+
+
+
+

Gift

+
+ + + + + + +
+
+
+

gift

+
+
    +
  • + btnID BIGINT
  • +
  • type ENUM(…)
  • +
  • count INT
  • +
  • recipient VARCHAR(32)
  • +
  • received TINYINT(1)
  • +
  • sender VARCHAR(32)
  • +
+
+
+
+
+
+

CTF

+
+ + + + + + + +
+
+
+

ctf_data

+
+
    +
  • + id BIGINT
  • +
  • flags VARCHAR(255)
  • +
  • score INT
  • +
  • restrictions VARCHAR(255)
  • +
  • message_id BIGINT
  • +
  • case_status TINYINT(1)
  • +
  • start_time DATETIME
  • +
  • end_time VARCHAR(255)
  • +
  • title VARCHAR(255)
  • +
  • tried INT
  • +
+
+
+
+
+

ctf_history

+
+
    +
  • + data_id BIGINT
  • +
  • uid BIGINT
  • +
  • count INT
  • +
  • solved TINYINT(1)
  • +
+
+
+
+
+

+
+ + diff --git a/main.py b/main.py index 45316c5..942d6af 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,9 @@ # Standard imports -import json import os # Third-party imports import discord +from dotenv import load_dotenv # Local imports from channel_check import update_channel # update_channel程式從core目錄底下引入 @@ -14,10 +14,6 @@ intt.message_content = True bot = discord.Bot(intents=intt) -# 變更目前位置到專案根目錄(SCAICT-DISCORD-BOT 資料夾),再找檔案 -os.chdir("./") -with open(f"{os.getcwd()}/token.json", "r", encoding="utf-8") as file: - token = json.load(file) for filename in os.listdir(f"{os.getcwd()}/cog"): if filename.endswith(".py"): @@ -25,6 +21,18 @@ print(f"📖 {filename} loaded") # test +@bot.command() +async def load(ctx, extension): + bot.load_extension(f"cog.{extension}") + await ctx.send(f"📖 {extension} loaded") + + +@bot.command() +async def unload(ctx, extension): + bot.unload_extension(f"cog.{extension}") + await ctx.send(f"📖 {extension} unloaded") + + @bot.event async def on_ready(): print(f"✅ {bot.user} is online") @@ -34,4 +42,6 @@ async def on_ready(): if __name__ == "__main__": - bot.run(token["discord_token"]) + load_dotenv(f"{os.getcwd()}/.env", verbose=True, override=True) + bot_token = os.getenv("DISCORD_TOKEN") + bot.run(bot_token) diff --git a/pyproject.toml b/pyproject.toml index 9f6e61d..6baeef5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ name = "scaict-uwu" # PEP 440 # @see https://sethmlarson.dev/pep-440 -version = "0.1.9.dev0" +version = "0.2.0" description = "A cat living in SCAICT Discord server." readme.file = "README.md" readme.content-type = "text/markdown" @@ -28,15 +28,15 @@ urls.repository = "https://github.com/SCAICT/SCAICT-uwu.git" urls.issues = "https://github.com/SCAICT/SCAICT-uwu/issues" # Lock file: requirements.txt dependencies = [ - "flask == 3.0.3", - "mysql-connector-python == 8.4.0", - "py-cord == 2.6.0", + "flask == 3.1.0", + "mysql-connector-python == 9.2.0", + "py-cord == 2.6.1", "python-dotenv == 1.0.1", "requests == 2.32.3", ] # Lock file: requirements_dev.txt optional-dependencies.dev = [ - "black == 24.8.0", - "pylint == 3.2.6", - "pytest == 8.3.2", + "black == 25.1.0", + "pylint == 3.3.4", + "pytest == 8.3.4", ] diff --git a/requirements.txt b/requirements.txt index b643953..46ef619 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,24 @@ -aiohappyeyeballs == 2.4.0 -aiohttp == 3.10.5 -aiosignal == 1.3.1 -attrs == 24.2.0 -blinker == 1.8.2 -certifi == 2024.7.4 -charset-normalizer == 3.3.2 -click == 8.1.7 +aiohappyeyeballs == 2.4.4 +aiohttp == 3.11.11 +aiosignal == 1.3.2 +attrs == 25.1.0 +blinker == 1.9.0 +certifi == 2025.1.31 +charset-normalizer == 3.4.1 +click == 8.1.8 colorama == 0.4.6 -flask == 3.0.3 -frozenlist == 1.4.1 -idna == 3.7 +flask == 3.1.0 +frozenlist == 1.5.0 +idna == 3.10 itsdangerous == 2.2.0 -jinja2 == 3.1.4 -markupsafe == 2.1.5 -multidict == 6.0.5 -mysql-connector-python == 8.4.0 -py-cord == 2.6.0 +jinja2 == 3.1.5 +markupsafe == 3.0.2 +multidict == 6.1.0 +mysql-connector-python == 9.2.0 +propcache == 0.2.1 +py-cord == 2.6.1 python-dotenv == 1.0.1 requests == 2.32.3 -urllib3 == 2.2.2 -werkzeug == 3.0.4 -yarl == 1.9.4 +urllib3 == 2.3.0 +werkzeug == 3.1.3 +yarl == 1.18.3 diff --git a/requirements_dev.txt b/requirements_dev.txt index 391a504..1999913 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,16 +1,16 @@ -astroid == 3.2.4 -black == 24.8.0 -click == 8.1.7 +astroid == 3.3.8 +black == 25.1.0 +click == 8.1.8 colorama == 0.4.6 -dill == 0.3.8 +dill == 0.3.9 iniconfig == 2.0.0 -isort == 5.13.2 +isort == 6.0.0 mccabe == 0.7.0 mypy-extensions == 1.0.0 -packaging == 24.1 +packaging == 24.2 pathspec == 0.12.1 -platformdirs == 4.2.2 +platformdirs == 4.3.6 pluggy == 1.5.0 -pylint == 3.2.6 -pytest == 8.3.2 +pylint == 3.3.4 +pytest == 8.3.4 tomlkit == 0.13.2 diff --git a/src/scaict_uwu/bot.py b/src/scaict_uwu/bot.py new file mode 100644 index 0000000..5a24462 --- /dev/null +++ b/src/scaict_uwu/bot.py @@ -0,0 +1,9 @@ +""" +Entry point for Discord bot using Pycord. +""" + + +def run() -> None: + """ + TODO: Complete this + """ diff --git a/src/scaict_uwu/bot/__init__.py b/src/scaict_uwu/core/__init__.py similarity index 100% rename from src/scaict_uwu/bot/__init__.py rename to src/scaict_uwu/core/__init__.py diff --git a/src/scaict_uwu/bot/cog/__init__.py b/src/scaict_uwu/core/bot/__init__.py similarity index 100% rename from src/scaict_uwu/bot/cog/__init__.py rename to src/scaict_uwu/core/bot/__init__.py diff --git a/src/scaict_uwu/bot/emoji/__init__.py b/src/scaict_uwu/core/bot/channels/__init__.py similarity index 100% rename from src/scaict_uwu/bot/emoji/__init__.py rename to src/scaict_uwu/core/bot/channels/__init__.py diff --git a/src/scaict_uwu/config/__init__.py b/src/scaict_uwu/core/bot/cogs/__init__.py similarity index 100% rename from src/scaict_uwu/config/__init__.py rename to src/scaict_uwu/core/bot/cogs/__init__.py diff --git a/src/scaict_uwu/installer/__init__.py b/src/scaict_uwu/core/bot/emoji/__init__.py similarity index 100% rename from src/scaict_uwu/installer/__init__.py rename to src/scaict_uwu/core/bot/emoji/__init__.py diff --git a/src/scaict_uwu/installer/cog/__init__.py b/src/scaict_uwu/core/bot/slash_commands/__init__.py similarity index 100% rename from src/scaict_uwu/installer/cog/__init__.py rename to src/scaict_uwu/core/bot/slash_commands/__init__.py diff --git a/src/scaict_uwu/logging/__init__.py b/src/scaict_uwu/core/bot/users/__init__.py similarity index 100% rename from src/scaict_uwu/logging/__init__.py rename to src/scaict_uwu/core/bot/users/__init__.py diff --git a/src/scaict_uwu/maintenance/__init__.py b/src/scaict_uwu/core/config/__init__.py similarity index 100% rename from src/scaict_uwu/maintenance/__init__.py rename to src/scaict_uwu/core/config/__init__.py diff --git a/src/scaict_uwu/core/config/config.py b/src/scaict_uwu/core/config/config.py new file mode 100644 index 0000000..1ae2e48 --- /dev/null +++ b/src/scaict_uwu/core/config/config.py @@ -0,0 +1,17 @@ +""" +Config +""" + +# Standard imports +import sys +from typing import ClassVar + + +class Config: + options: ClassVar[dict] + + def get_option(self, name: str): + if name in self.options: + return self.options[name] + + sys.exit("NoSuchOptionExpection") diff --git a/src/scaict_uwu/core/config/config_factory.py b/src/scaict_uwu/core/config/config_factory.py new file mode 100644 index 0000000..e91dff2 --- /dev/null +++ b/src/scaict_uwu/core/config/config_factory.py @@ -0,0 +1,11 @@ +""" +Config factory +""" + +# Local imports +from .config import Config + + +class ConfigFactory: + def get(self) -> Config: + return Config() diff --git a/src/scaict_uwu/core/config/config_names.py b/src/scaict_uwu/core/config/config_names.py new file mode 100644 index 0000000..5145bc1 --- /dev/null +++ b/src/scaict_uwu/core/config/config_names.py @@ -0,0 +1,7 @@ +""" +Config names +""" + + +class ConfigNames: + pass diff --git a/src/scaict_uwu/core/config/config_schema.py b/src/scaict_uwu/core/config/config_schema.py new file mode 100644 index 0000000..602704d --- /dev/null +++ b/src/scaict_uwu/core/config/config_schema.py @@ -0,0 +1,7 @@ +""" +Config schema +""" + + +class ConfigSchema: + pass diff --git a/src/scaict_uwu/maintenance/scripts/__init__.py b/src/scaict_uwu/core/maintenance/__init__.py similarity index 100% rename from src/scaict_uwu/maintenance/scripts/__init__.py rename to src/scaict_uwu/core/maintenance/__init__.py diff --git a/src/scaict_uwu/core/maintenance/maintenance.py b/src/scaict_uwu/core/maintenance/maintenance.py new file mode 100644 index 0000000..a6e2a22 --- /dev/null +++ b/src/scaict_uwu/core/maintenance/maintenance.py @@ -0,0 +1,78 @@ +""" +This is the module for the abstract class for all maintenance scripts. +""" + +# Local imports +# scaict_uwu.maintenance.parameters +from .parameters import MaintenanceParameters + + +class Maintenance: + """ + Abstract class for all maintenance scripts. + """ + + _parameters: MaintenanceParameters + """ + _parameters (MaintenanceParameters) + """ + + def __init__(self) -> None: + """ + Default constructor. Child classes should call this *first* if + implementing their own constructors. + """ + self._parameters = MaintenanceParameters() + self.add_default_params() + + def add_description(self, description: str) -> None: + """ + Set description for maintenance scription. + + Parameters: + description (str): The description to set. + """ + self.get_parameters().set_description(description) + + def get_parameters(self) -> MaintenanceParameters: + """ + Returns: + MaintenanceParameters + """ + return self._parameters + + def add_default_params(self) -> None: + """ + TODO + """ + + def can_execute_without_config(self) -> bool: + """ + Whether this script can run without config. + + Scripts that need to be able to run before installed should override\ + this to return true. + + Scripts that return true from this method will fail with a fatal error + if attempt to access the database. + + Subclasses that override this method to return true should also\ + override get_db_type() to return self::DB_NONE. + """ + return False + + def execute(self) -> bool: + """ + Do the actual work. All child classes will need to implement this. + + Returns: + bool: True for success, false for failure. + Returning false for failure will cause\ + do_maintenance.py to exit the process with a non-zero exit\ + status. + + Raises: + NotImplementedError: The method must be implemented in subclass. + """ + # Abstract + raise NotImplementedError diff --git a/src/scaict_uwu/core/maintenance/parameters.py b/src/scaict_uwu/core/maintenance/parameters.py new file mode 100644 index 0000000..82993f0 --- /dev/null +++ b/src/scaict_uwu/core/maintenance/parameters.py @@ -0,0 +1,56 @@ +""" +Module parameters for class MaintenanceParameters. +""" + + +class MaintenanceParameters: + """ + Command line parameter handler for maintenance scripts. + """ + + __description: str + """ + __description (str): Short description of what the script does. + """ + + def has_description(self) -> bool: + """ + Check whether the script has description. + + Returns: + bool + """ + return self.__description != "" + + def get_description(self) -> str: + """ + Get the short description of what the script does. + + Returns: + str: The short description + """ + return self.__description + + def set_description(self, text: str) -> None: + """ + Set a short description of what the script does. + + Parameters: + text (str) + """ + self.__description = text + + def get_help(self) -> str: + """ + Get help text. + + Returns: + str + """ + output: list = [] + + # Description + if self.has_description(): + output.append("") + + return "".join(output) diff --git a/src/scaict_uwu/rdbms/__init__.py b/src/scaict_uwu/core/maintenance/scripts/__init__.py similarity index 100% rename from src/scaict_uwu/rdbms/__init__.py rename to src/scaict_uwu/core/maintenance/scripts/__init__.py diff --git a/src/scaict_uwu/core/maintenance/scripts/update.py b/src/scaict_uwu/core/maintenance/scripts/update.py new file mode 100644 index 0000000..12a6760 --- /dev/null +++ b/src/scaict_uwu/core/maintenance/scripts/update.py @@ -0,0 +1,22 @@ +""" +Run all updaters. + +This is used when the database schema is modified and we need to apply patches. +""" + +# Local imports +# scaict_uwu.maintenance.maintenance +from ..maintenance import Maintenance + + +class UpdateMaintenance(Maintenance): + """ + Maintenance script to run database schema updates. + """ + + def __init__(self) -> None: + super().__init__() + self.add_description("Database updater.") + + def execute(self) -> bool: + return True diff --git a/src/scaict_uwu/stats/__init__.py b/src/scaict_uwu/core/stats/__init__.py similarity index 100% rename from src/scaict_uwu/stats/__init__.py rename to src/scaict_uwu/core/stats/__init__.py diff --git a/src/scaict_uwu/system_message/__init__.py b/src/scaict_uwu/core/user/__init__.py similarity index 100% rename from src/scaict_uwu/system_message/__init__.py rename to src/scaict_uwu/core/user/__init__.py diff --git a/src/scaict_uwu/user/__init__.py b/src/scaict_uwu/core/utils/__init__.py similarity index 100% rename from src/scaict_uwu/user/__init__.py rename to src/scaict_uwu/core/utils/__init__.py diff --git a/src/scaict_uwu/user/cog/__init__.py b/src/scaict_uwu/core/website/__init__.py similarity index 100% rename from src/scaict_uwu/user/cog/__init__.py rename to src/scaict_uwu/core/website/__init__.py diff --git a/src/scaict_uwu/data/tables.json b/src/scaict_uwu/data/tables.json new file mode 100644 index 0000000..566b2de --- /dev/null +++ b/src/scaict_uwu/data/tables.json @@ -0,0 +1,427 @@ +[ + { + "name": "user", + "comment": "The user table contains basic user information, user properties, etc.", + "columns": [ + { + "name": "DCname", + "comment": "", + "type": "string", + "options": { + "length": 32, + "default": null + } + }, + { + "name": "uid", + "comment": "Primary key", + "type": "bigint", + "options": { + "notnull": true + } + }, + { + "name": "DCMail", + "comment": "", + "type": "string", + "options": { + "length": 320, + "default": null + } + }, + { + "name": "githubName", + "comment": "", + "type": "string", + "options": { + "length": 39, + "default": null + } + }, + { + "name": "githubMail", + "comment": "", + "type": "string", + "options": { + "length": 320, + "default": null + } + }, + { + "name": "loveuwu", + "comment": "", + "type": "tinyint", + "options": { + "length": 1, + "notnull": true, + "default": 0 + } + }, + { + "name": "point", + "comment": "", + "type": "integer", + "options": { + "notnull": true, + "default": 0 + } + }, + { + "name": "ticket", + "comment": "", + "type": "integer", + "options": { + "notnull": true, + "default": 1 + } + }, + { + "name": "charge_combo", + "comment": "", + "type": "integer", + "options": { + "notnull": true, + "default": 0 + } + }, + { + "name": "next_lottery", + "comment": "", + "type": "integer", + "options": { + "notnull": true, + "default": 0 + } + }, + { + "name": "last_charge", + "comment": "", + "type": "datetime", + "options": { + "notnull": true, + "default": "1970-01-01 00:00:00" + } + }, + { + "name": "last_comment", + "comment": "", + "type": "date", + "options": { + "notnull": true, + "default": "1970-01-01" + } + }, + { + "name": "today_comments", + "comment": "", + "type": "integer", + "options": { + "notnull": true, + "default": 0 + } + } + ], + "indexes": [], + "pk": [ + "uid" + ] + }, + { + "name": "comment_points", + "comment": "", + "columns": [ + { + "name": "seq", + "comment": "Primary key", + "type": "integer", + "options": { + "autoincrement": true, + "notnull": true + } + }, + { + "name": "uid", + "comment": "Foreign key", + "type": "bigint", + "options": { + "notnull": true + } + }, + { + "name": "times", + "comment": "", + "type": "integer", + "options": { + "notnull": true, + "default": 2 + } + }, + { + "name": "next_reward", + "comment": "", + "type": "integer", + "options": { + "notnull": true, + "default": 1 + } + } + ], + "indexes": [], + "pk": [ + "seq" + ] + }, + { + "name": "game", + "comment": "", + "columns": [ + { + "name": "seq", + "comment": "", + "type": "bigint", + "options": { + "notnull": true, + "default": 0 + } + }, + { + "name": "lastID", + "comment": "", + "type": "bigint", + "options": { + "default": 0 + } + }, + { + "name": "niceColor", + "comment": "", + "type": "string", + "options": { + "length": 3, + "notnull": true, + "default": "FFF" + } + }, + { + "name": "nicecolorround", + "comment": "", + "type": "integer", + "options": { + "default": null + } + }, + { + "name": "niceColorCount", + "comment": "", + "type": "bigint", + "options": { + "default": 0 + } + } + ], + "indexes": [], + "pk": [] + }, + { + "name": "gift", + "comment": "", + "columns": [ + { + "name": "btnID", + "comment": "Primary key", + "type": "bigint", + "options": { + "notnull": true + } + }, + { + "name": "type", + "comment": "", + "type": "enum", + "options": { + "enum": [ + "電電點", + "抽獎券" + ], + "default": null + } + }, + { + "name": "count", + "comment": "", + "type": "integer", + "options": { + "default": null + } + }, + { + "name": "recipient", + "comment": "", + "type": "string", + "options": { + "length": 32, + "default": null + } + }, + { + "name": "received", + "comment": "", + "type": "tinyint", + "options": { + "length": 1, + "default": 0 + } + }, + { + "name": "sender", + "comment": "", + "type": "string", + "options": { + "length": 32, + "default": "admin" + } + } + ], + "indexes": [], + "pk": [ + "btnID" + ] + }, + { + "name": "ctf_data", + "comment": "", + "columns": [ + { + "name": "id", + "comment": "Primary key", + "type": "bigint", + "options": { + "notnull": true + } + }, + { + "name": "flags", + "comment": "", + "type": "string", + "options": { + "length": 255, + "default": null + } + }, + { + "name": "score", + "comment": "", + "type": "integer", + "options": { + "default": null + } + }, + { + "name": "restrictions", + "comment": "", + "type": "string", + "options": { + "length": 255, + "default": null + } + }, + { + "name": "message_id", + "comment": "", + "type": "bigint", + "options": { + "default": null + } + }, + { + "name": "case_status", + "comment": "", + "type": "tinyint", + "options": { + "length": 1, + "default": null + } + }, + { + "name": "start_time", + "comment": "", + "type": "datetime", + "options": { + "default": null + } + }, + { + "name": "end_time", + "comment": "", + "type": "string", + "options": { + "length": 255, + "default": null + } + }, + { + "name": "title", + "comment": "", + "type": "string", + "options": { + "length": 255, + "default": null + } + }, + { + "name": "tried", + "comment": "", + "type": "integer", + "options": { + "default": null + } + } + ], + "indexes": [], + "pk": [ + "seq" + ] + }, + { + "name": "ctf_history", + "comment": "", + "columns": [ + { + "name": "data_id", + "comment": "Foreign key", + "type": "bigint", + "options": { + "default": null + } + }, + { + "name": "uid", + "comment": "", + "type": "bigint", + "options": { + "default": null + } + }, + { + "name": "count", + "comment": "", + "type": "integer", + "options": { + "default": null + } + }, + { + "name": "solved", + "comment": "", + "type": "tinyint", + "options": { + "length": 1, + "notnull": true, + "default": 0 + } + } + ], + "indexes": [], + "pk": [] + } +] diff --git a/src/scaict_uwu/data/tables_diagram_layout.json b/src/scaict_uwu/data/tables_diagram_layout.json new file mode 100644 index 0000000..a720fb0 --- /dev/null +++ b/src/scaict_uwu/data/tables_diagram_layout.json @@ -0,0 +1,38 @@ +[ + { + "title": "User", + "color": "#dafdd5", + "tables": [ + [ "user" ] + ] + }, + { + "title": "Comment points", + "color": "#eaf3ff", + "tables": [ + [ "comment_points" ] + ] + }, + { + "title": "Game", + "color": "#eaf3ff", + "tables": [ + [ "game" ] + ] + }, + { + "title": "Gift", + "color": "#eaf3ff", + "tables": [ + [ "gift" ] + ] + }, + { + "title": "CTF", + "color": "#fef6e7", + "tables": [ + [ "ctf_data" ], + [ "ctf_history" ] + ] + } +] diff --git a/src/scaict_uwu/i18n/course/zh-hant.json b/src/scaict_uwu/i18n/course/zh-hant.json index 0967ef4..5b2335b 100644 --- a/src/scaict_uwu/i18n/course/zh-hant.json +++ b/src/scaict_uwu/i18n/course/zh-hant.json @@ -1 +1,25 @@ -{} +{ + "@metadata": { + "authors": [ + "Each Chen", + "os24", + "Winston Sung" + ] + }, + "course-token-redeem-button-label": "請輸入課程代碼", + "course-token-modal-title": "請輸入課程代碼", + "course-token-modal-input-text-label": "請輸入課程代碼", + "course-token-modal-redeemed-role": "已領取{0}身分組", + "course-token-modal-redeemed-course-theme": "課程主題:{0}", + "course-token-modal-redeemed-user": "使用者:{0}", + "course-token-modal-redeemed-lecturer": "講師:{0}", + "course-token-modal-redeemed-course-time": "課程時間:{0}", + "course-token-modal-failed": "領取失敗", + "course-token-modal-failed-user": "使用者:{0}", + "course-token-modal-failed-hint": "請重新確認課程代碼", + "course-send-redeem-embed-command-desc": "傳送課程代碼兌換鈕", + "course-redeem-embed-enter-token": "點下方按鈕輸入token", + "course-redeem-embed-redeem-role": "領取課程身分組!", + "course-create-theme-course-command-desc": "新增主題課程", + "course-create-theme-course-created": "已將{0}新增至 JSON;主題:{1},講師:{2},時間:{3}" +} diff --git a/src/scaict_uwu/utils/__init__.py b/src/scaict_uwu/libs/__init__.py similarity index 100% rename from src/scaict_uwu/utils/__init__.py rename to src/scaict_uwu/libs/__init__.py diff --git a/src/scaict_uwu/website/__init__.py b/src/scaict_uwu/libs/errors/__init__.py similarity index 100% rename from src/scaict_uwu/website/__init__.py rename to src/scaict_uwu/libs/errors/__init__.py diff --git a/src/scaict_uwu/libs/language/__init__.py b/src/scaict_uwu/libs/language/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/scaict_uwu/libs/language/language_tag.py b/src/scaict_uwu/libs/language/language_tag.py new file mode 100644 index 0000000..602c996 --- /dev/null +++ b/src/scaict_uwu/libs/language/language_tag.py @@ -0,0 +1,77 @@ +""" +This is the module for the class for all languages. +""" + +from .language_utils import LanguageUtils + + +class LanguageTag: + """ + The LanguageTag class deals with language data. + + Note: + This class is designed to be instantiated through LanguageTagFactory\ + rather than directly. + + Example: + language_tag_factory = LanguageTagFactory() + tag = language_tag_factory.get("zh-Hant") + tag = language_tag_factory.get_by_discord_code("zh-TW") + """ + + _tag: str + """ + _tag (str): The BCP 47 language subtag of the LanguageTag object. + """ + + def __init__(self, tag: str, /) -> None: + """ + Parameters: + tag (str): BCP 47 language tag. + """ + + self._tag = tag + + def get_bcp_47_tag(self) -> str: + """ + Get the BCP 47 language tag of the LanguageTag object. + + Returns: + str: The BCP 47 language tag. + """ + + return self._tag + + def get_system_message_tag(self) -> str: + """ + Get the system message language tag of the LanguageTag object. + + Returns: + str: The system message language tag. + """ + + return self._tag.lower() + + def get_discord_code(self) -> str | None: + """ + Get the Discord locale code of the LanguageTag object. + + See + + Returns: + (str | None): The Discord locale code. Return None when there is no\ + corresponding supported Discord locale code. + """ + + return LanguageUtils.get_discord_code(self._tag) + + def get_fallbacks(self) -> list[str]: + """ + Get the language fallback chain of the LanguageTag object. + + Returns: + list[str]: The list containing BCP 47 language tags of the language\ + fallback chain. + """ + + return [] diff --git a/src/scaict_uwu/libs/language/language_tag_factory.py b/src/scaict_uwu/libs/language/language_tag_factory.py new file mode 100644 index 0000000..f2caec6 --- /dev/null +++ b/src/scaict_uwu/libs/language/language_tag_factory.py @@ -0,0 +1,90 @@ +""" +This is the module for creating the objects for all languages. +""" + +# Standard imports +from typing import ClassVar + +# Local imports +from .language_tag import LanguageTag +from .language_utils import LanguageUtils + + +class LanguageTagFactory: + """ + The LanguageTagFactory class deals with LanguageTag object creations. + """ + + _tags: ClassVar[dict[str, LanguageTag]] = {} + """ + _tags (dict): The LanguageTag objects. + """ + + def _get(self, tag: str, /) -> LanguageTag: + """ + Get LanguageTag object by normalized BCP 47 language tag. + + Parameters: + tag (str): Normalized BCP 47 language tag. + + Returns: + LanguageTag: The LanguageTag object to the corresponding BCP 47\ + language tag. + """ + + if tag not in self._tags: + self._tags[tag] = LanguageTag(tag) + + return self._tags[tag] + + def get(self, tag: str, /) -> LanguageTag: + """ + Get LanguageTag object by normalized BCP 47 language tag. + + Parameters: + tag (str): Normalized BCP 47 language tag. + + Returns: + LanguageTag: The LanguageTag object to the corresponding BCP 47\ + language tag. + """ + + tag = LanguageUtils.to_bcp_47_case(tag) + + return self._get(tag) + + def get_by_unnormalized(self, tag: str, /) -> LanguageTag: + """ + Get LanguageTag object by unnormalized BCP 47 language tag. + + Parameters: + tag (str): Unnormalized BCP 47 language tag. + + Returns: + LanguageTag: The LanguageTag object to the corresponding BCP 47\ + language tag. + """ + + tag = LanguageUtils.to_bcp_47(tag) + + return self._get(tag) + + def get_by_discord_code(self, code: str, /) -> LanguageTag | None: + """ + Get LanguageTag object by Discord locale code. + + Parameters: + code (str): Discord locale code. + + Returns: + (LanguageTag | None): The LanguageTag object of the corresponding\ + Discord locale code. Return None when is not a supported\ + Discord locale code. + """ + + if code not in LanguageUtils.get_supported_discord_codes(): + return None + + tag = LanguageUtils.get_from_discord_code(code) + + return self._get(tag) diff --git a/src/scaict_uwu/libs/language/language_utils.py b/src/scaict_uwu/libs/language/language_utils.py new file mode 100644 index 0000000..09a1580 --- /dev/null +++ b/src/scaict_uwu/libs/language/language_utils.py @@ -0,0 +1,256 @@ +""" +This is the module for the class for language utilities. +""" + + +class LanguageUtils: + """ + The LanguageUtils class deals with language utilities. + """ + + @staticmethod + def get_bcp_47_prefix_mapping() -> dict[str, str]: + """ + Get the mapping of unnormalized BCP 47 language tag prefix to\ + normalized BCP 47 language tag prefix. + + Returns: + dict[str, str]: The mapping of unnormalized BCP 47 language tag\ + prefix to normalized BCP 47 language tag prefix. + """ + + return { + "art-lojban": "jbo", + "en-GB-oed": "en-GB-oxendict", + "i-ami": "ami", + "i-bnn": "bnn", + "i-hak": "hak", + "i-klingon": "tlh", + "i-lux": "lb", + "i-navajo": "nv", + "i-pwn": "pwn", + "i-tao": "tao", + "i-tay": "tay", + "i-tsu": "tsu", + "no-bok": "nb", + "no-nyn": "nn", + "sgn-BE-FR": "sfb", + "sgn-BE-NL": "vgt", + "sgn-BR": "bzs", + "sgn-CH-DE": "sgg", + "sgn-CO": "csn", + "sgn-DE": "gsg", + "sgn-DK": "dsl", + "sgn-ES": "ssp", + "sgn-FR": "fsl", + "sgn-GB": "bfi", + "sgn-GR": "gss", + "sgn-IE": "isg", + "sgn-IT": "ise", + "sgn-JP": "jsl", + "sgn-MX": "mfs", + "sgn-NI": "ncs", + "sgn-NL": "dse", + "sgn-NO": "nsl", + "sgn-PT": "psr", + "sgn-SE": "swl", + "sgn-US": "ase", + "sgn-ZA": "sfs", + "zh-cmn": "cmn", + "zh-gan": "gan", + "zh-guoyu": "cmn", + "zh-hakka": "hak", + "zh-min-nan": "nan", + "zh-nan": "nan", + "zh-wuu": "wuu", + "zh-xiang": "hsn", + "zh-yue": "yue", + } + + @staticmethod + def get_supported_discord_codes() -> list[str]: + """ + Get the Discord locale codes supported by Discord. + + See Pycord discord.commands.core valid_locales (not public) + See + + Returns: + list[str]: The list of Discord locale codes supported by Discord. + """ + + return [ + "bg", + "cs", + "da", + "de", + "el", + "en-GB", + "en-US", + # 'es-419' was missing from Pycord + "es-419", + "es-ES", + "fi", + "fr", + "hi", + "hr", + "hu", + # 'id' was missing from Pycord + "id", + "it", + "ja", + "ko", + "lt", + "nl", + "no", + "pl", + "pt-BR", + "ro", + "ru", + "sv-SE", + "th", + "tr", + "uk", + "vi", + "zh-CN", + "zh-TW", + ] + + @staticmethod + def get_discord_code_mapping() -> dict[str, str]: + """ + Get the mapping of BCP 47 language tag to Discord locale code. + + Returns: + dict[str, str]: The mapping of BCP 47 language tag to Discord locale code. + """ + + return { + "en": "en-US", + "es": "es-ES", + "nb": "no", + "sv": "sv-SE", + "zh-Hans": "zh-CN", + "zh-Hant": "zh-TW", + } + + @classmethod + def get_discord_code_to_bcp_47_mapping(cls, /) -> dict: + """ + Get the mapping of Discord locale code to BCP 47 language tag. + + Returns: + dict: The mapping of Discord locale code to BCP 47 language tag. + """ + + return {v: k for k, v in cls.get_discord_code_mapping().items()} + + @staticmethod + def to_bcp_47_case(tag: str, /) -> str: + """ + Convert language tag string to BCP 47 letter case. + + * language subtag: all lowercase. For example, zh. + * script subtag: first letter uppercase. For example, Latn. + * region subtag: all uppercase. For example, TW. + * variant subtags: all lowercase. For example, wadegile. + + Parameters: + tag (str): BCP 47 language tag string. + + Returns: + str: BCP 47 language tag string with BCP 47 letter case. + """ + + tag_lower_subtags: list[str] = tag.lower().split("-") + tag_bcp_47_case_subtags: list[str] = [] + + for index, subtag in enumerate(tag_lower_subtags): + if index > 0 and tag_lower_subtags[index - 1] == "x": + # When the previous segment is x, it is a private subtag and + # should be lowercase + tag_bcp_47_case_subtags.append(subtag.lower()) + elif len(subtag) == 2 and index > 0: + # BCP 47 region subtag + tag_bcp_47_case_subtags.append(subtag.upper()) + elif len(subtag) == 4 and index > 0: + # BCP 47 script subtag + tag_bcp_47_case_subtags.append(subtag.title()) + else: + # Use lowercase for other cases + tag_bcp_47_case_subtags.append(subtag.lower()) + + return "-".join(tag_bcp_47_case_subtags) + + @classmethod + def to_bcp_47(cls, tag: str, /) -> str: + """ + Normalize language tag string to BCP 47 language tag string, with + letter case formatting and deprecated code replacements (for example, + zh-min-nan => nan). + + Parameters: + tag (str): Unnormalized BCP 47 language tag string. + + Returns: + str: Normalized BCP 47 language tag string. + """ + + tag = tag.lower() + + for k, v in cls.get_bcp_47_prefix_mapping().items(): + if tag.startswith(k): + tag = v + tag.removeprefix(k) + + break + + return cls.to_bcp_47_case(tag) + + @classmethod + def is_supported_discord_code(cls, code: str, /) -> bool: + """ + Check if the given code is a supported Discord locale code. + + Parameters: + code (str): Discord locale code. + + Returns: + bool: Whether the given code is a supported Discord locale code. + """ + + return code in cls.get_supported_discord_codes() + + @classmethod + def get_discord_code(cls, tag: str, /) -> str | None: + """ + Get the Discord locale code from BCP 47 language tag. + + Parameters: + tag (str): BCP 47 language tag. + + Returns: + (str | None): the Discord locale code of the BCP 47 language tag.\ + Return None when there's no corresponding supported Discord\ + locale code. + """ + + code = cls.get_discord_code_mapping().get(tag, tag) + + if cls.is_supported_discord_code(code): + return code + + return None + + @classmethod + def get_from_discord_code(cls, code: str, /) -> str: + """ + Get the BCP 47 language tag from Discord locale code. + + Parameters: + code (str): Discord locale code. + + Returns: + str: the BCP 47 language tag of the Discord locale code. + """ + + return cls.get_discord_code_to_bcp_47_mapping().get(code, code) diff --git a/src/scaict_uwu/libs/logging/__init__.py b/src/scaict_uwu/libs/logging/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/scaict_uwu/libs/rdbms/__init__.py b/src/scaict_uwu/libs/rdbms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/scaict_uwu/libs/system_message/__init__.py b/src/scaict_uwu/libs/system_message/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/scaict_uwu/libs/system_message/message.py b/src/scaict_uwu/libs/system_message/message.py new file mode 100644 index 0000000..4d372fd --- /dev/null +++ b/src/scaict_uwu/libs/system_message/message.py @@ -0,0 +1,62 @@ +""" +This is the module for the class for all system messages. +""" + +# Local imports +from ..language.language_tag import LanguageTag + + +class Message: + """ + The Message class deals with fetching and processing of system messages. + """ + + _use_lang: str | None = None + """ + _use_lang (str|None): The language tag of the language for the system\ + message to use. + """ + + _in_lang: str | None = None + """ + _in_lang (str|None): The language tag of the language that the system\ + message actually used. + """ + + def __init__( + self, + key: str, + params: list, + use_lang: LanguageTag | None, + ) -> None: + """ + Set the language tag of the language that the message expected to use. + + Parameters: + key (str): Message key. + params (list): Message parameters. + use_lang: (Language|None): Language to use (None: defaults to\ + current user language). + """ + + def set_lang(self, lang_tag: str) -> None: + """ + Set the language tag of the language that the message expected to use. + + Parameters: + lang_tag (str): The language tag of the language that the message\ + expected to use. + """ + + self._use_lang = lang_tag + + def get_lang(self) -> str: + """ + Get the final language tag of the language that the message used or\ + falls back to. + + Returns: + str: Description of return value. + """ + + return self._use_lang or self._in_lang or "" diff --git a/src/scaict_uwu/maintenance.py b/src/scaict_uwu/maintenance.py new file mode 100644 index 0000000..c61880f --- /dev/null +++ b/src/scaict_uwu/maintenance.py @@ -0,0 +1,5 @@ +""" +Entry point for all maintenance scripts. + +TODO: Complete this +""" diff --git a/src/scaict_uwu/maintenance/data/tables.json b/src/scaict_uwu/maintenance/data/tables.json deleted file mode 100644 index 4f5049f..0000000 --- a/src/scaict_uwu/maintenance/data/tables.json +++ /dev/null @@ -1,137 +0,0 @@ -[ - { - "name": "user", - "comment": "The user table contains basic user information, user properties, etc.", - "columns": [ - { - "name": "uid", - "comment": "Primary key", - "type": "bigint", - "options": { - "notnull": true - } - }, - { - "name": "loveuwu", - "comment": "", - "type": "tinyint", - "options": { - "length": 1, - "notnull": true, - "default": 0 - } - }, - { - "name": "point", - "comment": "", - "type": "integer", - "options": { - "notnull": true, - "default": 0 - } - }, - { - "name": "ticket", - "comment": "", - "type": "integer", - "options": { - "notnull": true, - "default": 0 - } - }, - { - "name": "charge_combo", - "comment": "", - "type": "integer", - "options": { - "notnull": true, - "default": 0 - } - }, - { - "name": "next_lottery", - "comment": "", - "type": "integer", - "options": { - "notnull": true, - "default": 0 - } - }, - { - "name": "last_charge", - "comment": "", - "type": "datetime", - "options": { - "notnull": true, - "default": "1970-01-01 00:00:00" - } - }, - { - "name": "last_comment", - "comment": "", - "type": "date", - "options": { - "notnull": true, - "default": "1970-01-01" - } - }, - { - "name": "today_comments", - "comment": "", - "type": "integer", - "options": { - "notnull": true, - "default": 0 - } - } - ], - "indexes": [], - "pk": [ - "uid" - ] - }, - { - "name": "comment_points", - "comment": "", - "columns": [ - { - "name": "seq", - "comment": "Primary key", - "type": "integer", - "options": { - "autoincrement": true - } - }, - { - "name": "uid", - "comment": "Foreign key", - "type": "bigint", - "options": { - "notnull": true - } - }, - { - "name": "times", - "comment": "", - "type": "integer", - "options": { - "notnull": true, - "default": 2 - } - }, - { - "name": "next_reward", - "comment": "", - "type": "integer", - "options": { - "notnull": true, - "default": 1 - } - } - ], - "indexes": [], - "pk": [ - "seq" - ] - } -] diff --git a/src/scaict_uwu/service_container.py b/src/scaict_uwu/service_container.py new file mode 100644 index 0000000..1ccccc8 --- /dev/null +++ b/src/scaict_uwu/service_container.py @@ -0,0 +1,99 @@ +""" +Module for SCAICT-uwu service locator. +""" + +# Standard imports +import sys +from typing import Callable + +# Local imports +from .core.config.config import Config +from .core.config.config_factory import ConfigFactory +from .libs.language.language_tag_factory import LanguageTagFactory + + +class ServiceContainer: + """ + Service locator for SCAICT-uwu core services. + + Refer to service_wiring.py for the default implementations. + """ + + _services: dict = {} + """ + """ + + _service_instantiators: dict = {} + """ + """ + + _services_being_created: dict = {} + """ + """ + + def load_wiring_module(self, module) -> None: + try: + self.apply_wiring(module.get_wiring().items()) + except AttributeError: + sys.exit("InvalidWiringModuleException") + + def load_wiring_modules(self, modules: list) -> None: + for module in modules: + self.load_wiring_module(module) + + def apply_wiring(self, service_instantiators) -> None: + for name, instantiator in service_instantiators: + self.define(name, instantiator) + + def get_names(self) -> list: + # Convert dict_keys to list + return list(self._service_instantiators.keys()) + + def has(self, name: str) -> bool: + return name in self._service_instantiators + + def define(self, name: str, instantiator: Callable) -> None: + if self.has(name): + sys.exit("ServiceAlreadyDefinedException $name") + + self._service_instantiators[name] = instantiator + + def redefine(self, name: str, instantiator: Callable) -> None: + if not self.has(name): + sys.exit("NoSuchServiceException $name") + + if name in self._services: + sys.exit("CannotReplaceActiveServiceException $name") + + self._service_instantiators[name] = instantiator + + def create(self, name: str): + if not self.has(name): + sys.exit("NoSuchServiceException $name") + + if name in self._services_being_created: + sys.exit( + "RecursiveServiceDependencyException " + + "Circular dependency when creating service!" + ) + + self._services_being_created[name] = True + + return self._service_instantiators[name](self) + + def get(self, name: str): + if name not in self._services: + self._services[name] = self.create(name) + + return self._services[name] + + # Service helper functions + + def get_config(self) -> Config: + return self.get("Config") + + def get_config_factory(self) -> ConfigFactory: + return self.get("ConfigFactory") + + def get_language_tag_factory(self) -> LanguageTagFactory: + return self.get("LanguageTagFactory") diff --git a/src/scaict_uwu/service_wiring.py b/src/scaict_uwu/service_wiring.py new file mode 100644 index 0000000..05a0304 --- /dev/null +++ b/src/scaict_uwu/service_wiring.py @@ -0,0 +1,40 @@ +""" +Module for SCAICT-uwu default service implementations. +""" + +# Standard imports +from typing import Callable + +# Local imports +from .core.config.config import Config +from .core.config.config_factory import ConfigFactory +from .libs.language.language_tag_factory import LanguageTagFactory +from .service_container import ServiceContainer + + +def get_wiring() -> dict[str, Callable]: + """ + Get the service instantiator functions. + + Returns: + dict: A mapping of service names to their instantiator functions. + Format: {"service_name": instantiator_function} + """ + + return { + "Config": get_config, + "ConfigFactory": get_config_factory, + "LanguageTagFactory": get_language_tag_factory, + } + + +def get_config(services: ServiceContainer) -> Config: + return services.get_config_factory().get() + + +def get_config_factory(services: ServiceContainer) -> ConfigFactory: + return ConfigFactory() + + +def get_language_tag_factory(services: ServiceContainer) -> LanguageTagFactory: + return LanguageTagFactory() diff --git a/src/scaict_uwu/website.py b/src/scaict_uwu/website.py new file mode 100644 index 0000000..6dd86bc --- /dev/null +++ b/src/scaict_uwu/website.py @@ -0,0 +1,9 @@ +""" +Entry point for the SCAICT store website using Flask run. +""" + + +def run() -> None: + """ + TODO: Complete this + """ diff --git a/static/switch-btn.js b/static/switch-btn.js index 1281f50..971f200 100644 --- a/static/switch-btn.js +++ b/static/switch-btn.js @@ -4,49 +4,56 @@ var switchButton = document.querySelector('.switch-button'); var switchBtnRight = document.querySelector('.switch-button-case.right'); var switchBtnLeft = document.querySelector('.switch-button-case.left'); var activeSwitch = document.querySelector('.active'); -var numDraws = document.getElementById('numDraws');//決定要抽幾抽的表單 -//抓轉蛋機外觀元素,用來調整轉蛋機的外觀 +// 決定要抽幾抽的表單 +var numDraws = document.getElementById('numDraws'); + +// 抓轉蛋機外觀元素,用來調整轉蛋機的外觀 var st3 = document.getElementsByClassName('st3'); var st4 = document.getElementsByClassName('st4'); var st5 = document.getElementsByClassName('st5'); var st6 = document.getElementsByClassName('st6'); -function changeMachine(color3,color4,color5,color6){ + +function changeMachine(color3, color4, color5, color6) { for (var i = 0; i < st3.length; i++) { st3[i].style.fill = color3; } + for (var i = 0; i < st4.length; i++) { st4[i].style.fill = color4; } + for (var i = 0; i < st5.length; i++) { st5[i].style.fill = color5; } + for (var i = 0; i < st6.length; i++) { st6[i].style.fill = color6; } } -function switchLeft(){//左邊,單抽 + +// 左邊,單抽 +function switchLeft() { switchBtnRight.classList.remove('active-case'); switchBtnLeft.classList.add('active-case'); - activeSwitch.style.left= '0%'; - numDraws.value=1; + activeSwitch.style.left = '0%'; + numDraws.value = 1; changeMachine('#1e90ff', '#00bfff', '#87cefa', '#4682b4'); } -function switchRight(){//右邊,10連 +// 右邊,10連 +function switchRight() { switchBtnRight.classList.add('active-case'); switchBtnLeft.classList.remove('active-case'); activeSwitch.style.left = '50%'; - numDraws.value=10; + numDraws.value = 10; changeMachine('#FF0000', '#FF4500', '#B22222', '#8B0000'); - achine('#FFD700', '#FFA500', '#FF6347', '#d76a1d'); } -switchBtnLeft.addEventListener('click', function(){ +switchBtnLeft.addEventListener('click', function() { switchLeft(); }, false); -switchBtnRight.addEventListener('click', function(){ +switchBtnRight.addEventListener('click', function() { switchRight(); }, false); - diff --git a/templates/home.html b/templates/home.html index 5882e08..fe8b5e6 100644 --- a/templates/home.html +++ b/templates/home.html @@ -156,7 +156,7 @@

${product.name}

}, body: JSON.stringify({ id: selected.id }), }) - //alert recieved text + //alert received text .then(res => res.text()) .then(text => { // if response is html then show it in new page diff --git a/test/api_request.py b/test/api_request.py new file mode 100644 index 0000000..d330a92 --- /dev/null +++ b/test/api_request.py @@ -0,0 +1,20 @@ +# Standard imports +import json +import os + +# Third-party imports +from dotenv import load_dotenv +import requests + +load_dotenv(f"{os.getcwd()}/.env", verbose=True, override=True) +guild_id = os.getenv("GUILD_ID") +api_key = os.getenv("DISCORD_TOKEN") +headers = { + "Authorization": f"Bot {api_key}", + "Content-Type": "application/json", +} +url = f"https://discord.com/api/v10/guilds/{guild_id}/members/898141506588770334" +response = requests.get(url, headers=headers, timeout=5) + +formatted_json = json.dumps(response.json(), indent=4) +print(formatted_json) diff --git a/test/enumstruct.py b/test/enumstruct.py new file mode 100644 index 0000000..24fe787 --- /dev/null +++ b/test/enumstruct.py @@ -0,0 +1,14 @@ +# enum 測試 +from enum import Enum + + +class GiftType(Enum): + point = "電電點" + ticket = "抽獎券" + + +for gt in GiftType: # equal to print(GiftType.{item}.name) + print(gt) +print(GiftType.point.name) + +print(GiftType.point.value) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pytest/__init__.py b/tests/pytest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pytest/test_service_wiring.py b/tests/pytest/test_service_wiring.py new file mode 100644 index 0000000..d1beae1 --- /dev/null +++ b/tests/pytest/test_service_wiring.py @@ -0,0 +1,17 @@ +""" +Unit test for using Pytest. +""" + +# Standard imports +from typing import Callable + +# Local imports +from scaict_uwu import service_wiring + + +def test_get_wiring() -> None: + service_instantiators = service_wiring.get_wiring() + + for name, instantiator in service_instantiators.items(): + assert isinstance(name, str) + assert isinstance(instantiator, Callable) diff --git a/tests/unittest/__init__.py b/tests/unittest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unittest/test_language_tag_factory.py b/tests/unittest/test_language_tag_factory.py new file mode 100644 index 0000000..0cbb927 --- /dev/null +++ b/tests/unittest/test_language_tag_factory.py @@ -0,0 +1,47 @@ +""" +Unit test for LanguageTagFactory using unittest. +""" + +# Standard imports +import unittest + +# Local imports +from scaict_uwu.libs.language.language_tag import LanguageTag +from scaict_uwu.libs.language.language_tag_factory import LanguageTagFactory + + +class TestLanguageTagFactory(unittest.TestCase): + language_tag_factory: LanguageTagFactory | None + + def setUp(self) -> None: + self.language_tag_factory = LanguageTagFactory() + + def tearDown(self) -> None: + self.language_tag_factory = None + + def test_get(self) -> None: + language_tag: LanguageTag = self.language_tag_factory.get("zh-Hant") + + self.assertEqual(language_tag.get_bcp_47_tag(), "zh-Hant") + self.assertEqual(language_tag.get_system_message_tag(), "zh-hant") + self.assertEqual(language_tag.get_discord_code(), "zh-TW") + + language_tag = self.language_tag_factory.get("zh-hant") + + self.assertEqual(language_tag.get_bcp_47_tag(), "zh-Hant") + self.assertEqual(language_tag.get_system_message_tag(), "zh-hant") + self.assertEqual(language_tag.get_discord_code(), "zh-TW") + + def test_get_by_discord_code(self) -> None: + language_tag: LanguageTag | None = ( + self.language_tag_factory.get_by_discord_code("zh-TW") + ) + + self.assertIsNotNone(language_tag) + self.assertEqual(language_tag.get_bcp_47_tag(), "zh-Hant") + self.assertEqual(language_tag.get_system_message_tag(), "zh-hant") + self.assertEqual(language_tag.get_discord_code(), "zh-TW") + + +if __name__ == "__main__": + unittest.main() diff --git a/token.json b/token.json deleted file mode 100644 index dcbd62d..0000000 --- a/token.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "discord_token": "", - "secret_key": "", - "discord_client_id": "", - "discord_client_secret": "", - "discord_redirect_uri": "", - "github_client_id": "", - "github_client_secret": "", - "github_redirect_uri": "", - "github_discord_redirect_uri": "", - "send_gift_role":"" -}