├── .dockerignore ├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── dockerbuild.yml │ └── reusable_docker.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── deno ├── counter.ts └── deno.json ├── doc ├── privacy-policy-zh_tw.md ├── privacy-policy.md ├── terms-of-service-zh_tw.md └── terms-of-service.md ├── docker-compose.yml ├── index.js ├── package.json ├── pic ├── demo1.png ├── demo14.png ├── demo20.png ├── demo21.png ├── demo22.png ├── demo23.png ├── demo24.png ├── demo25.png ├── demo3.png └── logo.svg ├── reload.js ├── src ├── bot.js ├── command │ ├── commandManager.js │ ├── delMsgCommand.js │ ├── morePicCommand.js │ ├── morePicCommandV2.js │ ├── pagePicCommand.js │ ├── theAPicCommand.js │ ├── theBPicCommand.js │ ├── theNPicCommand.js │ └── theZPicCommand.js ├── events │ ├── backupLinkSender.js │ ├── embedSuppresser.js │ ├── messageSender.js │ ├── messageSenderMore.js │ ├── messageSenderPixiv.js │ ├── messageSubSender.js │ ├── typingSender.js │ └── videoLinkSender.js ├── index.js ├── regex │ ├── handleBahaRegex.js │ ├── handleBilibiliRegex.js │ ├── handleBlueskyRegex.js │ ├── handleEhRegex.js │ ├── handleInstagramRegex.js │ ├── handleInstagramRegexV2.js │ ├── handleMisskeyRegex.js │ ├── handleNhRegex.js │ ├── handlePchomeRegex.js │ ├── handlePixivRegex.js │ ├── handlePlurkRegex.js │ ├── handlePlurkRegexV2.js │ ├── handlePttRegex.js │ ├── handleThreadsRegex.js │ ├── handleTiktokRegex.js │ ├── handleTiktokRegexV2.js │ ├── handleTwitterRegex.js │ ├── handleTwitterRegexV2.js │ ├── handleWeiboRegex.js │ └── regexManager.js ├── reload.js └── utils │ ├── botLog.js │ ├── configManager.js │ ├── currentTime.js │ ├── refreshContextMenus.js │ ├── reloadBahaTK.js │ └── runCronJob.js └── workers ├── ermiana-count.js ├── pixiv-proxy.js └── weibo-proxy.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | doc/ 3 | pic/ 4 | .git 5 | .gitignore 6 | .env 7 | Dockerfile 8 | docker-compose* 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DCTK="Discord Bot token" 2 | DCID="Discord Bot client ID" 3 | DCWH="Discord Private Webhook URL" 4 | BHUD="Baha id" 5 | BHPD="Baha secret" 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": "google", 7 | "parserOptions": { 8 | "ecmaVersion": "latest", 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "linebreak-style": 0, 13 | "require-jsdoc": 0, 14 | "max-len": 0, 15 | "object-curly-spacing": ["error", "always"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/dockerbuild.yml: -------------------------------------------------------------------------------- 1 | name: DockerBuild 2 | 3 | on: 4 | push: 5 | paths: 6 | - ".github/**" 7 | - "src/**" 8 | - "workers/**" 9 | - "**/*.js" 10 | - "package.json" 11 | workflow_dispatch: {} 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | jobs: 22 | DockerBuild: 23 | uses: ./.github/workflows/reusable_docker.yml 24 | -------------------------------------------------------------------------------- /.github/workflows/reusable_docker.yml: -------------------------------------------------------------------------------- 1 | name: Reusable Docker 2 | 3 | on: 4 | workflow_call: {} 5 | 6 | env: 7 | GHCR_NAME: ghcr.io/canaria3406/ermiana 8 | 9 | jobs: 10 | builder: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checking Repository 15 | uses: actions/checkout@v4 16 | with: 17 | submodules: true 18 | 19 | - name: Setup multiarch 20 | uses: docker/setup-qemu-action@v3 21 | 22 | - name: Setup Buildx 23 | uses: docker/setup-buildx-action@v3 24 | 25 | - name: Login ghcr.io 26 | uses: docker/login-action@v3 27 | with: 28 | registry: ghcr.io 29 | username: ${{ github.repository_owner }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Docker meta 33 | id: meta 34 | uses: docker/metadata-action@v5 35 | with: 36 | images: | 37 | ${{ env.GHCR_NAME }} 38 | tags: | 39 | type=semver,pattern={{version}} 40 | type=ref,event=branch 41 | 42 | - name: Build & Push 43 | uses: docker/build-push-action@v5 44 | with: 45 | context: . 46 | platforms: linux/amd64,linux/arm64 47 | push: true 48 | tags: ${{ steps.meta.outputs.tags }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | .env -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | CMD ["npm", "run", "start"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 canaria3406 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | ermiana 4 |

5 | 6 |

7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 | A Discord bot that fixes sites with broken preview by providing more detailed images and webpage content. Supports multiple popular sites in Taiwan, East Asia. 15 | 16 | ![demo](pic/demo20.png) 17 | 18 | ## Invite BOT 19 | 20 | [discord.com/application-directory/1078919650764652594](https://discord.com/application-directory/1078919650764652594) 21 | 22 | ## Policies 23 | 24 | - [Privacy Policy](doc/privacy-policy.md) 25 | - [Terms of Service](doc/terms-of-service.md) 26 | - [隱私權政策](doc/privacy-policy-zh_tw.md) 27 | - [服務條款](doc/terms-of-service-zh_tw.md) 28 | 29 | ## Screenshot 30 | 31 | ![demo](pic/demo21.png) 32 | 33 | ![demo](pic/demo22.png) 34 | 35 | ![demo](pic/demo25.png) 36 | 37 | ![demo](pic/demo3.png) 38 | 39 | ![demo](pic/demo24.png) 40 | 41 | ![demo](pic/demo23.png) 42 | 43 | ## Support 44 | 45 | - [x] Community 46 | - [x] PTT.cc 47 | - [x] 八卦板 48 | - [x] 希洽板 49 | - [x] 裏洽板 50 | - [x] 西斯板 51 | - [x] 政黑板 52 | - [x] 表特板 53 | - [x] JAV板 54 | - [x] HG板 55 | - [x] DMMG板 56 | - [x] 巴哈姆特電玩資訊站 57 | - [x] 場外休憩區 58 | - [x] Bilibili 專欄 59 | - [ ] Dcard 60 | - [x] Social media 61 | - [x] Plurk 62 | - [x] Twitter 63 | - [x] Misskey 64 | - [x] Bluesky 65 | - [ ] Weibo 66 | - [x] instagram 67 | - [x] tiktok 68 | - [ ] threads 69 | - [x] Image sharing service 70 | - [x] Pixiv 71 | - [x] ehentai 72 | - [x] exhentai 73 | - [ ] nhentai 74 | - [x] E-commerce site 75 | - [x] PChome24h 76 | -------------------------------------------------------------------------------- /deno/counter.ts: -------------------------------------------------------------------------------- 1 | const botID = '1078919650764652594'; 2 | 3 | async function updateKV() { 4 | const kv = await Deno.openKv(); 5 | const response = await fetch("https://discord.com/api/v9/application-directory-static/applications/" + botID, { 6 | headers: { 7 | 'Referer': 'https://discord.com/application-directory/' + botID, 8 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0', 9 | } 10 | }); 11 | if (response.ok) { 12 | const responseData = await response.json(); 13 | await kv.set(["name"], responseData.name); 14 | await kv.set(["count"], responseData.directory_entry.guild_count.toString()); 15 | } 16 | kv.close(); 17 | } 18 | 19 | Deno.cron('update','0 0 * * *', async () => { 20 | console.log("Running cron job to update KV..."); 21 | await updateKV(); 22 | }); 23 | 24 | Deno.serve(async () => { 25 | const kv = await Deno.openKv(); 26 | const name = await kv.get(["name"]) || "Deno"; 27 | const count = await kv.get(["count"]) || "0"; 28 | kv.close(); 29 | const jsonData = { 30 | schemaVersion: 1, 31 | label: name.value, 32 | message: count.value + ' servers', 33 | color: '7289DA', 34 | }; 35 | return new Response(JSON.stringify(jsonData), { 36 | headers: { "content-type": "application/json" }, 37 | }); 38 | }); -------------------------------------------------------------------------------- /deno/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "start": "deno run --allow-net --unstable-kv --unstable-cron counter.ts" 4 | }, 5 | "deploy": { 6 | "project": "ermiana", 7 | "entrypoint": "counter.ts" 8 | } 9 | } -------------------------------------------------------------------------------- /doc/privacy-policy-zh_tw.md: -------------------------------------------------------------------------------- 1 | # 隱私權政策 2 | 生效日期:2023年11月1日 3 | 更新日期:2024年4月14日 4 | 5 | ## 蒐集的資料 6 | ermiana(以下稱"本服務")經過您或您所屬伺服器管理員的允許,得以讀取您在 Discord 伺服器中發送的訊息內容進行網頁預覽解析,但這些訊息的任何內容都不會被儲存。本服務永遠不會儲存任何屬於您的使用者資料,包括但不限於您的大頭貼、個人簡介、傳送的訊息和圖片。請放心,您的所有個人資料將始終受到 Discord 官方的保護。 7 | 8 | ## 資料刪除 9 | 由於本服務不會儲存任何使用者資料,因此無法刪除任何使用者資料。本服務所有的原始碼都是公開的,任何人都可以逕自檢視。若您對此仍有任何疑慮,請以書面方式將您的問題描述,附上您的真實姓名和 Discord 帳號 ID,發送至 admin#canaria.cc(將 # 替換為 @),我們將盡快為您解答。 10 | -------------------------------------------------------------------------------- /doc/privacy-policy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | Effective: November 1, 2023 3 | Last Updated: April 14, 2024 4 | 5 | ## Data Collected 6 | With the consent of you and your server administrator, Ermiana may read the content of messages sent by you in Discord servers for webpage preview parsing, but none of this message content will be stored. Ermiana will NEVER STORE any of your user data, including but not limited to your avatar, description, messages, and pictures sent. Rest assured that your personal data will always be kept safe and protected by Discord's high-quality service. 7 | 8 | ## Data Removal 9 | Ermiana DOES NOT STORE any data, so nothing can be deleted. All the source code is public and can be reviewed by anyone. If you still have any questions, you can send a written description of your questions, including your name and your Discord account ID, to admin#canaria.cc (replace # with @). 10 | -------------------------------------------------------------------------------- /doc/terms-of-service-zh_tw.md: -------------------------------------------------------------------------------- 1 | # 服務條款 2 | 生效日期:2023年11月1日 3 | 更新日期:2024年4月21日 4 | 5 | ## 我們所提供的服務 6 | ermiana(以下稱"本服務")是一個能夠自動解析並顯示網頁預覽及圖片的 Discord BOT,支援 PTT、巴哈場外、噗浪、twitter、pixiv 等多個網站。 7 | 8 | ## 使用本服務 9 | 本服務經過您及您所屬伺服器管理員的允許,得以讀取您在 Discord 伺服器中發送的訊息內容進行網頁預覽解析,但這些訊息的任何內容都不會被儲存。本服務永遠不會儲存任何屬於您的使用者資料,包括但不限於您的大頭貼、個人簡介、傳送的訊息和圖片。當伺服器管理員新增本服務至伺服器時,應當詳閱並同意本服務條款,並盡到通知伺服器成員的義務。若伺服器管理員不同意本服務條款,應立即停止使用本服務,並將本服務自伺服器移除。當伺服器成員使用本服務時,應當詳閱並同意本服務條款。若伺服器成員不同意本服務條款,應立即停止使用本服務,並退出包含本服務的所有伺服器。 10 | 11 | ## 責任歸屬 12 | 本服務所傳送的所有網頁預覽及圖片,均引用自使用者發送的訊息。當您使用本服務時,您了解、同意並知悉所有言論及圖片均引用自使用者發送的訊息,並由該網頁的原作者發表,與本服務完全無關。您了解、同意並知悉本服務僅提供網頁預覽及圖片的自動解析,且為確保匿名性和隱私性,不進行資料儲存或預先判斷等操作,因此無法判斷訊息的適切性、合法性、正確性和公正性。您了解、同意並知悉本服務不贊同或反對任何使用者言論,所有言論的管理責任應由伺服器管理員或其授權的成員承擔。您了解、同意並知悉所有言論的責任均由發送訊息的使用者及網頁的原作者完全負責。 13 | 14 | ## 爭議解決 15 | 大多數的爭議可以通過非正式途徑解決,倘若您對本服務有任何問題,您同意在提起訴訟或仲裁之前先與我們聯繫。您可以隨時以書面方式將您的問題描述、您希望的解決方法,附上您的真實姓名和 Discord 帳號 ID,發送至 admin#canaria.cc(將 # 替換為 @),我們收到後將盡快為您解決。若在充分溝通後仍有未解決的問題需要進行訴訟或仲裁,爭議雙方同意以中華民國法律作為準據法,並以臺灣臺北地方法院作為第一審管轄法院。 16 | 17 | ## 軟體授權 18 | 本服務是以 MIT 授權條款在 GitHub 上發布的開放原始碼專案。您可以在 https://github.com/canaria3406/ermiana 查看本專案的所有原始碼。若您根據 MIT 授權條款進行包括但不限於複製、修改、合併、發布、分發等操作時,您需要於所有衍生簡介中提及本服務,並附上專案的 GitHub 網址 ( https://github.com/canaria3406/ermiana )。 19 | 20 | ## 漏洞揭露 21 | 若要通報任何軟體漏洞,請至 https://github.com/canaria3406/ermiana/discussions 。 22 | 23 | ## 問題釋疑 24 | 若您對本條款有任何疑問,請以書面方式將您的問題描述,附上您的真實姓名和 Discord 帳號 ID,發送至 admin#canaria.cc(將 # 替換為 @),我們將盡快為您解答。 25 | -------------------------------------------------------------------------------- /doc/terms-of-service.md: -------------------------------------------------------------------------------- 1 | # Terms of Service 2 | Effective: November 1, 2023 3 | Last Updated: April 14, 2024 4 | 5 | ## What You Can Expect from Us 6 | Ermiana is a Discord bot that fixes sites with broken preview by providing more detailed images and webpage content. Supports multiple popular sites in Taiwan, including ptt.cc, gamer.com.tw, e-hentai, Plurk, Pixiv, and Twitter. 7 | 8 | ## Using the Service 9 | With the consent of you and your server administrator, Ermiana may read the content of messages sent by you in Discord servers for webpage preview parsing, but none of this message content will be stored. Ermiana will NEVER STORE any of your user data, which includes but is not limited to your avatar, description, messages, and pictures sent. When a server administrator adds Ermiana to a server, they should read and agree to the terms of service, and fulfill the obligation to notify server members. If a server administrator does not agree to the terms of service, they should immediately cease using Ermiana and remove it from the server. When server members use Ermiana, they should read and agree to the terms of service. If a server member does not agree to the terms of service, they should immediately cease using Ermiana and leave all servers containing Ermiana. 10 | 11 | ## Responsibility 12 | All webpage previews and images transmitted by Ermiana are quoted from messages sent by users. When you use Ermiana, you understand and agree that all comments and images are made by the original authors of the webpages contained in the messages sent by users, and have nothing to do with Ermiana. Ermiana only provides automatic parsing of webpage previews and images, DOES NOT store any data, and DOES NOT eendorse or object to any comments. You understand and agree that the responsibility for all comments lies entirely with the users sending the messages and the original authors of the webpages. 13 | 14 | ## Settling Disputes 15 | Most disputes can be resolved informally, so if you have an issue with the services, you agree to reach out to us before initiating a lawsuit or arbitration. This requires sending @canaria3406 a written description of the dispute, including your name, the nature of your complaint, and how you'd like to resolve it, along with your Discord account ID, to admin#canaria.cc (replace # with @). If there are unresolved issues necessitating litigation, both parties agree that the governing law shall be the laws of Taiwan, and Taiwan Taipei District Court shall serve as the court of first instance. 16 | 17 | ## Software 18 | Ermiana is an open-source project released on GitHub under the MIT license. You can view the source code at https://github.com/canaria3406/ermiana. You can reproduce this code in any form, but you need to include the GitHub URL of ermiana's source code in the bot's description when publishing the bot on Discord. 19 | 20 | ## Bug Reporting 21 | We support the reporting of vulnerabilities. To report any issue, please visit https://github.com/canaria3406/ermiana/discussions. 22 | 23 | ## Contact 24 | If you have any questions about these terms, please visit https://github.com/canaria3406/ermiana/discussions, or contact us at admin#canaria.cc (replace # with @). 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ermiana: 3 | image: ghcr.io/canaria3406/ermiana:master 4 | container_name: ermiana 5 | environment: 6 | # Discord Bot Token 7 | - DCTK=ABCD123456789 8 | # Discord Bot Application ID 9 | - DCID=123456789 10 | # Discord Private Webhook URL 11 | - DCWH="https://discord.com/api/webhooks/123456789/ABCD123456789" 12 | # 巴哈帳號 13 | - BHUD=abc123 14 | # 巴哈密碼 15 | - BHPD=abc123 16 | # 時區 17 | - TZ=Asia/Taipei 18 | restart: always 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import './src/index.js'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ermiana", 3 | "version": "1.1.0", 4 | "description": "A discord bot that shows hidden description.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "lint": "eslint --fix src/*.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/canaria3406/ermiana.git" 13 | }, 14 | "type": "module", 15 | "author": "canaria3406", 16 | "license": "MIT", 17 | "homepage": "https://github.com/canaria3406/ermiana", 18 | "dependencies": { 19 | "@discordjs/rest": "^2.3.0", 20 | "@discordjs/ws": "^1.1.1", 21 | "axios": "^1.7.2", 22 | "cheerio": "^1.0.0", 23 | "conf": "^12.0.0", 24 | "cron": "^3.1.7", 25 | "discord.js": "^14.15.3", 26 | "dotenv": "^16.4.5" 27 | }, 28 | "devDependencies": { 29 | "eslint": "^8.52.0", 30 | "eslint-config-google": "^0.14.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pic/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canaria3406/ermiana/52f806fd64a88d7d491b821b36b5d36478628764/pic/demo1.png -------------------------------------------------------------------------------- /pic/demo14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canaria3406/ermiana/52f806fd64a88d7d491b821b36b5d36478628764/pic/demo14.png -------------------------------------------------------------------------------- /pic/demo20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canaria3406/ermiana/52f806fd64a88d7d491b821b36b5d36478628764/pic/demo20.png -------------------------------------------------------------------------------- /pic/demo21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canaria3406/ermiana/52f806fd64a88d7d491b821b36b5d36478628764/pic/demo21.png -------------------------------------------------------------------------------- /pic/demo22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canaria3406/ermiana/52f806fd64a88d7d491b821b36b5d36478628764/pic/demo22.png -------------------------------------------------------------------------------- /pic/demo23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canaria3406/ermiana/52f806fd64a88d7d491b821b36b5d36478628764/pic/demo23.png -------------------------------------------------------------------------------- /pic/demo24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canaria3406/ermiana/52f806fd64a88d7d491b821b36b5d36478628764/pic/demo24.png -------------------------------------------------------------------------------- /pic/demo25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canaria3406/ermiana/52f806fd64a88d7d491b821b36b5d36478628764/pic/demo25.png -------------------------------------------------------------------------------- /pic/demo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canaria3406/ermiana/52f806fd64a88d7d491b821b36b5d36478628764/pic/demo3.png -------------------------------------------------------------------------------- /pic/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 643 | 644 | -------------------------------------------------------------------------------- /reload.js: -------------------------------------------------------------------------------- 1 | import './src/reload.js'; 2 | -------------------------------------------------------------------------------- /src/bot.js: -------------------------------------------------------------------------------- 1 | import { PermissionsBitField, Client, GatewayIntentBits } from 'discord.js'; 2 | import { currentTime } from './utils/currentTime.js'; 3 | import { configManager } from './utils/configManager.js'; 4 | import { reloadLog, guildLog } from './utils/botLog.js'; 5 | import { msgCommandsMap, btnCommandsMap } from './command/commandManager.js'; 6 | import { regexsMap, matchRules } from './regex/regexManager.js'; 7 | 8 | const client = new Client({ 9 | intents: [ 10 | GatewayIntentBits.Guilds, 11 | GatewayIntentBits.GuildMessages, 12 | GatewayIntentBits.MessageContent, 13 | ], 14 | }); 15 | 16 | client.on('ready', async () => { 17 | console.log(`Ready! 以 ${client.user.tag} 身分登入`); 18 | currentTime(); 19 | try { 20 | await client.shard.broadcastEval((c) => c.readyAt !== null); 21 | const promises = [ 22 | client.shard.fetchClientValues('guilds.cache.size'), 23 | client.shard.broadcastEval((c) => c.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0)), 24 | ]; 25 | Promise.all(promises) 26 | .then((results) => { 27 | const serverCount = results[0].reduce((acc, guildCount) => acc + guildCount, 0); 28 | const totalUserCount = results[1].reduce((acc, memberCount) => acc + memberCount, 0); 29 | console.log(`正在 ${serverCount} 個伺服器上運作中`); 30 | console.log(`正在服務 ${totalUserCount} 位使用者`); 31 | reloadLog(serverCount, totalUserCount); 32 | }) 33 | .catch(console.error); 34 | } catch { 35 | return; 36 | } 37 | }); 38 | 39 | client.on('messageCreate', async (message) => { 40 | if (message.author.bot) return; 41 | if (/http/.test(message.content)) { 42 | for (const [regex, handler] of regexsMap) { 43 | if (regex.test(message.content)) { 44 | if (message.channel.permissionsFor(client.user).has([ 45 | PermissionsBitField.Flags.SendMessages, 46 | PermissionsBitField.Flags.EmbedLinks, 47 | ]) && !matchRules(message.content)) { 48 | const result = message.content.match(regex); 49 | const spoiler = (/\|\|[\s\S]*http[\s\S]*\|\|/).test(message.content) ? `||${result[0]}||` : ''; 50 | await handler(result, message, spoiler); 51 | break; 52 | } else { 53 | break; 54 | } 55 | } 56 | } 57 | } 58 | }); 59 | 60 | client.on('guildCreate', async (guild) => { 61 | guildLog(guild); 62 | }); 63 | 64 | client.on('interactionCreate', async (interaction) => { 65 | if (!interaction.isMessageContextMenuCommand() && !interaction.isButton()) return; 66 | if (interaction.isMessageContextMenuCommand()) { 67 | for (const [commandNames, handler] of msgCommandsMap) { 68 | if (interaction.commandName === commandNames) { 69 | await handler(interaction); 70 | break; 71 | } 72 | } 73 | } else if (interaction.isButton()) { 74 | for (const [commandNames, handler] of btnCommandsMap) { 75 | if (interaction.customId === commandNames) { 76 | await handler(interaction); 77 | break; 78 | } 79 | } 80 | } else { 81 | return; 82 | } 83 | }); 84 | 85 | const config = await configManager(); 86 | client.login(config.DCTK); 87 | -------------------------------------------------------------------------------- /src/command/commandManager.js: -------------------------------------------------------------------------------- 1 | import { delMsgCommand } from './delMsgCommand.js'; 2 | import { morePicCommand } from './morePicCommandV2.js'; 3 | import { pagePicCommand } from './pagePicCommand.js'; 4 | import { theAPicCommand } from './theAPicCommand.js'; 5 | import { theBPicCommand } from './theBPicCommand.js'; 6 | import { theNPicCommand } from './theNPicCommand.js'; 7 | import { theZPicCommand } from './theZPicCommand.js'; 8 | 9 | export const btnCommandsMap = new Map([ 10 | ['theAPicture', theAPicCommand], 11 | ['theBPicture', theBPicCommand], 12 | ['theNPicture', theNPicCommand], 13 | ['theZPicture', theZPicCommand], 14 | ['morePictureButton', morePicCommand], 15 | ['pagePicture', pagePicCommand], 16 | ]); 17 | 18 | export const msgCommandsMap = new Map([ 19 | ['removeMessage', delMsgCommand], 20 | ]); 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/command/delMsgCommand.js: -------------------------------------------------------------------------------- 1 | import { configManager } from '../utils/configManager.js'; 2 | 3 | export async function delMsgCommand(interaction) { 4 | const config = await configManager(); 5 | const locales = { 6 | sucess: { 7 | 'en-GB': 'Message deleted successfully.', 8 | 'en-US': 'Message deleted successfully.', 9 | 'zh-TW': '成功刪除訊息。', 10 | 'zh-CN': '成功删除信息。', 11 | 'ja': 'メッセージを削除しました。', 12 | }, 13 | fail: { 14 | 'en-GB': 'Failed to delete the message.', 15 | 'en-US': 'Failed to delete the message.', 16 | 'zh-TW': '刪除訊息時發生錯誤。', 17 | 'zh-CN': '删除信息时发生错误。', 18 | 'ja': 'メッセージの削除に失敗しました。', 19 | }, 20 | noPermission: { 21 | 'en-GB': 'I cannot delete this message. Please contact the server administrator to grant me the necessary permissions.', 22 | 'en-US': 'I cannot delete this message. Please contact the server administrator to grant me the necessary permissions.', 23 | 'zh-TW': '我無法刪除這個訊息,請聯絡伺服器管理員,並給我相關權限。', 24 | 'zh-CN': '我无法删除这个信息,请联系服务器管理员,并给我相关权限。', 25 | 'ja': 'このメッセージを削除できません。サーバー管理者に連絡して、関連する権限を与えてください。', 26 | }, 27 | notMyMessage: { 28 | 'en-GB': 'I can only delete messages that I have sent.', 29 | 'en-US': 'I can only delete messages that I have sent.', 30 | 'zh-TW': '我只能刪除由我自己發送的訊息喔。', 31 | 'zh-CN': '我只能删除由我自己发送的信息。', 32 | 'ja': '私は自分で送信したメッセージのみを削除できます。', 33 | }, 34 | }; 35 | try { 36 | if (interaction.targetMessage.author.id === config.DCID) { 37 | if (interaction.targetMessage.deletable) { 38 | try { 39 | await interaction.targetMessage.delete() 40 | .then(() => { 41 | if (interaction.targetMessage.reference?.messageId) { 42 | interaction.reply( { content: (locales.sucess[interaction.locale] ?? locales.sucess['en-US']) }); 43 | } else if (interaction.targetMessage.interaction?.id) { 44 | interaction.reply( { content: (locales.sucess[interaction.locale] ?? locales.sucess['en-US']) }); 45 | } else { 46 | interaction.reply( { content: (locales.sucess[interaction.locale] ?? locales.sucess['en-US']), ephemeral: true }); 47 | } 48 | }) 49 | .catch(() => { 50 | interaction.reply( { content: (locales.fail[interaction.locale] ?? locales.fail['en-US']), ephemeral: true }); 51 | }); 52 | } catch { 53 | interaction.reply( { content: (locales.fail[interaction.locale] ?? locales.fail['en-US']), ephemeral: true }); 54 | } 55 | } else { 56 | interaction.reply( { content: (locales.noPermission[interaction.locale] ?? locales.noPermission['en-US']), ephemeral: true }); 57 | } 58 | } else { 59 | interaction.reply( { content: (locales.notMyMessage[interaction.locale] ?? locales.notMyMessage['en-US']), ephemeral: true }); 60 | } 61 | } catch {} 62 | } 63 | -------------------------------------------------------------------------------- /src/command/morePicCommand.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | 3 | export async function morePicCommand(interaction) { 4 | try { 5 | const picArray = []; 6 | await interaction.message.components[0].components 7 | .filter((_button, index) => index > 0 && index < 4) 8 | .forEach((button) => { 9 | picArray.push(button.url); 10 | }); 11 | 12 | await interaction.message.edit({ 13 | components: [], 14 | }); 15 | 16 | if (picArray) { 17 | const embedColor = interaction.message.embeds[0].color || 0x0e2e47; 18 | 19 | picArray.forEach(async (image, index) => { 20 | const picEmbed = new EmbedBuilder(); 21 | picEmbed.setColor(embedColor); 22 | picEmbed.setImage(image); 23 | picEmbed.setFooter({ text: 'ermiana', iconURL: 'https://ermiana.canaria.cc/pic/canaria.png' }); 24 | 25 | if (index === 0) { 26 | await interaction.reply({ embeds: [picEmbed] }); 27 | await new Promise((resolve) => setTimeout(resolve, 500)); 28 | } else { 29 | await new Promise((resolve) => setTimeout(resolve, 200)); 30 | await interaction.message.channel.send({ embeds: [picEmbed] }); 31 | } 32 | }); 33 | 34 | await new Promise((resolve) => setTimeout(resolve, 1000)); 35 | await interaction.deferUpdate(); 36 | } else { 37 | await interaction.message.edit({ 38 | components: [], 39 | }); 40 | console.log('more pic error: '+ interaction.message.guild.name); 41 | await interaction.reply( { content: '解析網址發生問題。', ephemeral: true }); 42 | } 43 | } catch {} 44 | } 45 | -------------------------------------------------------------------------------- /src/command/morePicCommandV2.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, ButtonBuilder, ActionRowBuilder, ButtonStyle } from 'discord.js'; 2 | 3 | export async function morePicCommand(interaction) { 4 | try { 5 | let picNow = 5; 6 | const imageUrl = interaction.message.embeds[0].image.url; 7 | const picArray = []; 8 | await interaction.message.components[0].components 9 | .filter((_button, index) => index < 4) 10 | .forEach((button, index) => { 11 | if (button.url !== null) { 12 | picArray.push(button.url); 13 | } else if (button.url === null) { 14 | picArray.push(imageUrl); 15 | picNow = index; 16 | } 17 | }); 18 | 19 | const totalPage = picArray.length; 20 | const targetPage = (picNow + 1) >= totalPage ? 0 : (picNow + 1); 21 | 22 | if (picNow === 5) { 23 | await interaction.message.edit({ 24 | components: [], 25 | }); 26 | await interaction.reply( { content: '解析網址發生問題。', ephemeral: true }); 27 | return; 28 | } else if (totalPage === 1 || totalPage === 0 || totalPage > 4) { 29 | await interaction.message.edit({ 30 | components: [], 31 | }); 32 | await interaction.reply( { content: '解析網址發生問題。', ephemeral: true }); 33 | return; 34 | } 35 | 36 | const currentEmbed = interaction.message.embeds[0]; 37 | const targetEmbed = EmbedBuilder.from(currentEmbed).setImage(picArray[targetPage]); 38 | 39 | const button = new ButtonBuilder() 40 | .setCustomId('morePictureButton') 41 | .setLabel('更多圖片') 42 | .setStyle(ButtonStyle.Secondary); 43 | const row = new ActionRowBuilder(); 44 | 45 | picArray.forEach((link, index) => { 46 | if (index === targetPage) { 47 | row.addComponents(button); 48 | } else { 49 | const linkButton = new ButtonBuilder() 50 | .setLabel((index + 1).toString()) 51 | .setURL(link) 52 | .setStyle(ButtonStyle.Link) 53 | .setDisabled(true); 54 | row.addComponents(linkButton); 55 | } 56 | }); 57 | 58 | await interaction.message.edit({ 59 | components: [row], 60 | embeds: [targetEmbed], 61 | }); 62 | 63 | await new Promise((resolve) => setTimeout(resolve, 300)); 64 | await interaction.deferUpdate(); 65 | } catch { 66 | try { 67 | await interaction.message.edit({ 68 | components: [], 69 | }); 70 | console.log('more pic error: '+ interaction.message.guild.name); 71 | await interaction.reply( { content: '解析網址發生問題。', ephemeral: true }); 72 | } catch { 73 | console.log('more pic error: '+ interaction.message.guild.name); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/command/pagePicCommand.js: -------------------------------------------------------------------------------- 1 | export async function pagePicCommand(interaction) { 2 | try { 3 | await interaction.message.edit({ 4 | components: [], 5 | }); 6 | await interaction.reply( { content: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' }); 7 | console.log('button error with rickroll: '+ interaction.message.guild.name); 8 | } catch {} 9 | } 10 | -------------------------------------------------------------------------------- /src/command/theAPicCommand.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, ButtonBuilder, ActionRowBuilder, ButtonStyle } from 'discord.js'; 2 | 3 | export async function theAPicCommand(interaction) { 4 | try { 5 | const imageUrl = interaction.message.embeds[0].image.url; 6 | const page = interaction.message.components[0].components[2].label.match(/(\d+)\/(\d+)/); 7 | 8 | if (!imageUrl || !page) { 9 | await interaction.message.edit({ 10 | components: [], 11 | }); 12 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 13 | console.log('button error1: '+ interaction.message.guild.name); 14 | return; 15 | } 16 | 17 | const currentPage = parseInt(page[1]); 18 | const totalPage = parseInt(page[2]); 19 | const targetPage = 1; 20 | 21 | if (currentPage === 1) { 22 | await new Promise((resolve) => setTimeout(resolve, 300)); 23 | await interaction.deferUpdate(); 24 | return; 25 | } 26 | 27 | if (targetPage < 1 || targetPage > totalPage) { 28 | await interaction.message.edit({ 29 | components: [], 30 | }); 31 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 32 | console.log('button error2: '+ interaction.message.guild.name); 33 | return; 34 | } 35 | 36 | const match = imageUrl.match(/\d+_p(\d+)(?:\.|_)/); 37 | if (!match) { 38 | await interaction.message.edit({ 39 | components: [], 40 | }); 41 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 42 | console.log('button error3: '+ interaction.message.guild.name); 43 | return; 44 | } else if (parseInt(match[1]) !== currentPage -1) { 45 | await interaction.message.edit({ 46 | components: [], 47 | }); 48 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 49 | console.log('button error4: '+ interaction.message.guild.name); 50 | return; 51 | } else { 52 | const currentEmbed = interaction.message.embeds[0]; 53 | const targetEmbed = EmbedBuilder.from(currentEmbed).setImage(imageUrl.replace(`_p${currentPage - 1}`, `_p${targetPage - 1}`)); 54 | 55 | const currentComponents = interaction.message.components[0]; 56 | const buttonPage = new ButtonBuilder() 57 | .setCustomId('pagePicture') 58 | .setLabel(`${targetPage}/${totalPage}`) 59 | .setStyle(ButtonStyle.Secondary) 60 | .setDisabled(true); 61 | const targetComponents = new ActionRowBuilder() 62 | .addComponents(currentComponents.components[0], currentComponents.components[1], buttonPage, currentComponents.components[3], currentComponents.components[4]); 63 | 64 | await interaction.message.edit({ 65 | components: [targetComponents], 66 | embeds: [targetEmbed], 67 | }); 68 | 69 | await new Promise((resolve) => setTimeout(resolve, 300)); 70 | await interaction.deferUpdate(); 71 | } 72 | } catch {} 73 | } 74 | -------------------------------------------------------------------------------- /src/command/theBPicCommand.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, ButtonBuilder, ActionRowBuilder, ButtonStyle } from 'discord.js'; 2 | 3 | export async function theBPicCommand(interaction) { 4 | try { 5 | const imageUrl = interaction.message.embeds[0].image.url; 6 | const page = interaction.message.components[0].components[2].label.match(/(\d+)\/(\d+)/); 7 | 8 | if (!imageUrl || !page) { 9 | await interaction.message.edit({ 10 | components: [], 11 | }); 12 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 13 | console.log('button error1: '+ interaction.message.guild.name); 14 | return; 15 | } 16 | 17 | const currentPage = parseInt(page[1]); 18 | const totalPage = parseInt(page[2]); 19 | const targetPage = parseInt(currentPage) - 1; 20 | 21 | if (currentPage === 1) { 22 | await new Promise((resolve) => setTimeout(resolve, 300)); 23 | await interaction.deferUpdate(); 24 | return; 25 | } 26 | 27 | if (targetPage < 1 || targetPage > totalPage) { 28 | await interaction.message.edit({ 29 | components: [], 30 | }); 31 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 32 | console.log('button error2: '+ interaction.message.guild.name); 33 | return; 34 | } 35 | 36 | const match = imageUrl.match(/\d+_p(\d+)(?:\.|_)/); 37 | if (!match) { 38 | await interaction.message.edit({ 39 | components: [], 40 | }); 41 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 42 | console.log('button error3: '+ interaction.message.guild.name); 43 | return; 44 | } else if (parseInt(match[1]) !== currentPage -1) { 45 | await interaction.message.edit({ 46 | components: [], 47 | }); 48 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 49 | console.log('button error4: '+ interaction.message.guild.name); 50 | return; 51 | } else { 52 | const currentEmbed = interaction.message.embeds[0]; 53 | const targetEmbed = EmbedBuilder.from(currentEmbed).setImage(imageUrl.replace(`_p${currentPage - 1}`, `_p${targetPage - 1}`)); 54 | 55 | const currentComponents = interaction.message.components[0]; 56 | const buttonPage = new ButtonBuilder() 57 | .setCustomId('pagePicture') 58 | .setLabel(`${targetPage}/${totalPage}`) 59 | .setStyle(ButtonStyle.Secondary) 60 | .setDisabled(true); 61 | const targetComponents = new ActionRowBuilder() 62 | .addComponents(currentComponents.components[0], currentComponents.components[1], buttonPage, currentComponents.components[3], currentComponents.components[4]); 63 | 64 | await interaction.message.edit({ 65 | components: [targetComponents], 66 | embeds: [targetEmbed], 67 | }); 68 | 69 | await new Promise((resolve) => setTimeout(resolve, 300)); 70 | await interaction.deferUpdate(); 71 | } 72 | } catch {} 73 | } 74 | -------------------------------------------------------------------------------- /src/command/theNPicCommand.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, ButtonBuilder, ActionRowBuilder, ButtonStyle } from 'discord.js'; 2 | 3 | export async function theNPicCommand(interaction) { 4 | try { 5 | const imageUrl = interaction.message.embeds[0].image.url; 6 | const page = interaction.message.components[0].components[2].label.match(/(\d+)\/(\d+)/); 7 | 8 | if (!imageUrl || !page) { 9 | await interaction.message.edit({ 10 | components: [], 11 | }); 12 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 13 | console.log('button error1: '+ interaction.message.guild.name); 14 | return; 15 | } 16 | 17 | const currentPage = parseInt(page[1]); 18 | const totalPage = parseInt(page[2]); 19 | const targetPage = parseInt(currentPage) + 1; 20 | 21 | if (currentPage === totalPage) { 22 | await new Promise((resolve) => setTimeout(resolve, 300)); 23 | await interaction.deferUpdate(); 24 | return; 25 | } 26 | 27 | if (targetPage < 1 || targetPage > totalPage) { 28 | await interaction.message.edit({ 29 | components: [], 30 | }); 31 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 32 | console.log('button error2: '+ interaction.message.guild.name); 33 | return; 34 | } 35 | 36 | const match = imageUrl.match(/\d+_p(\d+)(?:\.|_)/); 37 | if (!match) { 38 | await interaction.message.edit({ 39 | components: [], 40 | }); 41 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 42 | console.log('button error3: '+ interaction.message.guild.name); 43 | return; 44 | } else if (parseInt(match[1]) !== currentPage -1) { 45 | await interaction.message.edit({ 46 | components: [], 47 | }); 48 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 49 | console.log('button error4: '+ interaction.message.guild.name); 50 | return; 51 | } else { 52 | const currentEmbed = interaction.message.embeds[0]; 53 | const targetEmbed = EmbedBuilder.from(currentEmbed).setImage(imageUrl.replace(`_p${currentPage - 1}`, `_p${targetPage - 1}`)); 54 | 55 | const currentComponents = interaction.message.components[0]; 56 | const buttonPage = new ButtonBuilder() 57 | .setCustomId('pagePicture') 58 | .setLabel(`${targetPage}/${totalPage}`) 59 | .setStyle(ButtonStyle.Secondary) 60 | .setDisabled(true); 61 | const targetComponents = new ActionRowBuilder() 62 | .addComponents(currentComponents.components[0], currentComponents.components[1], buttonPage, currentComponents.components[3], currentComponents.components[4]); 63 | 64 | await interaction.message.edit({ 65 | components: [targetComponents], 66 | embeds: [targetEmbed], 67 | }); 68 | 69 | await new Promise((resolve) => setTimeout(resolve, 300)); 70 | await interaction.deferUpdate(); 71 | } 72 | } catch {} 73 | } 74 | -------------------------------------------------------------------------------- /src/command/theZPicCommand.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, ButtonBuilder, ActionRowBuilder, ButtonStyle } from 'discord.js'; 2 | 3 | export async function theZPicCommand(interaction) { 4 | try { 5 | const imageUrl = interaction.message.embeds[0].image.url; 6 | const page = interaction.message.components[0].components[2].label.match(/(\d+)\/(\d+)/); 7 | 8 | if (!imageUrl || !page) { 9 | await interaction.message.edit({ 10 | components: [], 11 | }); 12 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 13 | console.log('button error1: '+ interaction.message.guild.name); 14 | return; 15 | } 16 | 17 | const currentPage = parseInt(page[1]); 18 | const totalPage = parseInt(page[2]); 19 | const targetPage = totalPage; 20 | 21 | if (currentPage === totalPage) { 22 | await new Promise((resolve) => setTimeout(resolve, 300)); 23 | await interaction.deferUpdate(); 24 | return; 25 | } 26 | 27 | if (targetPage < 1 || targetPage > totalPage) { 28 | await interaction.message.edit({ 29 | components: [], 30 | }); 31 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 32 | console.log('button error2: '+ interaction.message.guild.name); 33 | return; 34 | } 35 | 36 | const match = imageUrl.match(/\d+_p(\d+)(?:\.|_)/); 37 | if (!match) { 38 | await interaction.message.edit({ 39 | components: [], 40 | }); 41 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 42 | console.log('button error3: '+ interaction.message.guild.name); 43 | return; 44 | } else if (parseInt(match[1]) !== currentPage -1) { 45 | await interaction.message.edit({ 46 | components: [], 47 | }); 48 | await interaction.reply( { content: '取得圖片發生問題。', ephemeral: true }); 49 | console.log('button error4: '+ interaction.message.guild.name); 50 | return; 51 | } else { 52 | const currentEmbed = interaction.message.embeds[0]; 53 | const targetEmbed = EmbedBuilder.from(currentEmbed).setImage(imageUrl.replace(`_p${currentPage - 1}`, `_p${targetPage - 1}`)); 54 | 55 | const currentComponents = interaction.message.components[0]; 56 | const buttonPage = new ButtonBuilder() 57 | .setCustomId('pagePicture') 58 | .setLabel(`${targetPage}/${totalPage}`) 59 | .setStyle(ButtonStyle.Secondary) 60 | .setDisabled(true); 61 | const targetComponents = new ActionRowBuilder() 62 | .addComponents(currentComponents.components[0], currentComponents.components[1], buttonPage, currentComponents.components[3], currentComponents.components[4]); 63 | 64 | await interaction.message.edit({ 65 | components: [targetComponents], 66 | embeds: [targetEmbed], 67 | }); 68 | 69 | await new Promise((resolve) => setTimeout(resolve, 300)); 70 | await interaction.deferUpdate(); 71 | } 72 | } catch {} 73 | } 74 | -------------------------------------------------------------------------------- /src/events/backupLinkSender.js: -------------------------------------------------------------------------------- 1 | export async function backupLinkSender(message, spoiler, backupLink) { 2 | try { 3 | function spoilerCheck(backupLink) { 4 | if (spoiler) { 5 | return `||${backupLink}||`; 6 | } 7 | return backupLink; 8 | } 9 | 10 | await message.reply({ content: spoilerCheck(backupLink), allowedMentions: { repliedUser: false } }); 11 | } catch { 12 | // console.log('backupLink send error'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/events/embedSuppresser.js: -------------------------------------------------------------------------------- 1 | export async function embedSuppresser(message) { 2 | try { 3 | if (message.deletable) { 4 | await message.suppressEmbeds(true); 5 | } 6 | } catch { 7 | // console.log('no permission'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/events/messageSender.js: -------------------------------------------------------------------------------- 1 | export async function messageSender(message, spoiler, iconURL, embed, textinfo) { 2 | try { 3 | const textinfo2 = textinfo || 'ermiana'; 4 | const iconURL2 = iconURL || 'https://ermiana.canaria.cc/pic/canaria.png'; 5 | embed.setFooter({ text: textinfo2, iconURL: iconURL2 }); 6 | await message.reply({ content: spoiler, embeds: [embed], allowedMentions: { repliedUser: false } }); 7 | } catch { 8 | // console.log('message send error'); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/events/messageSenderMore.js: -------------------------------------------------------------------------------- 1 | import { ButtonBuilder, ActionRowBuilder, ButtonStyle } from 'discord.js'; 2 | 3 | export async function messageSenderMore(message, spoiler, iconURL, embed, textinfo, linkArray) { 4 | try { 5 | const textinfo2 = textinfo || 'ermiana'; 6 | const iconURL2 = iconURL || 'https://ermiana.canaria.cc/pic/canaria.png'; 7 | embed.setFooter({ text: textinfo2, iconURL: iconURL2 }); 8 | const button = new ButtonBuilder() 9 | .setCustomId('morePictureButton') 10 | .setLabel('更多圖片') 11 | .setStyle(ButtonStyle.Secondary); 12 | 13 | const row = new ActionRowBuilder() 14 | .addComponents(button); 15 | 16 | linkArray.forEach((link, index) => { 17 | const linkButton = new ButtonBuilder() 18 | .setLabel((index + 2).toString()) 19 | .setURL(link) 20 | .setStyle(ButtonStyle.Link) 21 | .setDisabled(true); 22 | row.addComponents(linkButton); 23 | }); 24 | 25 | await message.reply({ content: spoiler, embeds: [embed], components: [row], allowedMentions: { repliedUser: false } }); 26 | } catch { 27 | // console.log('button send error'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/events/messageSenderPixiv.js: -------------------------------------------------------------------------------- 1 | import { ButtonBuilder, ActionRowBuilder, ButtonStyle } from 'discord.js'; 2 | 3 | export async function messageSenderPixiv(message, spoiler, iconURL, embed, textinfo, pageCount) { 4 | try { 5 | const textinfo2 = textinfo || 'ermiana'; 6 | const iconURL2 = iconURL || 'https://ermiana.canaria.cc/pic/canaria.png'; 7 | embed.setFooter({ text: textinfo2, iconURL: iconURL2 }); 8 | const button1 = new ButtonBuilder() 9 | .setCustomId('theAPicture') 10 | .setLabel('<<') 11 | .setStyle(ButtonStyle.Secondary); 12 | const button2 = new ButtonBuilder() 13 | .setCustomId('theBPicture') 14 | .setLabel('<') 15 | .setStyle(ButtonStyle.Secondary); 16 | const buttonPage = new ButtonBuilder() 17 | .setCustomId('pagePicture') 18 | .setLabel(`1/${pageCount}`) 19 | .setStyle(ButtonStyle.Secondary) 20 | .setDisabled(true); 21 | const button3 = new ButtonBuilder() 22 | .setCustomId('theNPicture') 23 | .setLabel('>') 24 | .setStyle(ButtonStyle.Secondary); 25 | const button4 = new ButtonBuilder() 26 | .setCustomId('theZPicture') 27 | .setLabel('>>') 28 | .setStyle(ButtonStyle.Secondary); 29 | const row = new ActionRowBuilder() 30 | .addComponents(button1, button2, buttonPage, button3, button4); 31 | 32 | await message.reply({ content: spoiler, embeds: [embed], components: [row], allowedMentions: { repliedUser: false } }); 33 | } catch { 34 | // console.log('button send error'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/events/messageSubSender.js: -------------------------------------------------------------------------------- 1 | export async function messageSubSender(message, spoiler, iconURL, embed, textinfo) { 2 | try { 3 | const textinfo2 = textinfo || 'ermiana'; 4 | const iconURL2 = iconURL || 'https://ermiana.canaria.cc/pic/canaria.png'; 5 | embed.setFooter({ text: textinfo2, iconURL: iconURL2 }); 6 | await message.channel.send({ content: spoiler, embeds: [embed] }); 7 | } catch { 8 | // console.log('message send error'); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/events/typingSender.js: -------------------------------------------------------------------------------- 1 | export async function typingSender(message) { 2 | try { 3 | await message.channel.sendTyping(); 4 | } catch { 5 | // console.log('send typing error'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/events/videoLinkSender.js: -------------------------------------------------------------------------------- 1 | import { PermissionsBitField } from 'discord.js'; 2 | 3 | export async function videoLinkSender(message, spoiler, videoLink) { 4 | function spoilerCheck(videoLink) { 5 | if (spoiler) { 6 | return `||[連結](${videoLink})||`; 7 | } 8 | return `[連結](${videoLink})`; 9 | } 10 | 11 | try { 12 | if (videoLink.includes('ext_tw_video') || videoLink.includes('tweet_video')) { 13 | if (message.channel.permissionsFor(message.client.user).has([ 14 | PermissionsBitField.Flags.AttachFiles, 15 | ]) ) { 16 | if (!spoiler) { 17 | await message.channel.send({ files: [videoLink] }); 18 | } else { 19 | await message.channel.send(spoilerCheck(videoLink)); 20 | } 21 | } else { 22 | await message.channel.send(spoilerCheck(videoLink)); 23 | } 24 | } else { 25 | await message.channel.send(spoilerCheck(videoLink)); 26 | } 27 | } catch { 28 | try { 29 | await message.channel.send(spoilerCheck(videoLink)); 30 | return; 31 | } catch { 32 | // console.log('videoLink backup send error'); 33 | } 34 | } 35 | } 36 | 37 | /* 38 | export async function videoLinkSender(message, spoiler, videoLink) { 39 | try { 40 | await message.channel.send(`[連結](${videoLink})`); 41 | } catch { 42 | // console.log('videoLink send error'); 43 | } 44 | } 45 | */ 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { ShardingManager } from 'discord.js'; 2 | import { configManager } from './utils/configManager.js'; 3 | import { runCronJob } from './utils/runCronJob.js'; 4 | 5 | const config = await configManager(); 6 | const manager = new ShardingManager('./src/bot.js', { 7 | token: config.DCTK, 8 | totalShards: 'auto', 9 | }); 10 | 11 | manager.on('shardCreate', (shard) => console.log(`Launched shard ${shard.id}`)); 12 | manager.spawn(); 13 | runCronJob(); 14 | -------------------------------------------------------------------------------- /src/regex/handleBahaRegex.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import * as cheerio from 'cheerio'; 4 | import Conf from 'conf'; 5 | import { reloadBahaTK } from '../utils/reloadBahaTK.js'; 6 | import { messageSender } from '../events/messageSender.js'; 7 | import { embedSuppresser } from '../events/embedSuppresser.js'; 8 | import { typingSender } from '../events/typingSender.js'; 9 | 10 | export async function handleBahaRegex( result, message, spoiler ) { 11 | const iconURL = 'https://ermiana.canaria.cc/pic/baha.png'; 12 | typingSender(message); 13 | try { 14 | const ermianaBH = new Conf({ projectName: 'ermianaJS' }); 15 | if (!ermianaBH.get('BAHAENUR') || !ermianaBH.get('BAHARUNE')) { 16 | await reloadBahaTK(); 17 | } 18 | const BAHAENUR = ermianaBH.get('BAHAENUR'); 19 | const BAHARUNE = ermianaBH.get('BAHARUNE'); 20 | 21 | const bahaHTML = await axios.request({ 22 | url: 'https://forum.gamer.com.tw/' + result[1], 23 | method: 'get', 24 | headers: { Cookie: 'BAHAENUR=' + BAHAENUR + '; BAHARUNE=' + BAHARUNE + ';' }, 25 | timeout: 2500, 26 | }); 27 | 28 | const $ = cheerio.load(bahaHTML.data); 29 | 30 | const bahaEmbed = new EmbedBuilder(); 31 | bahaEmbed.setColor(1559500); 32 | bahaEmbed.setTitle($('meta[property=og:title]').attr('content')); 33 | bahaEmbed.setURL('https://forum.gamer.com.tw/' + result[1]); 34 | try { 35 | bahaEmbed.setDescription($('meta[property=og:description]').attr('content')); 36 | } catch {} 37 | try { 38 | bahaEmbed.setImage($('meta[property=og:image]').attr('content')); 39 | } catch {} 40 | 41 | messageSender(message, spoiler, iconURL, bahaEmbed, 'ermiana'); 42 | embedSuppresser(message); 43 | } catch { 44 | // console.log('baha error'); 45 | await reloadBahaTK(); 46 | try { 47 | const ermianaBH2 = new Conf({ projectName: 'ermianaJS' }); 48 | const BAHAENUR2 = ermianaBH2.get('BAHAENUR'); 49 | const BAHARUNE2 = ermianaBH2.get('BAHARUNE'); 50 | 51 | const bahaHTML2 = await axios.request({ 52 | url: 'https://forum.gamer.com.tw/' + result[1], 53 | method: 'get', 54 | headers: { Cookie: 'BAHAENUR=' + BAHAENUR2 + '; BAHARUNE=' + BAHARUNE2 + ';' }, 55 | timeout: 2500, 56 | }); 57 | 58 | const $ = cheerio.load(bahaHTML2.data); 59 | 60 | const bahaEmbed2 = new EmbedBuilder(); 61 | bahaEmbed2.setColor(1559500); 62 | bahaEmbed2.setTitle($('meta[property=og:title]').attr('content')); 63 | bahaEmbed2.setURL('https://forum.gamer.com.tw/' + result[1]); 64 | try { 65 | bahaEmbed2.setDescription($('meta[property=og:description]').attr('content')); 66 | } catch {} 67 | try { 68 | bahaEmbed2.setImage($('meta[property=og:image]').attr('content')); 69 | } catch {} 70 | 71 | messageSender(message, spoiler, iconURL, bahaEmbed2, 'ermiana'); 72 | embedSuppresser(message); 73 | } catch { 74 | console.log('baha error: '+ message.guild.name); 75 | // console.log('baha second try error'); 76 | } 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/regex/handleBilibiliRegex.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import { messageSender } from '../events/messageSender.js'; 4 | import { embedSuppresser } from '../events/embedSuppresser.js'; 5 | import { typingSender } from '../events/typingSender.js'; 6 | import { messageSenderMore } from '../events/messageSenderMore.js'; 7 | 8 | export async function handleBilibiliRegex( result, message, spoiler ) { 9 | const iconURL = 'https://ermiana.canaria.cc/pic/bilibili.png'; 10 | typingSender(message); 11 | try { 12 | const biliHTML = await axios.request({ 13 | url: 'https://api.bilibili.com/x/polymer/web-dynamic/v1/detail?id=' + result[1], 14 | method: 'get', 15 | headers: { 16 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 17 | 'Accept-Encoding': 'gzip, deflate, br, zstd', 18 | 'Accept-Language': 'zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3', 19 | 'Connection': 'keep-alive', 20 | 'Host': 'api.bilibili.com', 21 | 'Upgrade-Insecure-Requests': '1', 22 | 'TE': 'Trailers', 23 | 'Priority': 'u=0, i', 24 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0', 25 | }, 26 | timeout: 2500, 27 | }); 28 | 29 | if (biliHTML.status === 200) { 30 | const typeBilibili = biliHTML.data.data.item.type; 31 | if (typeBilibili === 'DYNAMIC_TYPE_DRAW') { 32 | const picBilibili = biliHTML.data.data.item.modules.module_dynamic.major?.draw ? biliHTML.data.data.item.modules.module_dynamic.major.draw?.items : []; 33 | const bilibiliEmbed = new EmbedBuilder(); 34 | bilibiliEmbed.setColor(0x00aeec); 35 | try { 36 | bilibiliEmbed.setAuthor({ 37 | name: biliHTML.data.data.item.modules.module_author.mid.toString(), 38 | iconURL: biliHTML.data.data.item.modules.module_author.face, 39 | }); 40 | } catch {} 41 | try { 42 | bilibiliEmbed.setTitle(biliHTML.data.data.item.modules.module_author.name.toString()); 43 | } catch {} 44 | bilibiliEmbed.setURL(result[0]); 45 | 46 | try { 47 | if (biliHTML.data.data.item.modules.module_dynamic?.desc) { 48 | bilibiliEmbed.setDescription(biliHTML.data.data.item.modules.module_dynamic.desc.text.toString()); 49 | } 50 | } catch {} 51 | 52 | try { 53 | if (picBilibili) { 54 | bilibiliEmbed.setImage(picBilibili[0].src); 55 | } 56 | } catch {} 57 | 58 | try { 59 | if (!picBilibili || picBilibili.length == 0) { 60 | messageSender(message, spoiler, iconURL, bilibiliEmbed, 'ermiana'); 61 | await new Promise((resolve) => setTimeout(resolve, 500)); 62 | embedSuppresser(message); 63 | } else if (picBilibili.length == 1) { 64 | messageSender(message, spoiler, iconURL, bilibiliEmbed, 'ermiana'); 65 | await new Promise((resolve) => setTimeout(resolve, 500)); 66 | embedSuppresser(message); 67 | } else if (picBilibili.length > 1) { 68 | const imageArray =[]; 69 | picBilibili.filter((_pic, index) => index > 0 && index < 4) 70 | .forEach((pic) => 71 | imageArray.push(pic.src), 72 | ); 73 | messageSenderMore(message, spoiler, iconURL, bilibiliEmbed, 'ermiana', imageArray); 74 | await new Promise((resolve) => setTimeout(resolve, 500)); 75 | embedSuppresser(message); 76 | } 77 | } catch {} 78 | } else if (typeBilibili === 'DYNAMIC_TYPE_ARTICLE') { 79 | const picBilibili = biliHTML.data.data.item.modules.module_dynamic.major.article?.covers ? biliHTML.data.data.item.modules.module_dynamic.major.article?.covers[0] : ''; 80 | const bilibiliEmbedA = new EmbedBuilder(); 81 | bilibiliEmbedA.setColor(0x00aeec); 82 | try { 83 | bilibiliEmbedA.setAuthor({ 84 | name: biliHTML.data.data.item.modules.module_author.mid.toString(), 85 | iconURL: biliHTML.data.data.item.modules.module_author.face, 86 | }); 87 | } catch {} 88 | try { 89 | bilibiliEmbedA.setTitle(biliHTML.data.data.item.modules.module_author.name.toString()); 90 | } catch {} 91 | bilibiliEmbedA.setURL(result[0]); 92 | try { 93 | if (biliHTML.data.data.item.modules.module_dynamic.major?.article) { 94 | if (biliHTML.data.data.item.modules.module_dynamic.major.article.title) { 95 | bilibiliEmbedA.setDescription(biliHTML.data.data.item.modules.module_dynamic.major.article.title.toString()); 96 | } 97 | } 98 | } catch {} 99 | try { 100 | if (picBilibili) { 101 | bilibiliEmbedA.setImage(picBilibili); 102 | } 103 | } catch {} 104 | messageSender(message, spoiler, iconURL, bilibiliEmbedA, 'ermiana'); 105 | await new Promise((resolve) => setTimeout(resolve, 500)); 106 | embedSuppresser(message); 107 | } else if (typeBilibili === 'DYNAMIC_TYPE_WORD') { 108 | const bilibiliEmbedW = new EmbedBuilder(); 109 | bilibiliEmbedW.setColor(0x00aeec); 110 | try { 111 | bilibiliEmbedW.setAuthor({ 112 | name: biliHTML.data.data.item.modules.module_author.mid.toString(), 113 | iconURL: biliHTML.data.data.item.modules.module_author.face, 114 | }); 115 | } catch {} 116 | try { 117 | bilibiliEmbedW.setTitle(biliHTML.data.data.item.modules.module_author.name.toString()); 118 | } catch {} 119 | bilibiliEmbedW.setURL(result[0]); 120 | try { 121 | if (biliHTML.data.data.item.modules.module_dynamic?.desc) { 122 | bilibiliEmbedW.setDescription(biliHTML.data.data.item.modules.module_dynamic.desc.text.toString().substring(0, 600)); 123 | } 124 | } catch {} 125 | messageSender(message, spoiler, iconURL, bilibiliEmbedW, 'ermiana'); 126 | await new Promise((resolve) => setTimeout(resolve, 500)); 127 | embedSuppresser(message); 128 | } else { 129 | return; 130 | } 131 | } else { 132 | return; 133 | } 134 | } catch { 135 | console.log('bilibili error: '+ message.guild.name); 136 | } 137 | }; 138 | -------------------------------------------------------------------------------- /src/regex/handleBlueskyRegex.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import { messageSender } from '../events/messageSender.js'; 4 | // import { messageSubSender } from '../events/messageSubSender.js'; 5 | import { embedSuppresser } from '../events/embedSuppresser.js'; 6 | import { backupLinkSender } from '../events/backupLinkSender.js'; 7 | import { typingSender } from '../events/typingSender.js'; 8 | import { messageSenderMore } from '../events/messageSenderMore.js'; 9 | 10 | export async function handleBlueskyRegex( result, message, spoiler ) { 11 | const iconURL = 'https://ermiana.canaria.cc/pic/bluesky.png'; 12 | typingSender(message); 13 | try { 14 | const didResp = await axios.request({ 15 | method: 'get', 16 | url: 'https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=' + result[1], 17 | timeout: 2000, 18 | }); 19 | 20 | // console.log(didResp.data.did); 21 | if (didResp.status === 200) { 22 | const threadResp = await axios.request({ 23 | method: 'get', 24 | url: 'https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=at://' + didResp.data.did + '/app.bsky.feed.post/' + result[2], 25 | timeout: 2000, 26 | }); 27 | 28 | // console.log(threadResp.data.thread); 29 | if (threadResp.status === 200) { 30 | const blueskyEmbed = new EmbedBuilder(); 31 | blueskyEmbed.setColor(0x53b4ff); 32 | try { 33 | if (threadResp.data.thread.post.author?.avatar) { 34 | blueskyEmbed.setAuthor({ name: '@' + result[1], iconURL: threadResp.data.thread.post.author.avatar }); 35 | } else { 36 | blueskyEmbed.setAuthor({ name: '@' + result[1] }); 37 | } 38 | } catch {} 39 | try { 40 | if (threadResp.data.thread.post.author?.displayName) { 41 | blueskyEmbed.setTitle(threadResp.data.thread.post.author.displayName); 42 | blueskyEmbed.setURL('https://bsky.app/profile/' + result[1] + '/post/' + result[2]); 43 | } else { 44 | blueskyEmbed.setTitle(result[1]); 45 | blueskyEmbed.setURL('https://bsky.app/profile/' + result[1] + '/post/' + result[2]); 46 | } 47 | } catch {} 48 | try { 49 | if (threadResp.data.thread.post.record.text) { 50 | blueskyEmbed.setDescription(threadResp.data.thread.post.record.text); 51 | } 52 | } catch {} 53 | try { 54 | if (threadResp.data.thread.post.embed?.images) { 55 | blueskyEmbed.setImage(threadResp.data.thread.post.embed.images[0].fullsize); 56 | } 57 | } catch {} 58 | 59 | const threadinfo ='💬' + threadResp.data.thread.post.replyCount.toString() + ' 🔁' + threadResp.data.thread.post.repostCount.toString() + ' ❤️' + threadResp.data.thread.post.likeCount.toString(); 60 | /* 61 | messageSender(message, spoiler, iconURL, blueskyEmbed, threadinfo); 62 | embedSuppresser(message); 63 | 64 | try { 65 | if (threadResp.data.thread.post.embed?.images.length > 1) { 66 | threadResp.data.thread.post.embed.images 67 | .filter((_image, index) => index > 0 && index < 4) 68 | .forEach((image) => { 69 | const picEmbed = new EmbedBuilder(); 70 | picEmbed.setColor(0x53b4ff); 71 | picEmbed.setImage(image.fullsize); 72 | messageSubSender(message, spoiler, iconURL, picEmbed, 'ermiana'); 73 | }); 74 | } 75 | } catch {} 76 | */ 77 | try { 78 | if (!threadResp.data.thread.post.embed?.images) { 79 | messageSender(message, spoiler, iconURL, blueskyEmbed, threadinfo); 80 | embedSuppresser(message); 81 | } else if (threadResp.data.thread.post.embed?.images.length == 1) { 82 | messageSender(message, spoiler, iconURL, blueskyEmbed, threadinfo); 83 | embedSuppresser(message); 84 | } else if (threadResp.data.thread.post.embed?.images.length > 1) { 85 | const imageArray =[]; 86 | threadResp.data.thread.post.embed.images 87 | .filter((_image, index) => index > 0 && index < 4) 88 | .forEach((image) => { 89 | imageArray.push(image.fullsize); 90 | }); 91 | messageSenderMore(message, spoiler, iconURL, blueskyEmbed, threadinfo, imageArray); 92 | embedSuppresser(message); 93 | } 94 | } catch {} 95 | } 96 | } 97 | } catch { 98 | try { 99 | backupLinkSender(message, spoiler, `https://bsyy.app/profile/${result[1]}/post/${result[2]}`); 100 | await new Promise((resolve) => setTimeout(resolve, 1500)); 101 | embedSuppresser(message); 102 | } catch { 103 | console.log('bluesky error: '+ message.guild.name); 104 | } 105 | } 106 | }; 107 | -------------------------------------------------------------------------------- /src/regex/handleEhRegex.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import { messageSender } from '../events/messageSender.js'; 4 | import { embedSuppresser } from '../events/embedSuppresser.js'; 5 | import { typingSender } from '../events/typingSender.js'; 6 | 7 | export async function handleEhRegex( result, message, spoiler ) { 8 | const iconURL = 'https://ermiana.canaria.cc/pic/eh.png'; 9 | typingSender(message); 10 | const galleryId = parseInt(result[1]); 11 | const galleryToken = result[2]; 12 | try { 13 | const waitingtime = ((5 - (Math.ceil(new Date().getTime() / 1000) % 5)) % 5) * 1000; 14 | await new Promise((resolve) => setTimeout(resolve, waitingtime)); 15 | 16 | const resp = await axios.request({ 17 | method: 'post', 18 | url: 'https://api.e-hentai.org/api.php', 19 | data: { 20 | method: 'gdata', 21 | gidlist: [[galleryId, galleryToken]], 22 | namespace: 1, 23 | }, 24 | timeout: 2500, 25 | }); 26 | 27 | // merge the tag with same keys 28 | const tagMap = new Map(); 29 | resp.data.gmetadata[0].tags.forEach((element) => { 30 | const tag = element.split(':'); 31 | const tagList = tagMap.get(tag[0]); 32 | if (tagList) { 33 | tagList.push(tag[1]); 34 | } else { 35 | tagMap.set(tag[0], [tag[1]]); 36 | } 37 | }); 38 | 39 | // translate the tag keys 40 | const translateTags = []; 41 | const tagReplaceList = new Map([ 42 | ['artist', '繪師'], 43 | ['character', '角色'], 44 | ['cosplayer', 'coser'], 45 | ['female', '女性'], 46 | ['group', '社團'], 47 | ['language', '語言'], 48 | ['male', '男性'], 49 | ['mixed', '混合'], 50 | ['other', '其他'], 51 | ['parody', '原作'], 52 | ['reclass', '重新分類'], 53 | ['temp', '臨時'], 54 | ]); 55 | tagMap.forEach((value, key) => { 56 | const values = value.join(', '); 57 | if (tagReplaceList.has(key)) { 58 | translateTags.push(tagReplaceList.get(key) + ': ' + values); 59 | } else { 60 | translateTags.push(key + ': ' + values); 61 | } 62 | }); 63 | 64 | const ehEmbed = new EmbedBuilder(); 65 | ehEmbed.setColor(16185594); 66 | ehEmbed.setTitle(resp.data.gmetadata[0].title); 67 | ehEmbed.setURL(result[0]); 68 | ehEmbed.addFields( 69 | { name: '類別', value: resp.data.gmetadata[0].category, inline: true }, 70 | { name: '評分', value: resp.data.gmetadata[0].rating, inline: true }, 71 | { name: '上傳者', value: resp.data.gmetadata[0].uploader, inline: true }, 72 | ); 73 | try { 74 | ehEmbed.addFields({ name: '標籤', value: translateTags.join('\n') }); 75 | } catch {} 76 | try { 77 | ehEmbed.setImage(resp.data.gmetadata[0].thumb); 78 | } catch {} 79 | 80 | messageSender(message, spoiler, iconURL, ehEmbed, 'ermiana'); 81 | embedSuppresser(message); 82 | } catch { 83 | // console.log('eh no response'); 84 | const waitingtime2 = ((5 - (Math.ceil(new Date().getTime() / 1000) % 5)) % 5) * 1000; 85 | await new Promise((resolve) => setTimeout(resolve, waitingtime2)); 86 | // console.log('eh sleep'); 87 | try { 88 | const resp = await axios.request({ 89 | method: 'post', 90 | url: 'https://api.e-hentai.org/api.php', 91 | data: { 92 | method: 'gdata', 93 | gidlist: [[galleryId, galleryToken]], 94 | namespace: 1, 95 | }, 96 | timeout: 2500, 97 | }); 98 | 99 | // merge the tag with same keys 100 | const tagMap = new Map(); 101 | resp.data.gmetadata[0].tags.forEach((element) => { 102 | const tag = element.split(':'); 103 | const tagList = tagMap.get(tag[0]); 104 | if (tagList) { 105 | tagList.push(tag[1]); 106 | } else { 107 | tagMap.set(tag[0], [tag[1]]); 108 | } 109 | }); 110 | 111 | // translate the tag keys 112 | const translateTags = []; 113 | const tagReplaceList = new Map([ 114 | ['artist', '繪師'], 115 | ['character', '角色'], 116 | ['cosplayer', 'coser'], 117 | ['female', '女性'], 118 | ['group', '社團'], 119 | ['language', '語言'], 120 | ['male', '男性'], 121 | ['mixed', '混合'], 122 | ['other', '其他'], 123 | ['parody', '原作'], 124 | ['reclass', '重新分類'], 125 | ['temp', '臨時'], 126 | ]); 127 | tagMap.forEach((value, key) => { 128 | const values = value.join(', '); 129 | if (tagReplaceList.has(key)) { 130 | translateTags.push(tagReplaceList.get(key) + ': ' + values); 131 | } else { 132 | translateTags.push(key + ': ' + values); 133 | } 134 | }); 135 | 136 | const ehEmbed = new EmbedBuilder(); 137 | ehEmbed.setColor(16185594); 138 | ehEmbed.setTitle(resp.data.gmetadata[0].title); 139 | ehEmbed.setURL(result[0]); 140 | ehEmbed.addFields( 141 | { name: '類別', value: resp.data.gmetadata[0].category, inline: true }, 142 | { name: '評分', value: resp.data.gmetadata[0].rating, inline: true }, 143 | { name: '上傳者', value: resp.data.gmetadata[0].uploader, inline: true }, 144 | ); 145 | try { 146 | ehEmbed.addFields({ name: '標籤', value: translateTags.join('\n') }); 147 | } catch {} 148 | try { 149 | ehEmbed.setImage(resp.data.gmetadata[0].thumb); 150 | } catch {} 151 | 152 | messageSender(message, spoiler, iconURL, ehEmbed, 'ermiana'); 153 | embedSuppresser(message); 154 | } catch { 155 | console.log('eh error: '+ message.guild.name); 156 | } 157 | } 158 | }; 159 | -------------------------------------------------------------------------------- /src/regex/handleInstagramRegex.js: -------------------------------------------------------------------------------- 1 | // import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | // import { messageSender } from '../events/messageSender.js'; 4 | // import { messageSubSender } from '../events/messageSubSender.js'; 5 | import { embedSuppresser } from '../events/embedSuppresser.js'; 6 | // import { videoLinkSender } from '../events/videoLinkSender.js'; 7 | import { backupLinkSender } from '../events/backupLinkSender.js'; 8 | import { typingSender } from '../events/typingSender.js'; 9 | 10 | export async function handleInstagramRegex( result, message, spoiler ) { 11 | // const igid = result[1]; 12 | // const iconURL = 'https://ermiana.canaria.cc/pic/instagram.png'; 13 | /* try { 14 | const igResp = await axios({ 15 | url: 'https://www.instagram.com/graphql/query/', 16 | method: 'get', 17 | headers: {*/ 18 | // 'Accept': '*/*', 19 | /* 'Accept-Language': 'en-US,en;q=0.9', 20 | 'Host': 'www.instagram.com', 21 | 'Origin': 'https://www.instagram.com', 22 | 'Connection': 'keep-alive', 23 | 'Sec-Fetch-Dest': 'empty', 24 | 'Sec-Fetch-Mode': 'navigate', 25 | 'Sec-Fetch-Site': 'same-origin', 26 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0', 27 | 'Referer': `https://www.instagram.com/p/${igid}/`, 28 | }, 29 | params: { 30 | query_hash: 'b3055c01b4b222b8a47dc12b090e4e64', 31 | variables: JSON.stringify({ shortcode: igid }), 32 | }, 33 | timeout: 3000, 34 | }); 35 | 36 | if (igResp.status === 200) { 37 | const igEmbed = new EmbedBuilder(); 38 | igEmbed.setColor(0xE1306C); 39 | try { 40 | if (igResp.data.data.shortcode_media.owner.username && igResp.data.data.shortcode_media.owner.profile_pic_url) { 41 | igEmbed.setAuthor({ name: '@' + igResp.data.data.shortcode_media.owner.username, iconURL: igResp.data.data.shortcode_media.owner.profile_pic_url }); 42 | } else if (igResp.data.data.shortcode_media.owner.username) { 43 | igEmbed.setAuthor({ name: '@' + igResp.data.data.shortcode_media.owner.username }); 44 | } 45 | } catch {} 46 | try { 47 | if (igResp.data.data.shortcode_media.owner.full_name) { 48 | igEmbed.setTitle(igResp.data.data.shortcode_media.owner.full_name); 49 | } else { 50 | igEmbed.setTitle('Instagram'); 51 | } 52 | } catch {} 53 | try { 54 | if (igResp.data.data.shortcode_media.shortcode) { 55 | igEmbed.setURL('https://www.instagram.com/p/' + igResp.data.data.shortcode_media.shortcode + '/'); 56 | } else { 57 | igEmbed.setURL('https://www.instagram.com/p/' + igid + '/'); 58 | } 59 | } catch {} 60 | try { 61 | if (igResp.data.data.shortcode_media.edge_media_to_caption.edges[0].node.text) { 62 | igEmbed.setDescription(igResp.data.data.shortcode_media.edge_media_to_caption.edges[0].node.text.substring(0, 300)); 63 | } 64 | } catch {} 65 | try { 66 | if (igResp.data.data.shortcode_media.display_url && !igResp.data.data.shortcode_media.is_video) { 67 | igEmbed.setImage(igResp.data.data.shortcode_media.display_url); 68 | } 69 | } catch {} 70 | const iginfo = '💬' + (igResp.data.data.shortcode_media.edge_media_to_comment.count?.toString() || '0') + ' ❤️' + (igResp.data.data.shortcode_media.edge_media_preview_like.count?.toString() || '0'); 71 | try { 72 | messageSender(message, spoiler, iconURL, igEmbed, iginfo); 73 | embedSuppresser(message); 74 | } catch {} 75 | try { 76 | if (igResp.data.data.shortcode_media.is_video) { 77 | videoLinkSender(message, spoiler, `https://d.ddinstagram.com/p/${igid}/`); 78 | } 79 | } catch {} 80 | 81 | try { 82 | if (igResp.data.data.shortcode_media.edge_sidecar_to_children.edges) { 83 | igResp.data.data.shortcode_media.edge_sidecar_to_children.edges 84 | .filter((_edge, index) => index > 0 && index < 4) 85 | .forEach((edge) => { 86 | if (!edge.node.is_video) { 87 | const picEmbed = new EmbedBuilder(); 88 | picEmbed.setColor(0xE1306C); 89 | picEmbed.setImage(edge.node.display_url); 90 | messageSubSender(message, spoiler, iconURL, picEmbed, 'ermiana'); 91 | } 92 | }); 93 | } 94 | } catch {} 95 | 96 | } else { 97 | throw new Error(); 98 | } 99 | } catch { 100 | */ 101 | try { 102 | const igHTML = await axios.request({ 103 | url: `https://www.ddinstagram.com/p/DD9yTv4SvWb/`, 104 | method: 'get', 105 | timeout: 2000, 106 | }); 107 | 108 | if (igHTML.status == 200) { 109 | await typingSender(message); 110 | backupLinkSender(message, spoiler, `https://www.ddinstagram.com/p/${result[1]}/`); 111 | await new Promise((resolve) => setTimeout(resolve, 1500)); 112 | embedSuppresser(message); 113 | } else { 114 | return; 115 | } 116 | } catch { 117 | // console.log('instagram error: '+ message.guild.name); 118 | return; 119 | } 120 | // } 121 | }; 122 | -------------------------------------------------------------------------------- /src/regex/handleInstagramRegexV2.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { embedSuppresser } from '../events/embedSuppresser.js'; 3 | import { backupLinkSender } from '../events/backupLinkSender.js'; 4 | import { typingSender } from '../events/typingSender.js'; 5 | 6 | export async function handleInstagramRegex( result, message, spoiler ) { 7 | try { 8 | const igHTML = await axios.request({ 9 | url: `https://www.ddinstagram.com/`, 10 | method: 'get', 11 | timeout: 4400, 12 | }); 13 | 14 | if (igHTML.status === 200) { 15 | await typingSender(message); 16 | backupLinkSender(message, spoiler, `https://www.ddinstagram.com/p/${result[1]}/`); 17 | await new Promise((resolve) => setTimeout(resolve, 1500)); 18 | embedSuppresser(message); 19 | } else { 20 | throw new Error(); 21 | } 22 | } catch { 23 | try { 24 | const igHTML2 = await axios.request({ 25 | url: `https://www.instagramez.com/p/${result[1]}/`, 26 | method: 'get', 27 | timeout: 2500, 28 | }); 29 | 30 | if (igHTML2.status === 200) { 31 | await typingSender(message); 32 | backupLinkSender(message, spoiler, `https://www.instagramez.com/p/${result[1]}/`); 33 | await new Promise((resolve) => setTimeout(resolve, 1500)); 34 | embedSuppresser(message); 35 | } else { 36 | throw new Error(); 37 | } 38 | } catch { 39 | return; 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/regex/handleMisskeyRegex.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import { messageSender } from '../events/messageSender.js'; 4 | // import { messageSubSender } from '../events/messageSubSender.js'; 5 | import { embedSuppresser } from '../events/embedSuppresser.js'; 6 | // import { videoLinkSender } from '../events/videoLinkSender.js'; 7 | import { typingSender } from '../events/typingSender.js'; 8 | import { messageSenderMore } from '../events/messageSenderMore.js'; 9 | 10 | export async function handleMisskeyRegex( result, message, spoiler ) { 11 | const iconURL = 'https://ermiana.canaria.cc/pic/misskey.png'; 12 | typingSender(message); 13 | try { 14 | const resp = await axios.request({ 15 | method: 'post', 16 | url: 'https://misskey.io/api/notes/show', 17 | data: { 18 | noteId: result[1], 19 | }, 20 | timeout: 2000, 21 | }); 22 | 23 | if (resp.status === 200) { 24 | const misskeyEmbed = new EmbedBuilder(); 25 | misskeyEmbed.setColor(0x96d04a); 26 | try { 27 | misskeyEmbed.setAuthor({ name: '@' + resp.data.user.username, iconURL: resp.data.user.avatarUrl }); 28 | } catch {} 29 | try { 30 | misskeyEmbed.setTitle(resp.data.user.name); 31 | } catch {} 32 | 33 | misskeyEmbed.setURL(result[0]); 34 | 35 | try { 36 | misskeyEmbed.setDescription(resp.data.text); 37 | } catch {} 38 | 39 | try { 40 | if (resp.data.files[0]?.type == 'image/webp' || 'image/png' || 'image/jpg') { 41 | misskeyEmbed.setImage(resp.data.files[0].url); 42 | } 43 | } catch {} 44 | 45 | function sumReactions(reactions) { 46 | return Object.values(reactions).reduce((total, count) => total + count, 0); 47 | } 48 | 49 | const noteinfo = '💬' + resp.data.repliesCount.toString() + ' 🔁' + resp.data.renoteCount.toString() + ' ❤️' + sumReactions(resp.data.reactions).toString(); 50 | try { 51 | if (!resp.data?.files || resp.data.files.length == 0) { 52 | messageSender(message, spoiler, iconURL, misskeyEmbed, noteinfo); 53 | await new Promise((resolve) => setTimeout(resolve, 500)); 54 | embedSuppresser(message); 55 | } else if (resp.data.files?.length == 1) { 56 | messageSender(message, spoiler, iconURL, misskeyEmbed, noteinfo); 57 | await new Promise((resolve) => setTimeout(resolve, 500)); 58 | embedSuppresser(message); 59 | } else if (resp.data.files?.length > 1) { 60 | const imageArray =[]; 61 | resp.data.files 62 | .filter((_file, index) => index > 0 && index < 4) 63 | .forEach((file) => { 64 | if (file.type == 'image/webp' || 'image/png' || 'image/jpg') { 65 | imageArray.push(file.url); 66 | } 67 | }); 68 | messageSenderMore(message, spoiler, iconURL, misskeyEmbed, noteinfo, imageArray); 69 | await new Promise((resolve) => setTimeout(resolve, 500)); 70 | embedSuppresser(message); 71 | } 72 | } catch {} 73 | /* 74 | messageSender(message, spoiler, iconURL, misskeyEmbed, noteinfo); 75 | await new Promise((resolve) => setTimeout(resolve, 1500)); 76 | embedSuppresser(message); 77 | 78 | try { 79 | if (resp.data.files?.length > 1) { 80 | resp.data.files 81 | .filter((_file, index) => index > 0 && index < 4) 82 | .forEach((file) => { 83 | if (file.type == 'image/webp' || 'image/png' || 'image/jpg') { 84 | const noteEmbed = new EmbedBuilder(); 85 | noteEmbed.setColor(0x96d04a); 86 | noteEmbed.setImage(file.url); 87 | messageSubSender(message, spoiler, iconURL, noteEmbed, 'ermiana'); 88 | } 89 | }); 90 | } 91 | } catch {} 92 | */ 93 | 94 | /* 95 | 暫時先不處理影片 96 | try { 97 | resp.data.files?.forEach((file) => { 98 | if (file.type == 'video/mp4') { 99 | if (file.url.match(/https:\/\/media\.misskeyusercontent\.(?:com|jp)\/io\/.*\.mp4/)) { 100 | videoLinkSender(message, spoiler, file.url); 101 | } else if (file.url.match(/https:\/\/proxy\.misskeyusercontent\.(?:com|jp)\/image\.webp\?url=.*\.mp4/)) { 102 | const othersiteUrl = decodeURIComponent(file.url.match(/url=(.+)/)[1]); 103 | videoLinkSender(message, spoiler, othersiteUrl); 104 | } 105 | } 106 | }); 107 | } catch {} 108 | */ 109 | } else { 110 | console.error('Request failed'); 111 | } 112 | } catch { 113 | console.log('misskey error: '+ message.guild.name); 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /src/regex/handleNhRegex.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import Conf from 'conf'; 4 | import { messageSender } from '../events/messageSender.js'; 5 | import { embedSuppresser } from '../events/embedSuppresser.js'; 6 | import { typingSender } from '../events/typingSender.js'; 7 | 8 | export async function handleNhRegex( result, message, spoiler ) { 9 | const iconURL = 'https://ermiana.canaria.cc/pic/eh.png'; 10 | typingSender(message); 11 | const nid = result[1]; 12 | try { 13 | const ermianaNh = new Conf({ projectName: 'ermianaJS' }); 14 | if (!ermianaNh.get('NhHeaderCookie')) { 15 | await reloadNhTK(); 16 | } 17 | const NhHeaderCookie = ermianaNh.get('NhHeaderCookie'); 18 | 19 | const headers = { 20 | 'Accept': 'application/json', 21 | 'Accept-Encoding': 'gzip, deflate, br', 22 | 'Connection': 'keep-alive', 23 | 'Host': 'nhentai.net', 24 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36', 25 | 'Cookie': NhHeaderCookie, 26 | }; 27 | 28 | const resp = await axios.request({ 29 | method: 'get', 30 | url: 'https://nhentai.net/api/gallery/' + nid, 31 | headers: headers, 32 | timeout: 2000, 33 | }); 34 | 35 | if (resp.status === 200) { 36 | const tagMap = new Map(); 37 | resp.data.tags.forEach((tag) => { 38 | const type = tag.type; 39 | const name = tag.name; 40 | 41 | if (tagMap.has(type)) { 42 | tagMap.get(type).push(name); 43 | } else { 44 | tagMap.set(type, [name]); 45 | } 46 | }); 47 | 48 | // translate the tag keys 49 | const translateTags = []; 50 | const tagReplaceList = new Map([ 51 | ['artist', '繪師'], 52 | ['category', '類別'], 53 | ['character', '角色'], 54 | ['cosplayer', 'coser'], 55 | ['female', '女性'], 56 | ['group', '社團'], 57 | ['language', '語言'], 58 | ['male', '男性'], 59 | ['mixed', '混合'], 60 | ['other', '其他'], 61 | ['parody', '原作'], 62 | ['reclass', '重新分類'], 63 | ['temp', '臨時'], 64 | ['tag', '標籤'], 65 | ]); 66 | tagMap.forEach((value, key) => { 67 | const values = value.join(', '); 68 | if (tagReplaceList.has(key)) { 69 | translateTags.push(tagReplaceList.get(key) + ': ' + values); 70 | } else { 71 | translateTags.push(key + ': ' + values); 72 | } 73 | }); 74 | 75 | const nhEmbed = new EmbedBuilder(); 76 | nhEmbed.setColor(0xed2553); 77 | 78 | try { 79 | if (resp.data.title.japanese) { 80 | nhEmbed.setTitle(resp.data.title.japanese); 81 | } else { 82 | nhEmbed.setTitle(resp.data.title.english); 83 | } 84 | } catch {} 85 | 86 | nhEmbed.setURL(result[0]); 87 | // nhEmbed.addFields( 88 | // { name: "頁數", value: resp.data.num_pages.toString(), inline : true}, 89 | // { name: "收藏", value: resp.data.num_favorites.toString(), inline : true} 90 | // ); 91 | try { 92 | nhEmbed.addFields({ name: '說明', value: translateTags.reverse().join('\n') }); 93 | } catch {} 94 | try { 95 | nhEmbed.setImage('https://t.nhentai.net/galleries/' + resp.data.media_id + '/thumb.jpg'); 96 | } catch {} 97 | 98 | messageSender(message, spoiler, iconURL, nhEmbed, 'ermiana'); 99 | embedSuppresser(message); 100 | } else { 101 | console.error('Request failed'); 102 | } 103 | } catch { 104 | console.log('nh error'); 105 | await reloadNhTK(); 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /src/regex/handlePchomeRegex.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import * as cheerio from 'cheerio'; 4 | import { messageSender } from '../events/messageSender.js'; 5 | import { typingSender } from '../events/typingSender.js'; 6 | import { embedSuppresser } from '../events/embedSuppresser.js'; 7 | 8 | export async function handlePchomeRegex( result, message, spoiler ) { 9 | const iconURL = 'https://ermiana.canaria.cc/pic/pchome.png'; 10 | typingSender(message); 11 | try { 12 | const pcid = result[1]; 13 | const str = result[0]; 14 | 15 | const url1 = 'https://ecapi-cdn.pchome.com.tw/ecshop/prodapi/v2/prod/' + pcid + '&fields=Name,Nick,Price,Pic&_callback=jsonp_prod&2837602?_callback=jsonp_prod'; 16 | const resp1 = await axios.request({ 17 | method: 'get', 18 | url: url1, 19 | timeout: 2500, 20 | }); 21 | 22 | if (resp1.status === 200) { 23 | // const namestr = unescape(resp1.data.match(/"Name":"([^"]+)"/)[1].replace(/\\u/g, '%u')); 24 | const $ = cheerio.load(unescape(resp1.data.match(/"Nick":"(.*?)",/)[1].replace(/\\u/g, '%u').replace(/\\/g, ''))); 25 | const nickstr = $.text(); 26 | const pricestr = unescape(resp1.data.match(/"P":(\d+)/)[1].replace(/\\u/g, '%u')); 27 | const picstr = unescape(resp1.data.match(/"B":"(.*?)",/)[1].replace(/\\u/g, '%u')); 28 | const picurl = 'https://img.pchome.com.tw/cs' + picstr.replace(/\\/g, ''); 29 | 30 | const url2 = 'https://ecapi-cdn.pchome.com.tw/cdn/ecshop/prodapi/v2/prod/' + pcid + '/desc&fields=Meta,SloganInfo&_callback=jsonp_desc?_callback=jsonp_desc'; 31 | const resp2 = await axios.request({ 32 | method: 'get', 33 | url: url2, 34 | timeout: 2500, 35 | }); 36 | 37 | const brandstr = unescape(resp2.data.match(/BrandNames":\[(.*?)\]/)[1].replace(/\\u/g, '%u')).replace(/","/g, '_').replace(/^"|"$/g, ''); 38 | const sloganstr = unescape(resp2.data.match(/SloganInfo":\[(.*?)\]/)[1].replace(/\\u/g, '%u')).replace(/","/g, '\n').replace(/^"|"$/g, ''); 39 | 40 | const pchomeEmbed = new EmbedBuilder(); 41 | pchomeEmbed.setColor(0xEA1717); 42 | pchomeEmbed.setTitle(nickstr); 43 | pchomeEmbed.setURL(str); 44 | try { 45 | pchomeEmbed.setDescription(sloganstr); 46 | } catch {} 47 | try { 48 | pchomeEmbed.addFields( 49 | { name: '品牌', value: brandstr, inline: true }, 50 | ); 51 | } catch {} 52 | try { 53 | pchomeEmbed.addFields( 54 | { name: '價格', value: pricestr, inline: true }, 55 | ); 56 | } catch {} 57 | try { 58 | pchomeEmbed.setImage(picurl); 59 | } catch {} 60 | 61 | messageSender(message, spoiler, iconURL, pchomeEmbed, 'ermiana'); 62 | await new Promise((resolve) => setTimeout(resolve, 1000)); 63 | embedSuppresser(message); 64 | } 65 | } catch { 66 | console.log('pchome error: '+ message.guild.name); 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /src/regex/handlePixivRegex.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import { messageSender } from '../events/messageSender.js'; 4 | // import { messageSubSender } from '../events/messageSubSender.js'; 5 | import { embedSuppresser } from '../events/embedSuppresser.js'; 6 | import { backupLinkSender } from '../events/backupLinkSender.js'; 7 | import { typingSender } from '../events/typingSender.js'; 8 | import { messageSenderPixiv } from '../events/messageSenderPixiv.js'; 9 | 10 | export async function handlePixivRegex( result, message, spoiler ) { 11 | const iconURL = 'https://ermiana.canaria.cc/pic/pixiv.png'; 12 | typingSender(message); 13 | const pid = result[1]; 14 | try { 15 | const resp = await axios.request({ 16 | method: 'get', 17 | url: 'https://www.pixiv.net/ajax/illust/' + pid, 18 | timeout: 2500, 19 | }); 20 | 21 | const tagString = resp.data.body.tags.tags.map((element) => `[${element.tag}](https://www.pixiv.net/tags/${element.tag}/artworks)`).join(', ') || ''; 22 | // const pageCount = Math.min(resp.data.body.pageCount, 5); 23 | const pageCount = resp.data.body.pageCount || 1; 24 | 25 | const pixivEmbed = new EmbedBuilder(); 26 | pixivEmbed.setColor(0x0096fa); 27 | pixivEmbed.setTitle(resp.data.body.title); 28 | pixivEmbed.setURL(`https://www.pixiv.net/artworks/${pid}`); 29 | 30 | try { 31 | if (resp.data.body.extraData.meta.twitter.description) { 32 | pixivEmbed.setDescription(resp.data.body.extraData.meta.twitter.description.substring(0, 300)); 33 | } 34 | } catch {} 35 | 36 | try { 37 | pixivEmbed.addFields( 38 | { name: '作者', value: `[${resp.data.body.userName}](https://www.pixiv.net/users/${resp.data.body.userId})`, inline: true }, 39 | { name: '收藏', value: resp.data.body.bookmarkCount.toString(), inline: true }, 40 | ); 41 | } catch {} 42 | 43 | try { 44 | if (tagString) { 45 | pixivEmbed.addFields({ name: '標籤', value: tagString }); 46 | } 47 | } catch {} 48 | 49 | try { 50 | if (resp.data.body.urls.regular != null && (/i\.pximg\.net/).test(resp.data.body.urls.regular)) { 51 | const regularPicUrl = resp.data.body.urls.regular.replace('i.pximg.net', 'pixiv.canaria.cc'); 52 | pixivEmbed.setImage(regularPicUrl); 53 | if (pageCount == 1) { 54 | messageSender(message, spoiler, iconURL, pixivEmbed, 'ermiana'); 55 | } else if (pageCount > 1) { 56 | messageSenderPixiv(message, spoiler, iconURL, pixivEmbed, 'ermiana', pageCount); 57 | /* 58 | for (const i of Array(pageCount-1).keys()) { 59 | const picEmbed = new EmbedBuilder(); 60 | picEmbed.setColor(0x0096fa); 61 | picEmbed.setImage(regularPicUrl.replace('_p0', `_p${i+1}` )); 62 | messageSubSender(message, spoiler, iconURL, picEmbed, 'ermiana'); 63 | } 64 | */ 65 | } 66 | await new Promise((resolve) => setTimeout(resolve, 1000)); 67 | embedSuppresser(message); 68 | } else if (resp.data.body.userIllusts[pid]?.url && (/p0/).test(resp.data.body.userIllusts[pid]?.url)) { 69 | const userIllustsRegex = /\/img\/.*p0/; 70 | const userIllustsUrl = 'https://pixiv.canaria.cc/img-master' + resp.data.body.userIllusts[pid].url.match(userIllustsRegex)[0] + '_master1200.jpg'; 71 | pixivEmbed.setImage(userIllustsUrl); 72 | if (pageCount == 1) { 73 | messageSender(message, spoiler, iconURL, pixivEmbed, 'ermiana'); 74 | } else if (pageCount > 1) { 75 | messageSenderPixiv(message, spoiler, iconURL, pixivEmbed, 'ermiana', pageCount); 76 | /* 77 | for (const i of Array(pageCount-1).keys()) { 78 | const picEmbed = new EmbedBuilder(); 79 | picEmbed.setColor(0x0096fa); 80 | picEmbed.setImage(userIllustsUrl.replace('_p0', `_p${i+1}` )); 81 | messageSubSender(message, spoiler, iconURL, picEmbed, 'ermiana'); 82 | } 83 | */ 84 | } 85 | await new Promise((resolve) => setTimeout(resolve, 1000)); 86 | embedSuppresser(message); 87 | } else { 88 | try { 89 | const resp2 = await axios.request({ 90 | method: 'post', 91 | url: 'https://api.pixiv.cat/v1/generate', 92 | data: { 93 | p: result[0], 94 | }, 95 | timeout: 2500, 96 | }); 97 | if (resp2.data?.original_url) { 98 | pixivEmbed.setImage(resp2.data.original_url.replace('i.pximg.net', 'pixiv.canaria.cc')); 99 | messageSender(message, spoiler, iconURL, pixivEmbed, 'ermiana'); 100 | await new Promise((resolve) => setTimeout(resolve, 1000)); 101 | embedSuppresser(message); 102 | } 103 | if (resp2.data?.original_urls) { 104 | pixivEmbed.setImage(resp2.data.original_urls[0].replace('i.pximg.net', 'pixiv.canaria.cc')); 105 | messageSenderPixiv(message, spoiler, iconURL, pixivEmbed, 'ermiana', resp2.data.original_urls.length); 106 | /* 107 | const pageCount2 = Math.min(resp2.data.original_urls.length, 5); 108 | pixivEmbed.setImage(resp2.data.original_urls[0].replace('i.pximg.net', 'pixiv.canaria.cc')); 109 | messageSender(message, spoiler, iconURL, pixivEmbed, 'ermiana'); 110 | for (const i of Array(pageCount2-1).keys()) { 111 | const picEmbed = new EmbedBuilder(); 112 | picEmbed.setColor(0x0096fa); 113 | picEmbed.setImage(resp2.data.original_urls[i+1].replace('i.pximg.net', 'pixiv.canaria.cc')); 114 | messageSubSender(message, spoiler, iconURL, picEmbed, 'ermiana'); 115 | } 116 | */ 117 | await new Promise((resolve) => setTimeout(resolve, 1000)); 118 | embedSuppresser(message); 119 | } 120 | } catch {} 121 | } 122 | } catch {} 123 | } catch { 124 | try { 125 | backupLinkSender(message, spoiler, `https://www.phixiv.net/artworks/${pid}`); 126 | await new Promise((resolve) => setTimeout(resolve, 1500)); 127 | embedSuppresser(message); 128 | } catch { 129 | console.log('pixiv error: '+ message.guild.name); 130 | } 131 | } 132 | }; 133 | -------------------------------------------------------------------------------- /src/regex/handlePlurkRegex.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import * as cheerio from 'cheerio'; 4 | import { messageSender } from '../events/messageSender.js'; 5 | import { embedSuppresser } from '../events/embedSuppresser.js'; 6 | import { typingSender } from '../events/typingSender.js'; 7 | import { messageSenderMore } from '../events/messageSenderMore.js'; 8 | 9 | export async function handlePlurkRegex( result, message, spoiler ) { 10 | const iconURL = 'https://ermiana.canaria.cc/pic/plurk.png'; 11 | typingSender(message); 12 | try { 13 | const plurkHTML = await axios.request({ 14 | url: 'https://www.plurk.com/p/' + result[1], 15 | method: 'get', 16 | timeout: 2500, 17 | }); 18 | 19 | if (plurkHTML.status === 200) { 20 | const $ = cheerio.load(plurkHTML.data); 21 | 22 | const rePlurk = $('script').text().match(/"replurkers_count": (\d+),/)[1] || '0'; 23 | const favPlurk = $('script').text().match(/"favorite_count": (\d+),/)[1] || '0'; 24 | const respPlurk = $('script').text().match(/"response_count": (\d+),/)[1] || '0'; 25 | const plurkInfo = '💬' + respPlurk + ' 🔁' + rePlurk + ' ❤️' + favPlurk; 26 | 27 | const plurkName = $('.name').text() || '噗浪使用者'; 28 | const plurkContent = $('.text_holder').text() || ''; 29 | const rawPlurkIndex = $('script').text().indexOf('content_raw') || -1; 30 | const picPlurk = $('script').text().slice(rawPlurkIndex).match(/https:\/\/images\.plurk\.com\/[^\\"\s]+/g) || []; 31 | 32 | const plurkEmbed = new EmbedBuilder(); 33 | plurkEmbed.setColor(0xefa54c); 34 | plurkEmbed.setTitle(plurkName); 35 | plurkEmbed.setURL('https://www.plurk.com/p/' + result[1]); 36 | try { 37 | if (plurkContent) { 38 | plurkEmbed.setDescription(plurkContent); 39 | } 40 | } catch {} 41 | try { 42 | if (picPlurk.length > 0) { 43 | plurkEmbed.setImage(picPlurk[0]); 44 | } 45 | } catch {} 46 | 47 | try { 48 | if (!picPlurk || picPlurk.length == 0) { 49 | messageSender(message, spoiler, iconURL, plurkEmbed, plurkInfo); 50 | embedSuppresser(message); 51 | await new Promise((resolve) => setTimeout(resolve, 800)); 52 | embedSuppresser(message); 53 | } else if (picPlurk.length == 1) { 54 | messageSender(message, spoiler, iconURL, plurkEmbed, plurkInfo); 55 | embedSuppresser(message); 56 | await new Promise((resolve) => setTimeout(resolve, 800)); 57 | embedSuppresser(message); 58 | } else if (picPlurk.length > 1) { 59 | const imageArray =[]; 60 | picPlurk.filter((_pic, index) => index > 0 && index < 4) 61 | .forEach((pic) => { 62 | imageArray.push(pic); 63 | }); 64 | messageSenderMore(message, spoiler, iconURL, plurkEmbed, plurkInfo, imageArray); 65 | embedSuppresser(message); 66 | await new Promise((resolve) => setTimeout(resolve, 800)); 67 | embedSuppresser(message); 68 | } 69 | } catch {} 70 | } else { 71 | throw Error('plurk error: '+ message.guild.name); 72 | } 73 | } catch { 74 | console.log('plurk error: '+ message.guild.name); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/regex/handlePlurkRegexV2.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import * as cheerio from 'cheerio'; 4 | import { messageSender } from '../events/messageSender.js'; 5 | import { embedSuppresser } from '../events/embedSuppresser.js'; 6 | import { typingSender } from '../events/typingSender.js'; 7 | import { messageSenderMore } from '../events/messageSenderMore.js'; 8 | 9 | export async function handlePlurkRegex( result, message, spoiler ) { 10 | const iconURL = 'https://ermiana.canaria.cc/pic/plurk.png'; 11 | typingSender(message); 12 | try { 13 | const plurkHTML = await axios.request({ 14 | url: 'https://www.plurk.com/p/' + result[1], 15 | method: 'get', 16 | timeout: 2500, 17 | }); 18 | 19 | if (plurkHTML.status === 200) { 20 | const $ = cheerio.load(plurkHTML.data); 21 | 22 | const rePlurk = $('script').text().match(/"replurkers_count": (\d+),/)[1] || '0'; 23 | const favPlurk = $('script').text().match(/"favorite_count": (\d+),/)[1] || '0'; 24 | const respPlurk = $('script').text().match(/"response_count": (\d+),/)[1] || '0'; 25 | const plurkInfo = '💬' + respPlurk + ' 🔁' + rePlurk + ' ❤️' + favPlurk; 26 | 27 | const plurkName = $('.name').text() || '噗浪使用者'; 28 | const plurkContent = $('.text_holder').html().replace(//g, '\n').replace(/<[^>]+>/g, '') || ''; 29 | /* 30 | const plurkContent = ( 31 | $('script') 32 | .text() 33 | .replace(/<(?!br\s*\/?)[^>]+>/gi, '') 34 | .match(/"content":\s*"([^"]+)"/)?.[1] 35 | .replace(//g, '\n') 36 | .replace(/\\u[\dA-Fa-f]{4}/g, (match) => { 37 | return String.fromCharCode(parseInt(match.slice(2), 16)); 38 | }) 39 | .trim() || '' 40 | ); 41 | */ 42 | const rawPlurkIndex = $('script').text().indexOf('content_raw') || -1; 43 | const picPlurk = $('script').text().slice(rawPlurkIndex).match(/https:\/\/images\.plurk\.com\/[^\\"\s]+/g) || []; 44 | 45 | const plurkId = $('script').text().match(/"page_user":\s*{"id":\s*(\d+),/)?.[1] || '17527487'; 46 | const plurkAvatar = $('script').text().match(/"avatar":\s*(\d+)/)?.[1] || '79721750'; 47 | const plurkNickName = $('script').text().match(/"nick_name":\s*"([^"]+)"/)?.[1] || 'plurkuser'; 48 | 49 | const plurkEmbed = new EmbedBuilder(); 50 | plurkEmbed.setColor(0xefa54c); 51 | plurkEmbed.setTitle(plurkName); 52 | plurkEmbed.setURL('https://www.plurk.com/p/' + result[1]); 53 | try { 54 | if (plurkId && plurkAvatar && plurkNickName) { 55 | plurkEmbed.setAuthor({ name: '@' + plurkNickName, iconURL: `https://avatars.plurk.com/${plurkId}-medium${plurkAvatar}.gif` }); 56 | } 57 | } catch {} 58 | try { 59 | if (plurkContent) { 60 | plurkEmbed.setDescription(plurkContent); 61 | } 62 | } catch {} 63 | try { 64 | if (picPlurk.length > 0) { 65 | plurkEmbed.setImage(picPlurk[0]); 66 | } 67 | } catch {} 68 | 69 | try { 70 | if (!picPlurk || picPlurk.length == 0) { 71 | messageSender(message, spoiler, iconURL, plurkEmbed, plurkInfo); 72 | embedSuppresser(message); 73 | await new Promise((resolve) => setTimeout(resolve, 800)); 74 | embedSuppresser(message); 75 | } else if (picPlurk.length == 1) { 76 | messageSender(message, spoiler, iconURL, plurkEmbed, plurkInfo); 77 | embedSuppresser(message); 78 | await new Promise((resolve) => setTimeout(resolve, 800)); 79 | embedSuppresser(message); 80 | } else if (picPlurk.length > 1) { 81 | const imageArray =[]; 82 | picPlurk.filter((_pic, index) => index > 0 && index < 4) 83 | .forEach((pic) => { 84 | imageArray.push(pic); 85 | }); 86 | messageSenderMore(message, spoiler, iconURL, plurkEmbed, plurkInfo, imageArray); 87 | embedSuppresser(message); 88 | await new Promise((resolve) => setTimeout(resolve, 800)); 89 | embedSuppresser(message); 90 | } 91 | } catch {} 92 | } else { 93 | throw Error('plurk error: '+ message.guild.name); 94 | } 95 | } catch { 96 | console.log('plurk error: '+ message.guild.name); 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /src/regex/handlePttRegex.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import * as cheerio from 'cheerio'; 4 | import { messageSender } from '../events/messageSender.js'; 5 | import { embedSuppresser } from '../events/embedSuppresser.js'; 6 | import { typingSender } from '../events/typingSender.js'; 7 | import { backupLinkSender } from '../events/backupLinkSender.js'; 8 | 9 | export async function handlePttRegex( result, message, spoiler ) { 10 | const iconURL = 'https://ermiana.canaria.cc/pic/ptt.png'; 11 | const supportBoard = ['Gossiping', 'C_Chat', 'AC_In', 'H-GAME', 'sex', 'HatePolitics', 'Beauty', 'japanavgirls', 'DMM_GAMES']; 12 | 13 | function boardNameStandardization(boardName) { 14 | const boardNameStandardized = boardName.toLowerCase(); 15 | if (boardNameStandardized === 'gossiping') { 16 | return 'Gossiping'; 17 | } else if (boardNameStandardized === 'c_chat') { 18 | return 'C_Chat'; 19 | } else if (boardNameStandardized === 'ac_in') { 20 | return 'AC_In'; 21 | } else if (boardNameStandardized === 'h-game') { 22 | return 'H-GAME'; 23 | } else if (boardNameStandardized === 'sex') { 24 | return 'sex'; 25 | } else if (boardNameStandardized === 'hatepolitics') { 26 | return 'HatePolitics'; 27 | } else if (boardNameStandardized === 'beauty') { 28 | return 'Beauty'; 29 | } else if (boardNameStandardized === 'japanavgirls') { 30 | return 'japanavgirls'; 31 | } else if (boardNameStandardized === 'dmm_games') { 32 | return 'DMM_GAMES'; 33 | } else { 34 | return boardName; 35 | } 36 | } 37 | 38 | function getPictures(text) { 39 | const pattern = /https:\/\/.*\.(jpg|jpeg|png|gif|webp)/; 40 | const result = text.match(pattern); 41 | if (result && result.length > 0) { 42 | return result[0]; 43 | } else { 44 | return ''; 45 | } 46 | } 47 | 48 | function getMainContent(text) { 49 | const pattern = /^(.|\n)+批踢踢實業坊\(ptt\.cc\)/; 50 | const result = text.match(pattern); 51 | if (result && result.length > 0) { 52 | return getPictures(result[0]); 53 | } else { 54 | return getPictures(text); 55 | } 56 | } 57 | 58 | function getDescription(text) { 59 | const matches = (text+'5.完整新聞連結').match(/4\.完整新聞內文:[\s\S]+?5.完整新聞連結/) || (text+'5.完整新聞連結').match(/2\.記者署名:[\s\S]+?5.完整新聞連結/); 60 | const newsContent = matches ? matches[0].replace('4.完整新聞內文:\n', '').trim() : ''; 61 | return newsContent.replace(/^※.*$/gm, '').replace(/^\s*[\r\n]/gm, '').substring(0, 160); 62 | } 63 | 64 | try { 65 | if (supportBoard.includes(boardNameStandardization(result[1]))) { 66 | const pttHTML = await axios.request({ 67 | url: `https://www.ptt.cc/bbs/${boardNameStandardization(result[1])}/${result[2]}.html`, 68 | method: 'get', 69 | headers: { Cookie: 'over18=1;' }, 70 | timeout: 2000, 71 | }); 72 | 73 | typingSender(message); 74 | 75 | if (pttHTML.status === 200) { 76 | const $ = cheerio.load(pttHTML.data); 77 | const pttHtmlTitle = $('meta[property=og:title]').attr('content') || 'PTT.cc'; 78 | const pttHtmlDescription = $('meta[property=og:description]').attr('content') || ''; 79 | const mainContent = $('#main-content').text().substring(0, 1000) || ''; 80 | 81 | try { 82 | const pttResp = await axios.request({ 83 | method: 'get', 84 | url: `https://moptt.tw/ptt/${boardNameStandardization(result[1])}.${result[2]}`, 85 | timeout: 2000, 86 | }); 87 | 88 | const mopttEmbed = new EmbedBuilder(); 89 | mopttEmbed.setColor(0x2C2C2C); 90 | try { 91 | if (pttResp.status === 200) { 92 | mopttEmbed.setTitle(pttResp.data.title); 93 | } else { 94 | mopttEmbed.setTitle(pttHtmlTitle); 95 | } 96 | } catch {} 97 | try { 98 | mopttEmbed.setURL(`https://www.ptt.cc/bbs/${boardNameStandardization(result[1])}/${result[2]}.html`); 99 | } catch {} 100 | try { 101 | if (pttHtmlDescription.match(/1\.媒體來源:/)) { 102 | if (getDescription(mainContent)) { 103 | mopttEmbed.setDescription(getDescription(mainContent)); 104 | } else if (pttHtmlDescription) { 105 | mopttEmbed.setDescription(pttHtmlDescription); 106 | } 107 | } else if (pttHtmlDescription) { 108 | mopttEmbed.setDescription(pttHtmlDescription); 109 | } else if (pttResp.data.description) { 110 | mopttEmbed.setDescription(pttResp.data.description); 111 | } 112 | } catch {} 113 | try { 114 | if (pttResp.data.imageSource) { 115 | mopttEmbed.setImage(pttResp.data.imageSource); 116 | } else if (mainContent) { 117 | const constentPic = getMainContent(mainContent); 118 | if (constentPic) { 119 | mopttEmbed.setImage(constentPic); 120 | } 121 | } 122 | } catch {} 123 | try { 124 | messageSender(message, spoiler, iconURL, mopttEmbed, 'ermiana'); 125 | embedSuppresser(message); 126 | } catch {} 127 | } catch { 128 | try { 129 | const pttEmbed = new EmbedBuilder(); 130 | pttEmbed.setColor(0x2C2C2C); 131 | try { 132 | pttEmbed.setTitle(pttHtmlTitle); 133 | pttEmbed.setURL(result[0]); 134 | } catch {} 135 | try { 136 | if (pttHtmlDescription.match(/1\.媒體來源:/)) { 137 | if (getDescription(mainContent)) { 138 | pttEmbed.setDescription(getDescription(mainContent)); 139 | } else if (pttHtmlDescription) { 140 | pttEmbed.setDescription(pttHtmlDescription); 141 | } 142 | } else if (pttHtmlDescription) { 143 | pttEmbed.setDescription(pttHtmlDescription); 144 | } 145 | } catch {} 146 | try { 147 | if (mainContent) { 148 | const constentPic = getMainContent(mainContent); 149 | if (constentPic) { 150 | pttEmbed.setImage(constentPic); 151 | } 152 | } 153 | } catch {} 154 | try { 155 | messageSender(message, spoiler, iconURL, pttEmbed, 'ermiana'); 156 | embedSuppresser(message); 157 | } catch {} 158 | } catch {} 159 | } 160 | } else { 161 | await new Promise((resolve) => setTimeout(resolve, 1800)); 162 | const pttHTML2 = await axios.request({ 163 | url: `https://ptt-demo.canaria.cc/bbs/${boardNameStandardization(result[1])}/${result[2]}.html`, 164 | method: 'get', 165 | headers: { Cookie: 'over18=1;' }, 166 | timeout: 3000, 167 | }); 168 | 169 | if (pttHTML2.status === 200) { 170 | const $ = cheerio.load(pttHTML2.data); 171 | const pttHtmlTitle2 = $('meta[property=og:title]').attr('content') || 'PTT.cc'; 172 | const pttHtmlDescription2 = $('meta[property=og:description]').attr('content') || ''; 173 | const mainContent2 = $('#main-content').text().substring(0, 1000) || ''; 174 | 175 | try { 176 | const pttResp2 = await axios.request({ 177 | method: 'get', 178 | url: `https://moptt.tw/ptt/${boardNameStandardization(result[1])}.${result[2]}`, 179 | timeout: 3000, 180 | }); 181 | const mopttEmbed2 = new EmbedBuilder(); 182 | mopttEmbed2.setColor(0x2C2C2C); 183 | try { 184 | if (pttResp2.status === 200) { 185 | mopttEmbed2.setTitle(pttResp2.data.title); 186 | } else { 187 | mopttEmbed2.setTitle(pttHtmlTitle2); 188 | } 189 | } catch {} 190 | try { 191 | mopttEmbed2.setURL(`https://www.ptt.cc/bbs/${boardNameStandardization(result[1])}/${result[2]}.html`); 192 | } catch {} 193 | try { 194 | if (pttHtmlDescription2.match(/1\.媒體來源:/)) { 195 | if (getDescription(mainContent2)) { 196 | mopttEmbed2.setDescription(getDescription(mainContent2)); 197 | } else if (pttHtmlDescription2) { 198 | mopttEmbed2.setDescription(pttHtmlDescription2); 199 | } 200 | } else if (pttHtmlDescription2) { 201 | mopttEmbed2.setDescription(pttHtmlDescription2); 202 | } else if (pttResp2.data.description) { 203 | mopttEmbed2.setDescription(pttResp2.data.description); 204 | } 205 | } catch {} 206 | try { 207 | if (pttResp2.data.imageSource) { 208 | mopttEmbed2.setImage(pttResp2.data.imageSource); 209 | } else if (mainContent2) { 210 | const constentPic2 = getMainContent(mainContent2); 211 | if (constentPic2) { 212 | mopttEmbed2.setImage(constentPic2); 213 | } 214 | } 215 | } catch {} 216 | try { 217 | messageSender(message, spoiler, iconURL, mopttEmbed2, 'ermiana'); 218 | embedSuppresser(message); 219 | } catch {} 220 | } catch { 221 | try { 222 | const pttEmbed2 = new EmbedBuilder(); 223 | pttEmbed2.setColor(0x2C2C2C); 224 | try { 225 | pttEmbed2.setTitle(pttHtmlTitle2); 226 | pttEmbed2.setURL(result[0]); 227 | } catch {} 228 | try { 229 | if (pttHtmlDescription2.match(/1\.媒體來源:/)) { 230 | if (getDescription(mainContent2)) { 231 | pttEmbed2.setDescription(getDescription(mainContent2)); 232 | } else if (pttHtmlDescription2) { 233 | pttEmbed2.setDescription(pttHtmlDescription2); 234 | } 235 | } else if (pttHtmlDescription2) { 236 | pttEmbed2.setDescription(pttHtmlDescription2); 237 | } 238 | } catch {} 239 | try { 240 | if (mainContent2) { 241 | const constentPic2 = getMainContent(mainContent2); 242 | if (constentPic2) { 243 | pttEmbed2.setImage(constentPic2); 244 | } 245 | } 246 | } catch {} 247 | try { 248 | messageSender(message, spoiler, iconURL, pttEmbed2, 'ermiana'); 249 | embedSuppresser(message); 250 | } catch {} 251 | } catch {} 252 | } 253 | } 254 | } 255 | } 256 | } catch { 257 | try { 258 | backupLinkSender(message, spoiler, `https://www.pttweb.cc/bbs/${boardNameStandardization(result[1])}/${result[2]}`); 259 | await new Promise((resolve) => setTimeout(resolve, 1000)); 260 | embedSuppresser(message); 261 | } catch { 262 | console.log('ptt error: '+ message.guild.name); 263 | } 264 | } 265 | }; 266 | -------------------------------------------------------------------------------- /src/regex/handleThreadsRegex.js: -------------------------------------------------------------------------------- 1 | // import axios from 'axios'; 2 | import { embedSuppresser } from '../events/embedSuppresser.js'; 3 | import { backupLinkSender } from '../events/backupLinkSender.js'; 4 | import { typingSender } from '../events/typingSender.js'; 5 | 6 | export async function handleThreadsRegex( result, message, spoiler ) { 7 | try { 8 | await typingSender(message); 9 | backupLinkSender(message, spoiler, result[0].replace(/threads\.net/, 'fixthreads.net')); 10 | await new Promise((resolve) => setTimeout(resolve, 1500)); 11 | embedSuppresser(message); 12 | } catch { 13 | // console.log('threads error: '+ message.guild.name); 14 | return; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/regex/handleTiktokRegex.js: -------------------------------------------------------------------------------- 1 | import { embedSuppresser } from '../events/embedSuppresser.js'; 2 | import { backupLinkSender } from '../events/backupLinkSender.js'; 3 | import { typingSender } from '../events/typingSender.js'; 4 | 5 | export async function handleTiktokRegex( result, message, spoiler ) { 6 | typingSender(message); 7 | try { 8 | backupLinkSender(message, spoiler, result[0].replace(/tiktok\.com/, 'tnktok.com')); 9 | await new Promise((resolve) => setTimeout(resolve, 1500)); 10 | embedSuppresser(message); 11 | } catch { 12 | console.log('tiktok error: '+ message.guild.name); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/regex/handleTiktokRegexV2.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { embedSuppresser } from '../events/embedSuppresser.js'; 3 | import { backupLinkSender } from '../events/backupLinkSender.js'; 4 | import { typingSender } from '../events/typingSender.js'; 5 | 6 | export async function handleTiktokRegex( result, message, spoiler ) { 7 | try { 8 | const tkHTML = await axios.request({ 9 | url: result[0].replace(/tiktok\.com/, 'tnktok.com'), 10 | method: 'get', 11 | timeout: 3000, 12 | }); 13 | 14 | if (tkHTML.status === 200) { 15 | await typingSender(message); 16 | backupLinkSender(message, spoiler, result[0].replace(/tiktok\.com/, 'tnktok.com')); 17 | await new Promise((resolve) => setTimeout(resolve, 1500)); 18 | embedSuppresser(message); 19 | } else { 20 | throw new Error(); 21 | } 22 | } catch { 23 | try { 24 | const tkHTML2 = await axios.request({ 25 | url: result[0].replace(/tiktok\.com/, 'tiktokez.com'), 26 | method: 'get', 27 | timeout: 3000, 28 | }); 29 | 30 | if (tkHTML2.status === 200) { 31 | await typingSender(message); 32 | backupLinkSender(message, spoiler, result[0].replace(/tiktok\.com/, 'tiktokez.com')); 33 | await new Promise((resolve) => setTimeout(resolve, 1500)); 34 | embedSuppresser(message); 35 | } else { 36 | throw new Error(); 37 | } 38 | } catch { 39 | return; 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/regex/handleTwitterRegex.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import { messageSender } from '../events/messageSender.js'; 4 | import { embedSuppresser } from '../events/embedSuppresser.js'; 5 | import { videoLinkSender } from '../events/videoLinkSender.js'; 6 | import { backupLinkSender } from '../events/backupLinkSender.js'; 7 | import { typingSender } from '../events/typingSender.js'; 8 | 9 | function videoLinkFormat( link ) { 10 | const linkmatch = link.match(/https:\/\/.*?\.mp4/); 11 | if (linkmatch) { 12 | return linkmatch[0].includes('ext_tw_video') ? `${linkmatch[0]}?s=19` : linkmatch[0]; 13 | } else { 14 | return 'https://twitter.com/'; 15 | } 16 | } 17 | 18 | export async function handleTwitterRegex( result, message, spoiler ) { 19 | const iconURL = 'https://ermiana.canaria.cc/pic/twitter.png'; 20 | typingSender(message); 21 | const tid = result[1]; 22 | try { 23 | // use fxtwitter api 24 | const fxapiResp = await axios.request({ 25 | method: 'get', 26 | url: 'https://api.fxtwitter.com/i/status/' + tid, 27 | timeout: 2500, 28 | }); 29 | 30 | if (fxapiResp.status === 200) { 31 | const fxapitwitterEmbed = new EmbedBuilder(); 32 | fxapitwitterEmbed.setColor(0x1DA1F2); 33 | try { 34 | if (fxapiResp.data.tweet.author.screen_name && fxapiResp.data.tweet.author.avatar_url) { 35 | fxapitwitterEmbed.setAuthor({ name: '@' + fxapiResp.data.tweet.author.screen_name, iconURL: fxapiResp.data.tweet.author.avatar_url }); 36 | } else if (fxapiResp.data.tweet.author.screen_name) { 37 | fxapitwitterEmbed.setAuthor({ name: '@' + fxapiResp.data.tweet.author.screen_name }); 38 | } 39 | } catch {} 40 | try { 41 | if (fxapiResp.data.tweet.author.name) { 42 | fxapitwitterEmbed.setTitle(fxapiResp.data.tweet.author.name); 43 | } else { 44 | fxapitwitterEmbed.setTitle('Twitter.com'); 45 | } 46 | } catch {} 47 | try { 48 | if (fxapiResp.data.tweet.url) { 49 | fxapitwitterEmbed.setURL(fxapiResp.data.tweet.url); 50 | } else { 51 | fxapitwitterEmbed.setURL('https://twitter.com/i/status/' + tid); 52 | } 53 | } catch {} 54 | try { 55 | if (fxapiResp.data.tweet.text) { 56 | fxapitwitterEmbed.setDescription(fxapiResp.data.tweet.text); 57 | } 58 | } catch {} 59 | try { 60 | if (fxapiResp.data.tweet.media.mosaic && fxapiResp.data.tweet.media.mosaic.type === 'mosaic_photo') { 61 | fxapitwitterEmbed.setImage(fxapiResp.data.tweet.media.mosaic.formats.jpeg); 62 | } else if (fxapiResp.data.tweet.media.photos[0].type === 'photo') { 63 | fxapitwitterEmbed.setImage(fxapiResp.data.tweet.media.photos[0].url + '?name=large'); 64 | } 65 | } catch {} 66 | try { 67 | fxapitwitterEmbed.setTimestamp(fxapiResp.data.tweet.created_timestamp * 1000); 68 | } catch {} 69 | const fxapitweetinfo = '💬' + (fxapiResp.data.tweet.replies?.toString() || '0') + ' 🔁' + (fxapiResp.data.tweet.retweets?.toString() || '0') + ' ❤️' + (fxapiResp.data.tweet.likes?.toString() || '0'); 70 | try { 71 | messageSender(message, spoiler, iconURL, fxapitwitterEmbed, fxapitweetinfo); 72 | embedSuppresser(message); 73 | } catch {} 74 | try { 75 | if (fxapiResp.data.tweet.media) { 76 | fxapiResp.data.tweet.media.all.forEach((element) => { 77 | if (element.type != 'photo') { 78 | videoLinkSender(message, spoiler, videoLinkFormat(element.url)); 79 | } 80 | }); 81 | } 82 | } catch {} 83 | } else { 84 | throw new Error('fxtwitter api error: '+ tid); 85 | } 86 | } catch { 87 | // console.log('fxtwitter api error: '+ message.guild.name); 88 | try { 89 | const vxapiResp = await axios.request({ 90 | method: 'get', 91 | url: 'https://api.vxtwitter.com/i/status/' + tid, 92 | timeout: 2500, 93 | }); 94 | 95 | if (vxapiResp.status === 200) { 96 | // use vxtwitter api 97 | const vxapitwitterEmbed = new EmbedBuilder(); 98 | vxapitwitterEmbed.setColor(0x1DA1F2); 99 | try { 100 | if (vxapiResp.data.user_screen_name) { 101 | vxapitwitterEmbed.setAuthor({ name: '@' + vxapiResp.data.user_screen_name }); 102 | } 103 | } catch {} 104 | try { 105 | if (vxapiResp.data.user_name) { 106 | vxapitwitterEmbed.setTitle(vxapiResp.data.user_name); 107 | } else { 108 | vxapitwitterEmbed.setTitle('Twitter.com'); 109 | } 110 | } catch {} 111 | try { 112 | if (vxapiResp.data.tweetURL) { 113 | vxapitwitterEmbed.setURL(vxapiResp.data.tweetURL); 114 | } else { 115 | vxapitwitterEmbed.setURL('https://twitter.com/i/status/' + tid); 116 | } 117 | } catch {} 118 | try { 119 | if (vxapiResp.data.text) { 120 | vxapitwitterEmbed.setDescription(vxapiResp.data.text); 121 | } 122 | } catch {} 123 | try { 124 | if (vxapiResp.data.media_extended && vxapiResp.data.media_extended[0].type === 'image' && vxapiResp.data.mediaURLs.length === 1) { 125 | vxapitwitterEmbed.setImage(vxapiResp.data.mediaURLs[0] + '?name=large'); 126 | } else if (vxapiResp.data.media_extended) { 127 | const vxapiRespImage = []; 128 | vxapiResp.data.media_extended.forEach((element) => { 129 | if (element.type === 'image') { 130 | vxapiRespImage.push(element.url); 131 | } 132 | }); 133 | if (vxapiRespImage.length === 1) { 134 | vxapitwitterEmbed.setImage(vxapiRespImage[0] + '?name=large'); 135 | } else if (vxapiRespImage.length > 1) { 136 | vxapitwitterEmbed.setImage('https://convert.vxtwitter.com/rendercombined.jpg?imgs=' + vxapiRespImage.join(',')); 137 | } 138 | } 139 | } catch {} 140 | try { 141 | vxapitwitterEmbed.setTimestamp(vxapiResp.data.date_epoch * 1000); 142 | } catch {} 143 | const vxapitweetinfo = '💬' + (vxapiResp.data.replies?.toString() || '0') + ' 🔁' + (vxapiResp.data.retweets?.toString() || '0') + ' ❤️' + (vxapiResp.data.likes?.toString() || '0'); 144 | try { 145 | messageSender(message, spoiler, iconURL, vxapitwitterEmbed, vxapitweetinfo); 146 | embedSuppresser(message); 147 | } catch {} 148 | try { 149 | if (vxapiResp.data.media_extended) { 150 | vxapiResp.data.media_extended.forEach((element) => { 151 | if (element.type != 'image') { 152 | videoLinkSender(message, spoiler, videoLinkFormat(element.url)); 153 | } 154 | }); 155 | } 156 | } catch {} 157 | } else { 158 | throw new Error('vxtwitter api error: '+ tid); 159 | } 160 | } catch { 161 | // console.log('vxtwitter api error: '+ message.guild.name); 162 | try { 163 | // console.log('fx vx twitter api error: '+ tid); 164 | backupLinkSender(message, spoiler, `https://fxtwitter.com/i/status/${result[1]}`); 165 | embedSuppresser(message); 166 | } catch { 167 | console.log('twitter api error: '+ message.guild.name); 168 | } 169 | } 170 | } 171 | }; 172 | -------------------------------------------------------------------------------- /src/regex/handleTwitterRegexV2.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import { messageSender } from '../events/messageSender.js'; 4 | import { embedSuppresser } from '../events/embedSuppresser.js'; 5 | import { videoLinkSender } from '../events/videoLinkSender.js'; 6 | import { backupLinkSender } from '../events/backupLinkSender.js'; 7 | import { typingSender } from '../events/typingSender.js'; 8 | 9 | export async function handleTwitterRegex( result, message, spoiler ) { 10 | const iconURL = 'https://ermiana.canaria.cc/pic/twitter.png'; 11 | typingSender(message); 12 | const tid = result[1]; 13 | 14 | function twitterEmbedMaker(autherID, autherIconURL, autherName, tweetURL, tweetText, tweetImage, tweetTimestamp) { 15 | const twitterEmbed = new EmbedBuilder(); 16 | twitterEmbed.setColor(0x1DA1F2); 17 | if (autherID && autherIconURL) { 18 | twitterEmbed.setAuthor({ name: '@' + autherID, iconURL: autherIconURL }); 19 | } else if (autherID) { 20 | twitterEmbed.setAuthor({ name: '@' + autherID }); 21 | } 22 | if (autherName) { 23 | twitterEmbed.setTitle(autherName); 24 | } else { 25 | twitterEmbed.setTitle('Twitter.com'); 26 | } 27 | if (tweetURL) { 28 | twitterEmbed.setURL(tweetURL); 29 | } else { 30 | twitterEmbed.setURL('https://twitter.com/i/status/' + tid); 31 | } 32 | if (tweetText) { 33 | twitterEmbed.setDescription(tweetText); 34 | } 35 | if (tweetImage) { 36 | twitterEmbed.setImage(tweetImage); 37 | } 38 | if (tweetTimestamp) { 39 | twitterEmbed.setTimestamp(tweetTimestamp); 40 | } 41 | return twitterEmbed; 42 | } 43 | 44 | try { 45 | // use fxtwitter api 46 | const fxapiResp = await axios.request({ 47 | method: 'get', 48 | url: 'https://api.fxtwitter.com/i/status/' + tid, 49 | timeout: 2500, 50 | }); 51 | 52 | if (fxapiResp.status === 200) { 53 | const fxapitweetinfo = '💬' + (fxapiResp.data.tweet.replies?.toString() || '0') + ' 🔁' + (fxapiResp.data.tweet.retweets?.toString() || '0') + ' ❤️' + (fxapiResp.data.tweet.likes?.toString() || '0'); 54 | if (fxapiResp.data.tweet?.media) { 55 | const fxapiRespVideo = []; 56 | fxapiResp.data.tweet.media.all.forEach((element) => { 57 | if (element.type !== 'photo') { 58 | fxapiRespVideo.push(element.url); 59 | } 60 | }); 61 | if (fxapiResp.data.tweet.media?.mosaic && fxapiResp.data.tweet.media?.mosaic.type === 'mosaic_photo') { 62 | const fxapitwitterEmbed = twitterEmbedMaker(fxapiResp.data.tweet.author.screen_name, 63 | (fxapiResp.data.tweet.author.avatar_url||''), 64 | (fxapiResp.data.tweet.author.name||'Twitter.com'), 65 | (fxapiResp.data.tweet.url||''), 66 | (fxapiResp.data.tweet.text)||'', 67 | fxapiResp.data.tweet.media.mosaic.formats.jpeg, 68 | fxapiResp.data.tweet.created_timestamp * 1000); 69 | messageSender(message, spoiler, iconURL, fxapitwitterEmbed, fxapitweetinfo); 70 | embedSuppresser(message); 71 | fxapiRespVideo.forEach((url) => { 72 | videoLinkSender(message, spoiler, url); 73 | }); 74 | } else if (fxapiResp.data.tweet.media?.photos && !fxapiResp.data.tweet.media?.mosaic) { 75 | const fxapitwitterEmbed = twitterEmbedMaker(fxapiResp.data.tweet.author.screen_name, 76 | (fxapiResp.data.tweet.author.avatar_url||''), 77 | (fxapiResp.data.tweet.author.name||'Twitter.com'), 78 | (fxapiResp.data.tweet.url||''), 79 | (fxapiResp.data.tweet.text||''), 80 | fxapiResp.data.tweet.media.photos[0].url + '?name=large', 81 | fxapiResp.data.tweet.created_timestamp * 1000); 82 | messageSender(message, spoiler, iconURL, fxapitwitterEmbed, fxapitweetinfo); 83 | embedSuppresser(message); 84 | fxapiRespVideo.forEach((url) => { 85 | videoLinkSender(message, spoiler, url); 86 | }); 87 | } else if (fxapiRespVideo && !fxapiResp.data.tweet.media?.photos) { 88 | const fxapitwitterEmbed = twitterEmbedMaker(fxapiResp.data.tweet.author.screen_name, 89 | (fxapiResp.data.tweet.author.avatar_url||''), 90 | (fxapiResp.data.tweet.author.name||'Twitter.com'), 91 | (fxapiResp.data.tweet.url||''), 92 | (fxapiResp.data.tweet.text||''), 93 | '', 94 | fxapiResp.data.tweet.created_timestamp * 1000); 95 | messageSender(message, spoiler, iconURL, fxapitwitterEmbed, fxapitweetinfo); 96 | embedSuppresser(message); 97 | videoLinkSender(message, spoiler, `https://d.vxtwitter.com/i/status/${result[1]}`); 98 | fxapiRespVideo.filter((_url, index) => index > 0) 99 | .forEach((url) => { 100 | videoLinkSender(message, spoiler, url); 101 | }); 102 | } 103 | } else { 104 | const fxapitwitterEmbed = twitterEmbedMaker(fxapiResp.data.tweet.author.screen_name, 105 | (fxapiResp.data.tweet.author.avatar_url||''), 106 | (fxapiResp.data.tweet.author.name||'Twitter.com'), 107 | (fxapiResp.data.tweet.url||''), 108 | (fxapiResp.data.tweet.text||''), 109 | '', 110 | fxapiResp.data.tweet.created_timestamp * 1000); 111 | messageSender(message, spoiler, iconURL, fxapitwitterEmbed, fxapitweetinfo); 112 | embedSuppresser(message); 113 | } 114 | } else { 115 | throw new Error('fxtwitter api error: '+ tid); 116 | } 117 | } catch { 118 | try { 119 | // use vxtwitter api 120 | const vxapiResp = await axios.request({ 121 | method: 'get', 122 | url: 'https://api.vxtwitter.com/i/status/' + tid, 123 | timeout: 2500, 124 | }); 125 | 126 | if (vxapiResp.status === 200) { 127 | const vxapitweetinfo = '💬' + (vxapiResp.data.replies?.toString() || '0') + ' 🔁' + (vxapiResp.data.retweets?.toString() || '0') + ' ❤️' + (vxapiResp.data.likes?.toString() || '0'); 128 | if (vxapiResp.data?.media_extended) { 129 | const vxapiRespImage = []; 130 | vxapiResp.data.media_extended.forEach((element) => { 131 | if (element.type === 'image') { 132 | vxapiRespImage.push(element.url); 133 | } 134 | }); 135 | const vxapiRespVideo = []; 136 | vxapiResp.data.media_extended.forEach((element) => { 137 | if (element.type !== 'image') { 138 | vxapiRespVideo.push(element.url); 139 | } 140 | }); 141 | if (vxapiRespImage.length === 1) { 142 | const vxapitwitterEmbed = twitterEmbedMaker(vxapiResp.data.user_screen_name, 143 | '', 144 | (vxapiResp.data.user_name||'Twitter.com'), 145 | (vxapiResp.data.tweetURL||''), 146 | (vxapiResp.data.text||''), 147 | vxapiRespImage[0] + '?name=large', 148 | vxapiResp.data.date_epoch * 1000); 149 | messageSender(message, spoiler, iconURL, vxapitwitterEmbed, vxapitweetinfo); 150 | embedSuppresser(message); 151 | vxapiRespVideo.forEach((url) => { 152 | videoLinkSender(message, spoiler, url); 153 | }); 154 | } else if (vxapiRespImage.length > 1) { 155 | const vxapitwitterEmbed = twitterEmbedMaker(vxapiResp.data.user_screen_name, 156 | '', 157 | (vxapiResp.data.user_name||'Twitter.com'), 158 | (vxapiResp.data.tweetURL||''), 159 | (vxapiResp.data.text||''), 160 | 'https://convert.vxtwitter.com/rendercombined.jpg?imgs=' + vxapiRespImage.join(','), 161 | vxapiResp.data.date_epoch * 1000); 162 | messageSender(message, spoiler, iconURL, vxapitwitterEmbed, vxapitweetinfo); 163 | embedSuppresser(message); 164 | vxapiRespVideo.forEach((url) => { 165 | videoLinkSender(message, spoiler, url); 166 | }); 167 | } else { 168 | backupLinkSender(message, spoiler, `https://vxtwitter.com/i/status/${result[1]}`); 169 | embedSuppresser(message); 170 | vxapiRespVideo.filter((_url, index) => index > 0 && index < 4) 171 | .forEach((url) => { 172 | videoLinkSender(message, spoiler, url); 173 | }); 174 | } 175 | } else { 176 | const vxapitwitterEmbed = twitterEmbedMaker(vxapiResp.data.user_screen_name, 177 | '', 178 | (vxapiResp.data.user_name||'Twitter.com'), 179 | (vxapiResp.data.tweetURL||''), 180 | (vxapiResp.data.text||''), 181 | '', 182 | vxapiResp.data.date_epoch * 1000); 183 | messageSender(message, spoiler, iconURL, vxapitwitterEmbed, vxapitweetinfo); 184 | embedSuppresser(message); 185 | } 186 | } else { 187 | throw new Error('vxtwitter api timeout: '+ tid); 188 | } 189 | } catch { 190 | // throw new Error('fxvxtwitter api error: '+ tid); 191 | try { 192 | // console.log('fx vx twitter api error: '+ tid); 193 | backupLinkSender(message, spoiler, `https://fxtwitter.com/i/status/${result[1]}`); 194 | embedSuppresser(message); 195 | } catch { 196 | console.log('twitter error: '+ message.guild.name); 197 | } 198 | } 199 | } 200 | }; 201 | -------------------------------------------------------------------------------- /src/regex/handleWeiboRegex.js: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import axios from 'axios'; 3 | import * as cheerio from 'cheerio'; 4 | import { messageSender } from '../events/messageSender.js'; 5 | // import { messageSubSender } from '../events/messageSubSender.js'; 6 | import { embedSuppresser } from '../events/embedSuppresser.js'; 7 | import { typingSender } from '../events/typingSender.js'; 8 | import { messageSenderMore } from '../events/messageSenderMore.js'; 9 | 10 | export async function handleWeiboRegex( result, message, spoiler ) { 11 | const iconURL = 'https://ermiana.canaria.cc/pic/weibo.png'; 12 | typingSender(message); 13 | try { 14 | const weiboResp = await axios.request({ 15 | method: 'get', 16 | url: 'https://m.weibo.cn/statuses/show', 17 | params: { 18 | id: result[1], 19 | }, 20 | timeout: 2000, 21 | }); 22 | 23 | if (weiboResp.status === 200) { 24 | const weiboEmbed = new EmbedBuilder(); 25 | weiboEmbed.setColor(0xff0000); 26 | try { 27 | if (weiboResp.data.data.user?.screen_name) { 28 | weiboEmbed.setTitle(weiboResp.data.data.user.screen_name); 29 | weiboEmbed.setURL('https://m.weibo.cn/detail/' + result[1]); 30 | } 31 | } catch {} 32 | try { 33 | if (weiboResp.data.data.text) { 34 | const $ = cheerio.load(weiboResp.data.data.text); 35 | const cleanedText = $.text(); 36 | weiboEmbed.setDescription(cleanedText.substring(0, 300)); 37 | } 38 | } catch {} 39 | try { 40 | if (weiboResp.data.data.pics) { 41 | const match = weiboResp.data.data.pics[0].large.url.match(/https:\/\/(\w+)\.sinaimg\.cn\/(.+)/); 42 | if (match) { 43 | weiboEmbed.setImage(`https://weibo-pic.canaria.cc/${match[1]}/${match[2]}`); 44 | } 45 | } 46 | } catch {} 47 | 48 | const weiboinfo ='💬' + weiboResp.data.data.comments_count.toString() + ' 🔁' + weiboResp.data.data.reposts_count.toString() + ' ❤️' + weiboResp.data.data.attitudes_count.toString(); 49 | 50 | try { 51 | if (!weiboResp.data.data?.pics || weiboResp.data.data?.pics.length == 0) { 52 | messageSender(message, spoiler, iconURL, weiboEmbed, weiboinfo); 53 | embedSuppresser(message); 54 | } else if (weiboResp.data.data?.pics.length == 1) { 55 | messageSender(message, spoiler, iconURL, weiboEmbed, weiboinfo); 56 | embedSuppresser(message); 57 | } else if (weiboResp.data.data.pics?.length > 1) { 58 | const imageArray =[]; 59 | weiboResp.data.data.pics 60 | .filter((_pic, index) => index > 0 && index < 4) 61 | .forEach((pic) => { 62 | const match = pic.large.url.match(/https:\/\/(\w+)\.sinaimg\.cn\/(.+)/); 63 | if (match) { 64 | imageArray.push(`https://weibo-pic.canaria.cc/${match[1]}/${match[2]}`); 65 | } 66 | }); 67 | messageSenderMore(message, spoiler, iconURL, weiboEmbed, weiboinfo, imageArray); 68 | embedSuppresser(message); 69 | } 70 | } catch {} 71 | 72 | /* 73 | messageSender(message, spoiler, iconURL, weiboEmbed, weiboinfo); 74 | embedSuppresser(message); 75 | 76 | try { 77 | if (weiboResp.data.data.pics?.length > 1) { 78 | weiboResp.data.data.pics 79 | .filter((_pic, index) => index > 0 && index < 4) 80 | .forEach((pic) => { 81 | const match = pic.large.url.match(/https:\/\/(\w+)\.sinaimg\.cn\/(.+)/); 82 | if (match) { 83 | const picEmbed = new EmbedBuilder(); 84 | picEmbed.setColor(0xff0000); 85 | picEmbed.setImage(`https://weibo-pic.canaria.cc/${match[1]}/${match[2]}`); 86 | messageSubSender(message, spoiler, iconURL, picEmbed, 'ermiana'); 87 | } 88 | }); 89 | } 90 | } catch {} 91 | */ 92 | } 93 | } catch { 94 | console.log('weibo error: '+ message.guild.name); 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /src/regex/regexManager.js: -------------------------------------------------------------------------------- 1 | import { handleEhRegex } from './handleEhRegex.js'; 2 | // import { handlePttRegex } from './handlePttRegex.js'; 3 | // [/https?:\/\/www\.ptt\.cc\/bbs\/([a-zA-Z-_]+)\/(M\.[0-9]+\.A\.[0-9A-Z]+)\.html/, handlePttRegex], 4 | import { handleBahaRegex } from './handleBahaRegex.js'; 5 | import { handlePixivRegex } from './handlePixivRegex.js'; 6 | import { handlePlurkRegex } from './handlePlurkRegexV2.js'; 7 | import { handleTwitterRegex } from './handleTwitterRegexV2.js'; 8 | import { handleMisskeyRegex } from './handleMisskeyRegex.js'; 9 | import { handlePchomeRegex } from './handlePchomeRegex.js'; 10 | import { handleBlueskyRegex } from './handleBlueskyRegex.js'; 11 | // import { handleInstagramRegex } from './handleInstagramRegexV2.js'; 12 | // [/https:\/\/www\.instagram\.com\/(?:p|reel)\/([a-zA-Z0-9-_]+)/, handleInstagramRegex], 13 | // [/https:\/\/www\.instagram\.com\/[A-Za-z0-9_.]+\/(?:p|reel)\/([a-zA-Z0-9-_]+)/, handleInstagramRegex], 14 | import { handleTiktokRegex } from './handleTiktokRegexV2.js'; 15 | import { handleBilibiliRegex } from './handleBilibiliRegex.js'; 16 | // import { handleThreadsRegex } from './handleThreadsRegex.js'; 17 | // [/https:\/\/www\.threads\.net\/@[A-Za-z0-9_.]+\/post\/[a-zA-Z0-9-_]+/, handleThreadsRegex], 18 | // import { handleWeiboRegex } from './handleWeiboRegex.js'; 19 | // [/https:\/\/m\.weibo\.cn\/detail\/([0-9]+)/, handleWeiboRegex], 20 | 21 | export const regexsMap = new Map([ 22 | [/https:\/\/x\.com\/[A-Za-z0-9_]{1,15}\/status\/([0-9]+)/, handleTwitterRegex], 23 | [/https:\/\/twitter\.com\/[A-Za-z0-9_]{1,15}\/status\/([0-9]+)/, handleTwitterRegex], 24 | [/https?:\/\/m\.gamer\.com\.tw\/forum\/((?:C|Co)\.php\?bsn=60076&(?:snA|sn)=[0-9]+)/, handleBahaRegex], 25 | [/https?:\/\/forum\.gamer\.com\.tw\/((?:C|Co)\.php\?bsn=60076&(?:snA|sn)=[0-9]+)/, handleBahaRegex], 26 | [/https:\/\/www\.pixiv\.net\/artworks\/([0-9]+)/, handlePixivRegex], 27 | [/https:\/\/www\.pixiv\.net\/en\/artworks\/([0-9]+)/, handlePixivRegex], 28 | [/https:\/\/e(?:x|-)hentai\.org\/g\/([0-9]+)\/([0-9a-z]+)/, handleEhRegex], 29 | [/https:\/\/www\.plurk\.com\/m\/p\/([a-zA-Z0-9]{3,10})/, handlePlurkRegex], 30 | [/https:\/\/www\.plurk\.com\/p\/([a-zA-Z0-9]{3,10})/, handlePlurkRegex], 31 | [/https:\/\/24h\.pchome\.com\.tw\/prod\/([^?]+)/, handlePchomeRegex], 32 | [/https:\/\/bsky\.app\/profile\/([a-zA-Z0-9-.]+)\/post\/([a-zA-Z0-9]{10,16})/, handleBlueskyRegex], 33 | [/https:\/\/misskey\.io\/notes\/([a-zA-Z0-9]{10,16})/, handleMisskeyRegex], 34 | [/https:\/\/www\.tiktok\.com\/@[a-zA-Z0-9-_.]+\/video\/[0-9]+/, handleTiktokRegex], 35 | [/https:\/\/www\.bilibili\.com\/opus\/([0-9]+)/, handleBilibiliRegex], 36 | ]); 37 | 38 | export function matchRules(content) { 39 | const rules = [ 40 | /\<[\s\S]*http[\s\S]*\>/, 41 | /\~\~[\s\S]*http[\s\S]*\~\~/, 42 | ]; 43 | return rules.some((rule) => rule.test(content)); 44 | } 45 | -------------------------------------------------------------------------------- /src/reload.js: -------------------------------------------------------------------------------- 1 | import { refreshContextMenus } from './utils/refreshContextMenus.js'; 2 | 3 | refreshContextMenus(); 4 | -------------------------------------------------------------------------------- /src/utils/botLog.js: -------------------------------------------------------------------------------- 1 | import { configManager } from './configManager.js'; 2 | import { WebhookClient, EmbedBuilder } from 'discord.js'; 3 | 4 | export async function reloadLog(serverCount, totalUserCount) { 5 | try { 6 | const config = await configManager(); 7 | const webhookLink = new WebhookClient({ url: config.DCWH }); 8 | const reloadEmbed = new EmbedBuilder() 9 | .setColor(0xfff3a9) 10 | .setTitle('**【 ermiana 已重新啟動】**') 11 | .setDescription(`正在 ${serverCount} 個伺服器上運作中\n正在服務 ${totalUserCount} 位使用者`) 12 | .setTimestamp(); 13 | 14 | await webhookLink.send({ 15 | username: 'ermiana', 16 | avatarURL: 'https://cdn.discordapp.com/avatars/1078919650764652594/45d5f492295af445b65299dd6fb806b1.png', 17 | embeds: [reloadEmbed], 18 | }); 19 | } catch (error) { 20 | console.log('reloadLog error'); 21 | } 22 | } 23 | 24 | export async function guildLog(guild) { 25 | try { 26 | const config = await configManager(); 27 | const webhookLink = new WebhookClient({ url: config.DCWH }); 28 | const guildOwner = await guild.fetchOwner(); 29 | const guildIcon = guild.iconURL(); 30 | 31 | const guildCreateEmbed = new EmbedBuilder() 32 | .setColor(0xfff3a9) 33 | .setTitle('**【 ermiana 被新增至伺服器】**') 34 | .setDescription(`伺服器名稱:${guild.name} (${guild.id})\n伺服器管理員:${guildOwner.displayName} (@${guildOwner.user.username})\n伺服器總人數:${guild.memberCount}`) 35 | .setThumbnail(guildIcon) 36 | .setTimestamp(); 37 | 38 | await webhookLink.send({ 39 | username: 'ermiana', 40 | avatarURL: 'https://cdn.discordapp.com/avatars/1078919650764652594/45d5f492295af445b65299dd6fb806b1.png', 41 | embeds: [guildCreateEmbed], 42 | }); 43 | } catch (error) { 44 | console.log('guildLog error'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/configManager.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | export async function configManager() { 4 | dotenv.config(); 5 | const config = { 6 | DCTK: process.env.DCTK, 7 | DCID: process.env.DCID, 8 | DCWH: process.env.DCWH, 9 | BHUD: process.env.BHUD, 10 | BHPD: process.env.BHPD, 11 | }; 12 | return config; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/currentTime.js: -------------------------------------------------------------------------------- 1 | export async function currentTime() { 2 | const now = new Date(); 3 | const year = now.getFullYear(); 4 | const month = (now.getMonth() + 1).toString().padStart(2, '0'); 5 | const day = now.getDate().toString().padStart(2, '0'); 6 | const hours = now.getHours().toString().padStart(2, '0'); 7 | const minutes = now.getMinutes().toString().padStart(2, '0'); 8 | const seconds = now.getSeconds().toString().padStart(2, '0'); 9 | 10 | const currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; 11 | console.log(currentTime); 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/refreshContextMenus.js: -------------------------------------------------------------------------------- 1 | import { configManager } from './configManager.js'; 2 | import { ContextMenuCommandBuilder, ApplicationCommandType, REST, Routes } from 'discord.js'; 3 | 4 | export async function refreshContextMenus() { 5 | const commandData = [ 6 | new ContextMenuCommandBuilder() 7 | .setName('removeMessage') 8 | .setNameLocalizations({ 9 | 'en-GB': 'Delete BOT Message', 10 | 'en-US': 'Delete BOT Message', 11 | 'zh-TW': '刪除機器人訊息', 12 | 'zh-CN': '删除机器人信息', 13 | 'ja': 'ロボメセを削除', 14 | }) 15 | .setType(ApplicationCommandType.Message), 16 | ]; 17 | 18 | const config = await configManager(); 19 | const rest = new REST({ version: '9' }).setToken(config.DCTK); 20 | try { 21 | await rest.put( 22 | Routes.applicationCommands(config.DCID), 23 | { body: commandData }, 24 | ); 25 | console.log('Successfully reloaded Context Menus.'); 26 | } catch { 27 | console.log('Failed to reload Context Menus.'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/reloadBahaTK.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import Conf from 'conf'; 3 | import { configManager } from './configManager.js'; 4 | 5 | export async function reloadBahaTK() { 6 | const headers = { 7 | 'Content-Type': 'application/x-www-form-urlencoded', 8 | 'Cookie': 'ckAPP_VCODE=9487', 9 | }; 10 | 11 | const config = await configManager(); 12 | const data = 'uid=' + config.BHUD + '&passwd=' + config.BHPD + '&vcode=9487'; 13 | 14 | await axios.request({ 15 | method: 'post', 16 | url: 'https://api.gamer.com.tw/mobile_app/user/v3/do_login.php', 17 | headers: headers, 18 | data: data, 19 | timeout: 2500, 20 | }) 21 | .then((response) => { 22 | const ermianaBH = new Conf({ projectName: 'ermianaJS' }); 23 | const cookies = response.headers['set-cookie']; 24 | cookies.forEach((element) => { 25 | if (element.startsWith('BAHAENUR=')) { 26 | // console.log('set BAHAENUR= ' + element.split('BAHAENUR=')[1].split(';')[0]); 27 | ermianaBH.set('BAHAENUR', element.split('BAHAENUR=')[1].split(';')[0]); 28 | } 29 | if (element.startsWith('BAHARUNE=')) { 30 | // console.log('set BAHARUNE= ' + element.split('BAHARUNE=')[1].split(';')[0]); 31 | ermianaBH.set('BAHARUNE', element.split('BAHARUNE=')[1].split(';')[0]); 32 | } 33 | }); 34 | }) 35 | .catch((error) => { 36 | console.log('baha api error : ' + error); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/runCronJob.js: -------------------------------------------------------------------------------- 1 | import { CronJob } from 'cron'; 2 | import { reloadBahaTK } from './reloadBahaTK.js'; 3 | 4 | export function runCronJob() { 5 | const bahaJob = new CronJob('00 30 15 * * 0,2,4,6', function() { 6 | try { 7 | console.log('Baha Cronjob running...'); 8 | reloadBahaTK(); 9 | } catch { 10 | console.log('Failed to reload BahaTK.'); 11 | } 12 | }, null, true, 'Asia/Taipei'); 13 | console.log('Baha Cronjob set ok!'); 14 | bahaJob.start(); 15 | } 16 | -------------------------------------------------------------------------------- /workers/ermiana-count.js: -------------------------------------------------------------------------------- 1 | addEventListener('fetch', (event) => { 2 | event.respondWith(handleRequest(event.request)); 3 | }); 4 | 5 | async function handleRequest(request) { 6 | const botID = '1078919650764652594'; 7 | const response = await fetch('https://discord.com/api/v9/application-directory-static/applications/' + botID, { 8 | headers: { 9 | 'Referer': 'https://discord.com/application-directory/' + botID, 10 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0', 11 | }, 12 | }); 13 | 14 | if (!response.ok) { 15 | const errorResponse = { 16 | schemaVersion: 1, 17 | label: 'API error', 18 | message: '-1 servers', 19 | color: '7289DA', 20 | }; 21 | return new Response(JSON.stringify(errorResponse), { 22 | headers: { 'Content-Type': 'application/json' }, 23 | }); 24 | } 25 | 26 | const responseData = await response.json(); 27 | const jsonResponse = { 28 | schemaVersion: 1, 29 | label: responseData.name, 30 | message: responseData.directory_entry.guild_count.toString() + ' servers', 31 | color: '7289DA', 32 | }; 33 | return new Response(JSON.stringify(jsonResponse), { 34 | headers: { 'Content-Type': 'application/json' }, 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /workers/pixiv-proxy.js: -------------------------------------------------------------------------------- 1 | addEventListener('fetch', (event) => { 2 | event.respondWith(handleRequest(event.request)); 3 | }); 4 | 5 | async function handleRequest(request) { 6 | const url = new URL(request.url); 7 | const path = url.pathname; 8 | 9 | if (path == '/') { 10 | return Response.redirect('https://canaria.cc', 301); 11 | } else { 12 | url.hostname = 'i.pximg.net'; 13 | const imgRequest = new Request(url, request); 14 | return fetch(imgRequest, { 15 | headers: { 16 | 'Referer': 'https://www.pixiv.net/', 17 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', 18 | }, 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /workers/weibo-proxy.js: -------------------------------------------------------------------------------- 1 | addEventListener('fetch', (event) => { 2 | event.respondWith(handleRequest(event.request)); 3 | }); 4 | 5 | async function handleRequest(request) { 6 | const url = new URL(request.url); 7 | const path = url.pathname; 8 | 9 | if (path == '/') { 10 | return Response.redirect('https://canaria.cc', 301); 11 | } else { 12 | const match = path.match(/\/(\w+)\/(.+)/); 13 | if (match) { 14 | url.hostname = `${match[1]}.sinaimg.cn`; 15 | url.pathname = `/${match[2]}`; 16 | const imgRequest = new Request(url, request); 17 | return fetch(imgRequest, { 18 | headers: { 19 | 'Referer': 'https://m.weibo.cn/', 20 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', 21 | }, 22 | }); 23 | } else { 24 | return Response.redirect('https://canaria.cc', 301); 25 | } 26 | } 27 | } 28 | --------------------------------------------------------------------------------