-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 239 KB
/
content.json
1
{"pages":[{"title":"","text":"如果哪一天,有人看到一個網站, 界面很樸素,內容也不多,但是無一不是用心去完成 每一個細節都經過了精心雕刻,蘊含的思想經歷 內容深度和技術含量不亞於任何其它創作或非創作網站, 甚至隱藏了很多只有深入體會才能注意到的細節。 後來可能又發現這個網站甚至從未聲張過, 沒有任何推廣,搜索排名也很靠後。 網站本身也從未涉及到商業、 或者絕大部分網站都去做的一些世俗的事。 這時候可能發現她完全是用愛去完成的。 在這個有一點點精神或物質資源都會被迅速瓜分, 有一點點技術就會去放大 100 倍張揚的現世 她仍然低調,保持蘊含著 20 年前那種能給予觀測者很大的空間的主題思想, 和那種出淤泥而不染。 如果有人看到了她並且被感動, 如同當年改變了我一生的那個絲毫不奢華卻蘊含著非常單純的愛的網站一樣 這就是我的願望。 自述 about me 後端開發 JAVA 後端一枚,對程式興致普普通通,如果是有目標的編程則充滿興趣 動漫廚 尤其廚刀劍神域,曾經寫的 discord bot,其形象取自 SAO UW 篇的 愛麗絲·滋貝魯庫 項目 fun project discord bot - 小愛麗絲 介紹文章 邀請她 巴哈姆特 - 巴友紀錄插件 介紹文章 下載點 巴哈姆特 - IP 紀錄插件 介紹文章 下載點 Pixai 爬蟲領取獎勵 介紹文章 Github DockerHub 時光機 rememeber 聯絡我 contact請透過上方 fb 私訊,或是寄信至以下[email protected] 當今社群網站垃圾訊息繁多,請務必說明來意","link":"/about/index.html"},{"title":"友鏈","text":"申請友鏈需知 申請請提供:站點名稱、站點鏈接、站點描述、logo 或頭像(不要設置防盜鏈)。 排名不分先後,刷新後重排,更新信息後請留言告知。 會定期清理很久很久不更新的、不符合要求的友鏈。 不存儲友鏈圖片,如果友鏈圖片換了無法更新。圖片裂了的會替換成默認圖,需要更換的請留言告知。 本站友鏈信息如下,申請友鏈前請先添加本站信息:網站圖標:點我取得網站名稱:貓謎工坊網站地址:https://smilin.net網站簡介:Code · Thinking · ACG 加載中,稍等幾秒...","link":"/friend/index.html"},{"title":"留言板","text":"留言 tips:github登入後按時間可正序查看❤️ 生性懶惰,三天打魚兩天曬網但亦樂於解答疑問,有留必應程式煩惱很好、日常瑣事也罷,留下你在這造訪的足跡吧~","link":"/message/index.html"},{"title":"Privacy Policy","text":"點擊這裡查看中文隱私權政策 Last updated: May 03, 2024 This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You. We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy. This Privacy Policy has been created with the help of the Privacy Policy Generator. Interpretation and Definitions Interpretation The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural. Definitions For the purposes of this Privacy Policy: Account means a unique account created for You to access our Service or parts of our Service. Affiliate means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority. Company (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to 貓謎工坊. Cookies are small files that are placed on Your computer, mobile device or any other device by a website, containing the details of Your browsing history on that website among its many uses. Country refers to: China Device means any device that can access the Service such as a computer, a cellphone or a digital tablet. Personal Data is any information that relates to an identified or identifiable individual. Service refers to the Website. Service Provider means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used. Third-party Social Media Service refers to any website or any social network website through which a User can log in or create an account to use the Service. Usage Data refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit). Website refers to 貓謎工坊, accessible from https://smilin.net You means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable. Collecting and Using Your Personal Data Types of Data Collected Personal Data While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to: Email address Usage Data Usage Data Usage Data is collected automatically when using the Service. Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data. When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data. We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device. Information from Third-Party Social Media Services The Company allows You to create an account and log in to use the Service through the following Third-party Social Media Services: Google Facebook Instagram Twitter LinkedIn If You decide to register through or otherwise grant us access to a Third-Party Social Media Service, We may collect Personal data that is already associated with Your Third-Party Social Media Service's account, such as Your name, Your email address, Your activities or Your contact list associated with that account. You may also have the option of sharing additional information with the Company through Your Third-Party Social Media Service's account. If You choose to provide such information and Personal Data, during registration or otherwise, You are giving the Company permission to use, share, and store it in a manner consistent with this Privacy Policy. Tracking Technologies and Cookies We use Cookies and similar tracking technologies to track the activity on Our Service and store certain information. Tracking technologies used are beacons, tags, and scripts to collect and track information and to improve and analyze Our Service. The technologies We use may include: Cookies or Browser Cookies. A cookie is a small file placed on Your Device. You can instruct Your browser to refuse all Cookies or to indicate when a Cookie is being sent. However, if You do not accept Cookies, You may not be able to use some parts of our Service. Unless you have adjusted Your browser setting so that it will refuse Cookies, our Service may use Cookies. Web Beacons. Certain sections of our Service and our emails may contain small electronic files known as web beacons (also referred to as clear gifs, pixel tags, and single-pixel gifs) that permit the Company, for example, to count users who have visited those pages or opened an email and for other related website statistics (for example, recording the popularity of a certain section and verifying system and server integrity). Cookies can be "Persistent" or "Session" Cookies. Persistent Cookies remain on Your personal computer or mobile device when You go offline, while Session Cookies are deleted as soon as You close Your web browser. Learn more about cookies on the Privacy Policies website article. We use both Session and Persistent Cookies for the purposes set out below: Necessary / Essential Cookies Type: Session Cookies Administered by: Us Purpose: These Cookies are essential to provide You with services available through the Website and to enable You to use some of its features. They help to authenticate users and prevent fraudulent use of user accounts. Without these Cookies, the services that You have asked for cannot be provided, and We only use these Cookies to provide You with those services. Cookies Policy / Notice Acceptance Cookies Type: Persistent Cookies Administered by: Us Purpose: These Cookies identify if users have accepted the use of cookies on the Website. Functionality Cookies Type: Persistent Cookies Administered by: Us Purpose: These Cookies allow us to remember choices You make when You use the Website, such as remembering your login details or language preference. The purpose of these Cookies is to provide You with a more personal experience and to avoid You having to re-enter your preferences every time You use the Website. For more information about the cookies we use and your choices regarding cookies, please visit our Cookies Policy or the Cookies section of our Privacy Policy. Use of Your Personal Data The Company may use Personal Data for the following purposes: To provide and maintain our Service, including to monitor the usage of our Service. To manage Your Account: to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user. For the performance of a contract: the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service. To contact You: To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation. To provide You with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information. To manage Your requests: To attend and manage Your requests to Us. For business transfers: We may use Your information to evaluate or conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Our assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which Personal Data held by Us about our Service users is among the assets transferred. For other purposes: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience. We may share Your personal information in the following situations: With Service Providers: We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to contact You. For business transfers: We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of Our business to another company. With Affiliates: We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us. With business partners: We may share Your information with Our business partners to offer You certain products, services or promotions. With other users: when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside. If You interact with other users or register through a Third-Party Social Media Service, Your contacts on the Third-Party Social Media Service may see Your name, profile, pictures and description of Your activity. Similarly, other users will be able to view descriptions of Your activity, communicate with You and view Your profile. With Your consent: We may disclose Your personal information for any other purpose with Your consent. Retention of Your Personal Data The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies. The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods. Transfer of Your Personal Data Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to — and maintained on — computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction. Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer. The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information. Delete Your Personal Data You have the right to delete or request that We assist in deleting the Personal Data that We have collected about You. Our Service may give You the ability to delete certain information about You from within the Service. You may update, amend, or delete Your information at any time by signing in to Your Account, if you have one, and visiting the account settings section that allows you to manage Your personal information. You may also contact Us to request access to, correct, or delete any personal information that You have provided to Us. Please note, however, that We may need to retain certain information when we have a legal obligation or lawful basis to do so. Disclosure of Your Personal Data Business Transactions If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy. Law enforcement Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency). Other legal requirements The Company may disclose Your Personal Data in the good faith belief that such action is necessary to: Comply with a legal obligation Protect and defend the rights or property of the Company Prevent or investigate possible wrongdoing in connection with the Service Protect the personal safety of Users of the Service or the public Protect against legal liability Security of Your Personal Data The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security. Children's Privacy Our Service does not address anyone under the age of 13. We do not knowingly collect personally identifiable information from anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age of 13 without verification of parental consent, We take steps to remove that information from Our servers. If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information. Links to Other Websites Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit. We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services. Changes to this Privacy Policy We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page. We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the "Last updated" date at the top of this Privacy Policy. You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page. Contact Us If you have any questions about this Privacy Policy, You can contact us: By email: [email protected] By visiting this page on our website: https://smilin.net/message/","link":"/privacyPolicyEn/index.html"},{"title":"隱私權政策","text":"Click here for the English 最後更新:2024年5月3日 本隱私政策描述了我們在您使用服務時對信息的收集、使用和披露的政策和程序,並告知您有關您的隱私權利及法律如何保護您。 我們使用您的個人數據來提供和改善服務。使用該服務即表示您同意根據本隱私政策收集和使用信息。本隱私政策是借助隱私政策生成器創建的。 解釋和定義 解釋 首字母大寫的詞語具有下列條件下定義的含義。以下定義無論出現於單數還是複數形式均具有相同的含義。 定義 就本隱私政策而言: 帳戶指為您訪問我們的服務或服務部分而創建的唯一帳戶。 關聯公司指控制、被控制或與一方共同控制的實體,其中“控制”指擁有50%或更多的股份、股權或其他有權選舉董事或其他管理權力的證券。 公司(在本協議中稱為“公司”、“我們”、“我們”或“我們的”)指貓謎工坊。 Cookie是由網站放置在您的電腦、移動設備或任何其他設備上的小型文件,包含您在該網站上的瀏覽歷史詳情等多種用途。 國家指:中國 裝置指可以訪問服務的任何設備,如電腦、手機或數碼平板。 個人數據是指與已識別或可識別的個人相關的任何信息。 服務指網站。 服務提供者指代表公司處理數據的任何自然人或法人。它指代表公司提供服務的第三方公司或個人,執行與服務相關的服務或協助公司分析服務的使用方式。 第三方社交媒體服務指任何網站或任何社交網絡網站,用戶可以通過該網站登錄或創建帳戶以使用服務。 使用數據指自動收集的數據,無論是由使用服務產生的還是來自服務基礎設施本身的數據(例如,頁面訪問的持續時間)。 網站指貓謎工坊,可從https://smilin.net訪問 您指訪問或使用服務的個人,或代表該個人訪問或使用服務的公司或其他法律實體。 收集和使用您的個人數據 收集的數據類型 個人數據 在您使用我們的服務時,我們可能會要求您提供可用於聯繫或識別您的某些個人身份信息。個人身份信息可能包括但不限於: 電子郵件地址 使用數據 使用數據 使用服務時自動收集使用數據。 使用數據可能包括信息,例如您的設備的互聯網協議地址(例如IP地址)、瀏覽器類型、瀏覽器版本、您訪問的我們服務的頁面、您訪問的時間和日期、您在這些頁面上花費的時間、唯一設備標識符以及其他診斷數據。 當您通過移動設備訪問服務時,我們可能會自動收集某些信息,包括但不限於您使用的移動設備類型、您的移動設備的唯一ID、您的移動設備的IP地址、您的移動操作系統、您使用的移動互聯網瀏覽器類型、唯一設備標識符以及其他診斷數據。 當您訪問我們的服務或通過移動設備訪問服務時,我們還可能收集您的瀏覽器發送的信息。 來自第三方社交媒體服務的信息 公司允許您通過以下第三方社交媒體服務創建帳戶並登錄以使用服務: Google Facebook Instagram Twitter LinkedIn 如果您決定通過或授予我們訪問第三方社交媒體服務的許可,我們可能會收集已經與您的第三方社交媒體服務帳戶相關的個人數據,例如您的姓名、您的電子郵件地址、您的活動或您的聯繫人列表與該帳戶相關。 您還可以選擇與公司分享更多信息和個人數據。如果您選擇在註冊期間或其他時間提供此類信息和個人數據,您即授予公司使用、共享和按照本隱私政策的方式儲存它的權利。 跟蹤技術和Cookie 我們使用Cookie和類似的跟蹤技術來跟蹤我們服務的活動並存儲某些信息。使用的跟蹤技術包括信標、標籤和腳本來收集和跟蹤信息以及改進和分析我們的服務。我們使用的技術可能包括: Cookie或瀏覽器Cookie。 Cookie是放置在您設備上的小文件。您可以指示您的瀏覽器拒絕所有Cookie或指示何時發送Cookie。然而,如果您不接受Cookie,您可能無法使用我們服務的某些部分。除非您已調整瀏覽器設置以拒絕Cookie,否則我們的服務可能會使用Cookie。 網絡信標。 我們服務的某些部分和我們的電子郵件可能包含稱為網絡信標(也稱為清晰gif、像素標籤和單像素gif)的小型電子文件,例如,允許公司計算訪問這些頁面或打開電子郵件的用戶數以及其他相關網站統計(例如,記錄某個部分的受歡迎程度和驗證系統和服務器的完整性)。 Cookie可以是“持久的”或“會話的”。持久Cookie在您離線時仍然存在於您的個人計算機或移動設備上,而會話Cookie在您關閉瀏覽器時被刪除。在隱私政策網站文章中了解更多關於cookie的信息。 我們出於以下目的使用會話和持久Cookie: 必要/基本Cookie 類型:會話Cookie 由我們管理 目的:這些Cookie對於提供您通過網站請求的服務並使您能夠使用其某些功能至關重要。它們有助於認證用戶並防止用戶帳戶被欺詐使用。沒有這些Cookie,您所請求的服務將無法提供,我們僅使用這些Cookie來提供這些服務。 Cookie政策/通知接受Cookie 類型:持久Cookie 由我們管理 目的:這些Cookie用於識別用戶是否接受了網站上使用cookie。 功能性Cookie 類型:持久Cookie 由我們管理 目的:這些Cookie允許我們記住您在使用網站時所做的選擇,如記住您的登錄詳情或語言偏好。這些Cookie的目的是為您提供更個人化的體驗,並避免您每次使用網站時都需要重新輸入您的偏好設置。 有關我們使用的cookie以及您對cookie的選擇的更多信息,請訪問我們的Cookie政策或我們隱私政策的Cookie部分。 使用您的個人數據 公司可能出於以下目的使用個人數據: 提供並維護我們的服務,包括監控我們服務的使用。 管理您的帳戶:管理您作為服務用戶的註冊。您提供的個人數據可以讓您訪問服務的不同功能,這些功能可供您作為註冊用戶使用。 履行合同:開發、遵守和執行您已購買或通過服務購買的產品、物品或服務的購買合同或與我們通過服務達成的任何其他合同。 聯繫您:通過電子郵件、電話通話、短信或其他等效形式的電子通訊聯繫您,例如移動應用的推送通知,關於功能、產品或合同服務的更新或信息性通信,包括必要或合理的安全更新。 為您提供新聞、特別優惠和有關我們提供的其他類似於您已購買或詢問的商品、服務和活動的一般信息,除非您選擇不接收此類信息。 管理您的請求:受理並管理您對我們的請求。 業務轉讓:我們可能使用您的信息評估或進行合併、出售、重組、解散或其他出售或轉讓部分或全部資產的交易,無論是作為一個持續經營的企業還是作為破產、清算或類似程序的一部分,其中涉及我們服務用戶的個人數據是被轉讓的資產之一。 其他目的:我們可能出於其他目的使用您的信息,如數據分析、識別使用趨勢、確定我們的促銷活動的有效性以及評估和改進我們的服務、產品、服務、營銷和您的體驗。 我們可能在以下情況下共享您的個人信息: 與服務提供者共享:我們可能會與服務提供者共享您的個人信息,以監控和分析我們的服務使用情況,以聯繫您。 業務轉讓:在與任何合併、資產出售、融資或收購我們全部或部分業務的另一家公司的交易中,我們可能會共享或轉讓您的個人信息。 與關聯公司共享:我們可能會與我們的關聯公司共享您的信息,在這種情況下,我們將要求這些關聯公司遵守本隱私政策。關聯公司包括我們的母公司和任何其他子公司、合資企業夥伴或我們控制或與我們共同控制的其他公司。 與業務夥伴共享:我們可能會與我們的業務夥伴共享您的信息,以提供您特定的產品、服務或促銷。 與其他用戶共享:當您在公共區域與其他用戶共享個人信息或以其他方式互動時,此類信息可能被所有用戶查看並可能在外部公開分發。如果您與其他用戶互動或通過第三方社交媒體服務註冊,您的第三方社交媒體服務的聯繫人可能會看到您的名字、個人資料、照片和您的活動描述。同樣,其他用戶將能夠查看您的活動描述,與您通信並查看您的個人資料。 經您同意:我們可能會在您同意的情況下披露您的個人信息。 保留您的個人數據 公司將只保留為實現本隱私政策中列出的目的而必需的個人數據。我們將保留和使用您的個人數據,以遵守我們的法律義務(例如,如果我們根據適用法律要求保留您的數據),解決爭議,並執行我們的法律協議和政策。 公司還將保留使用數據以用於內部分析目的。使用數據通常保留較短的時間,除非該數據用於加強安全性或改善我們服務的功能,或我們有法律義務保留該數據更長時間。 轉讓您的個人數據 您的信息,包括個人數據,將在公司的運營辦公室以及處理涉及的其他地點進行處理。這意味著這些信息可能會被轉移到並保存在您的州、省、國家或其他政府管轄區以外的計算機上,那裡的數據保護法可能與您的管轄區不同。 您對本隱私政策的同意以及您提交此類信息代表您同意該轉讓。 公司將採取所有合理必要的措施,確保按照本隱私政策安全地處理您的數據,並且不會將您的個人數據轉移到沒有包括您數據安全和其他個人信息安全控制措施的組織或國家。 刪除您的個人數據 您有權刪除或要求我們協助刪除我們收集的關於您的個人數據。 我們的服務可能使您能夠從服務中刪除有關您的某些信息。 您可以隨時通過登錄您的帳戶(如果有的話)並訪問允許您管理個人信息的帳戶設置部分來更新、修改或刪除您的信息。您也可以聯繫我們要求訪問、更正或刪除您向我們提供的任何個人信息。 不過,請注意,當我們有法律義務或合法基礎需要保留某些信息時,我們可能需要保留這些信息。 披露您的個人數據 業務交易 如果公司參與合併、收購或資產出售,您的個人數據可能會被轉讓。我們將在您的個人數據被轉讓並適用不同隱私政策之前通知您。 執法 在某些情況下,如果法律要求或應公共機構(例如法院或政府機構)的有效請求,公司可能需要披露您的個人數據。 其他法律要求 公司可能因為以下理由而以善意信念披露您的個人數據: 遵守法律義務 保護和捍衛公司的權利或財產 防止或調查與服務相關的可能不當行為 保護服務用戶或公眾的人身安全 防止法律責任 保護您的個人數據安全 保護您的個人數據安全對我們很重要,但請記住,互聯網上的傳輸方法或電子存儲方法都不是100%安全的。雖然我們努力使用商業上可接受的方式來保護您的個人數據,但我們不能保證其絕對安全。 兒童隱私 我們的服務不針對13歲以下的人。我們不會故意收集13歲以下任何人的個人可識別信息。如果您是父母或監護人,並且您知道您的孩子向我們提供了個人數據,請與我們聯繫。如果我們意識到我們在未經父母同意的情況下收集了13歲以下任何人的個人數據,我們會採取步驟從我們的服務器中刪除該信息。 如果我們需要依賴同意作為處理您信息的法律依據,並且您的國家要求父母的同意,我們可能會要求您的父母在我們收集和使用該信息之前給予同意。 其他網站鏈接 我們的服務可能包含指向不由我們運營的其他網站的鏈接。如果您點擊第三方鏈接,您將被引導到該第三方的網站。我們強烈建議您查看您訪問的每個網站的隱私政策。 我們對任何第三方網站的內容、隱私政策或做法概不負責。 本隱私政策的更改 我們可能不時更新我們的隱私政策。我們將通過在此頁面上發布新的隱私政策來通知您任何更改。 我們將通過電子郵件和/或我們服務上的顯著通知,在變更生效之前讓您知道,並更新本隱私政策頂部的“最後更新”日期。 建議您定期查看本隱私政策以了解任何更改。本隱私政策的更改在其發布在此頁面上時生效。 聯繫我們 如果您對本隱私政策有任何疑問,您可以通過以下方式聯繫我們: 通過電子郵件:[email protected] 通過訪問我們的網站上的這個頁面:https://smilin.net/message/","link":"/privacyPolicy/index.html"}],"posts":[{"title":"Day1 - 甚麼是DiscordBot?","text":"DiscordBot 是…DiscordBot顧名思義就是在Discord上運作的BotDiscord是平台,Bot是機器人、也就是我們的程式 DiscordBot 可以做到…DiscordBot與一般Bot的不同正是他依附於Discord即是說,他的主要目的在於跟Discord的互動我在Discord上的行為被Bot接收->Bot再回饋結果給我 聊天,天氣預測,播放音樂,查詢訊息…廣泛來說,只要你是你會寫的東西,都可以塞進去實現(廢話) 如果我做一隻DiscordBot…Discord屬於時下流行的通訊軟體之一,不管是從事商業或是娛樂,Discord絕對是值得你嘗試接觸的 得益於Discord提供豐富的功能,讓DiscordBot可以以簡單的程式碼做到複雜的事情,不管是為了完成需求亦或是學生想練手,都會是不錯的銜接題目 製作一隻DiscordBot會很困難嗎…完全不會!製作一隻Bot是容易的,有教學更是事半功倍 接下來30天內、本文會一步步教學各位該如何製作一隻DiscordBot。","link":"/2020/09/01/12thDay1/"},{"title":"Day10 - 音樂系統(1)","text":"昨天我們把架構很籠統的說了大概今天的內容會專注在編程為主,discord.js的元素不會再多贅述可能會比較枯燥一點萬里長路始於足下,同學們加油~ 音樂總方法 新增一個音樂系統的分類,然後在底下創建MusicFunction方法一併把OnMessage事件下的音樂指令導向這個方法這樣Message事件就不會因為我們的程式碼肥大,進而影響閱讀 function MusicFunction(msg) { //將訊息內的前綴字截斷,後面的字是我們要的 const content = msg.content.substring(prefix[1].Value.length); //指定我們的間隔符號 const splitText = ' '; //用間隔符號隔開訊息 contents[0] = 指令,contents[1] = 參數 const contents = content.split(splitText); switch (contents[0]) { case 'play': //點歌&播放歌曲功能 playMusic(msg, contents); break; case 'replay': //重播當前歌曲 break; case 'np': //當前歌曲資訊 break; case 'queue': //歌曲清單 break; case 'skip': //中斷歌曲 break; case 'disconnect': //退出語音頻道並且清空歌曲清單 disconnectMusic(msg.guild.id, msg.channel.id); break; } } 按照昨天說的,我們分割字串後,用switch case把每個指令獨立出來後面指令也會function化,用呼叫的更加美觀 歌曲指令 play//?play async function playMusic(msg, contents) { //定義我們的第一個參數必需是網址 const urlED = contents[1]; try { //第一個參數不是連結就要篩選掉 if (urlED.substring(0, 4) !== 'http') return msg.reply('The link is not working.1'); //透過library判斷連結是否可運行 const validate = await ytdl.validateURL(urlED); if (!validate) return msg.reply('The link is not working.2'); //獲取歌曲資訊 const info = await ytdl.getInfo(urlED); //判斷資訊是否正常 if (info.videoDetails) { //指令下達者是否在語音頻道 if (msg.member.voice.channel) { //判斷bot是否已經連到語音頻道 是:將歌曲加入歌單 不是:進入語音頻道並且播放歌曲 if (!client.voice.connections.get(msg.guild.id)) { //將歌曲加入歌單 musicList.push(urlED); //進入語音頻道 msg.member.voice.channel.join() .then(connection => { msg.reply('來了~'); const guildID = msg.guild.id; const channelID = msg.channel.id; //播放歌曲 playMusic2(connection, guildID, channelID); }) .catch(err => { msg.reply('bot進入語音頻道時發生錯誤,請再試一次'); console.log(err, 'playMusicError2'); }) } else { //將歌曲加入歌單 musicList.push(urlED); msg.reply('已將歌曲加入歌單!'); } } else return msg.reply('請先進入頻道:3...'); } else return msg.reply('The link is not working.3'); } catch (err) { console.log(err, 'playMusicError'); } } 寫到這會發現需要一個填充歌曲列表的變數,歌曲列表應該是不管到哪一個音樂系統下都可以使用的,所以我們宣告在function外面(全域變數) //?play 遞迴函式 async function playMusic2(connection, guildID, channelID) { try { //播放前歌曲清單不能沒有網址 if (musicList.length > 0) { //設定音樂相關參數 const streamOptions = { seek: 0, volume: 0.5, Bitrate: 192000, Passes: 1, highWaterMark: 1 }; //讀取清單第一位網址 const stream = await ytdl(musicList[0], { filter: 'audioonly', quality: 'highestaudio', highWaterMark: 26214400 //25ms }) //播放歌曲,並且存入dispatcher const dispatcher = connection.play(stream, streamOptions); //監聽歌曲播放結束事件 dispatcher.on("finish", finish => { //將清單中第一首歌清除 if (musicList.length > 0) musicList.shift(); //播放歌曲 playMusic2(connection, guildID, channelID); }) } else disconnectMusic(guildID, channelID); //清空歌單並且退出語音頻道 } catch (err) { console.log(err, 'playMusic2Error'); } } 歌曲指令 disconnect//?disconnect function disconnectMusic(guildID, channelID) { try { //判斷bot是否在此群組的語音頻道 if (client.voice.connections.get(guildID)) { //清空歌曲清單 musicList = new Array(); //退出語音頻道 client.voice.connections.get(guildID).disconnect(); client.channels.fetch(channelID).then(channel => channel.send('晚安~')); } else client.channels.fetch(channelID).then(channel => channel.send('可是..我還沒進來:3')) } catch (err) { console.log(err, 'disconnectMusicError'); } } 到此,音樂系統的一個基本循環(播放->退出)就寫完了明天我們繼續完善剩下的功能 以下是今天的程式碼 //#region 全域變數 const Discord = require('discord.js'); const client = new Discord.Client(); const ytdl = require('ytdl-core'); const auth = require('./JSONHome/auth.json'); const prefix = require('./JSONHome/prefix.json'); //#endregion //#region 登入 client.login(auth.key); client.on('ready', () => { console.log(`Logged in as ${client.user.tag}!`); }); //#endregion //#region message事件入口 client.on('message', msg => { //前置判斷 try { if (!msg.guild || !msg.member) return; //訊息內不存在guild元素 = 非群組消息(私聊) if (!msg.member.user) return; //幫bot值多拉一層,判斷上層物件是否存在 if (msg.member.user.bot) return; //訊息內bot值為正 = 此消息為bot發送 } catch (err) { return; } //字串分析 try { let tempPrefix = '-1'; const prefixED = Object.keys(prefix); //前綴符號定義 prefixED.forEach(element => { if (msg.content.substring(0, prefix[element].Value.length) === prefix[element].Value) { tempPrefix = element; } }); //實作 switch (tempPrefix) { case '0': //文字回應功能 const cmd = msg.content.substring(prefix[tempPrefix].Value.length).split(' '); //以空白分割前綴以後的字串 switch (cmd[0]) { case 'ping': msg.channel.send('pong'); break; case '老婆': msg.reply('你沒有老婆!!'); break; case 'myAvatar': const avatar = GetMyAvatar(msg); if (avatar.files) msg.channel.send(`${msg.author}`, avatar).catch(err => { console.log(err) }); break; } break; case '1': //音樂指令 MusicFunction(msg); break; } } catch (err) { console.log('OnMessageError', err); } }); //#endregion //#region 音樂系統 //歌曲清單 let musicList = new Array(); function MusicFunction(msg) { //將訊息內的前綴字截斷,後面的字是我們要的 const content = msg.content.substring(prefix[1].Value.length); //指定我們的間隔符號 const splitText = ' '; //用間隔符號隔開訊息 contents[0] = 指令,contents[1] = 參數 const contents = content.split(splitText); switch (contents[0]) { case 'play': //點歌&播放歌曲功能 playMusic(msg, contents); break; case 'replay': //重播當前歌曲 break; case 'np': //當前歌曲資訊 break; case 'queue': //歌曲清單 break; case 'skip': //中斷歌曲 break; case 'disconnect': //退出語音頻道並且清空歌曲清單 disconnectMusic(msg.guild.id, msg.channel.id); break; } } //?play async function playMusic(msg, contents) { //定義我們的第一個參數必需是網址 const urlED = contents[1]; try { //第一個參數不是連結就要篩選掉 if (urlED.substring(0, 4) !== 'http') return msg.reply('The link is not working.1'); //透過library判斷連結是否可運行 const validate = await ytdl.validateURL(urlED); if (!validate) return msg.reply('The link is not working.2'); //獲取歌曲資訊 const info = await ytdl.getInfo(urlED); //判斷資訊是否正常 if (info.videoDetails) { //指令下達者是否在語音頻道 if (msg.member.voice.channel) { //判斷bot是否已經連到語音頻道 是:將歌曲加入歌單 不是:進入語音頻道並且播放歌曲 if (!client.voice.connections.get(msg.guild.id)) { //將歌曲加入歌單 musicList.push(urlED); //進入語音頻道 msg.member.voice.channel.join() .then(connection => { msg.reply('來了~'); const guildID = msg.guild.id; const channelID = msg.channel.id; //播放歌曲 playMusic2(connection, guildID, channelID); }) .catch(err => { msg.reply('bot進入語音頻道時發生錯誤,請再試一次'); console.log(err, 'playMusicError2'); }) } else { //將歌曲加入歌單 musicList.push(urlED); msg.reply('已將歌曲加入歌單!'); } } else return msg.reply('請先進入頻道:3...'); } else return msg.reply('The link is not working.3'); } catch (err) { console.log(err, 'playMusicError'); } } //?play 遞迴函式 async function playMusic2(connection, guildID, channelID) { try { //播放前歌曲清單不能沒有網址 if (musicList.length > 0) { //設定音樂相關參數 const streamOptions = { seek: 0, volume: 0.5, Bitrate: 192000, Passes: 1, highWaterMark: 1 }; //讀取清單第一位網址 const stream = await ytdl(musicList[0], { filter: 'audioonly', quality: 'highestaudio', highWaterMark: 26214400 //25ms }) //播放歌曲,並且存入dispatcher const dispatcher = connection.play(stream, streamOptions); //監聽歌曲播放結束事件 dispatcher.on("finish", finish => { //將清單中第一首歌清除 if (musicList.length > 0) musicList.shift(); //播放歌曲 playMusic2(connection, guildID, channelID); }) } else disconnectMusic(guildID, channelID); //清空歌單並且退出語音頻道 } catch (err) { console.log(err, 'playMusic2Error'); } } //?disconnect function disconnectMusic(guildID, channelID) { try { //判斷bot是否在此群組的語音頻道 if (client.voice.connections.get(guildID)) { //清空歌曲清單 musicList = new Array(); //退出語音頻道 client.voice.connections.get(guildID).disconnect(); client.channels.fetch(channelID).then(channel => channel.send('晚安~')); } else client.channels.fetch(channelID).then(channel => channel.send('可是..我還沒進來:3')) } catch (err) { console.log(err, 'disconnectMusicError'); } } //#endregion //#region 子類方法 //獲取頭像 function GetMyAvatar(msg) { try { return { files: [{ attachment: msg.author.displayAvatarURL('png', true), name: 'avatar.jpg' }] }; } catch (err) { console.log('GetMyAvatar,Error'); } } //#endregion","link":"/2020/09/10/12thDay10/"},{"title":"Day11 - 音樂系統(2)","text":"今天我們把剩下的功能做完 繼續做之前,我們回顧一下音樂播放的其中一小段 在playMusic2這段,我們將音樂網址與相關設定打入connection後,connection開始播放歌曲,並且返還控制項;我們宣告一個dispatcher來接收控制項,並且在下一行監聽finish事件 從這一段會注意到他解決了一件事情,那就是我們該怎麼監測歌曲播放的狀態? dispatcher這個物件在被賦予後,可以直接當成是我們的遙控器不管是監聽歌曲是不是播完了,還是調整音量&咖歌等,都會需要調用dispatcher 所以如果之後我們要繼續實作咖歌&循環播放等功能,除了需要調用歌曲清單以外,還需要調用到dispatcher 這邊在音樂系統的最上方,將dispatcher宣告成全域變數並且記得原本宣告dispatcher的const要拿掉! 觀念說完,那我們繼續: 中斷歌曲//?skip function skipMusic() { //將歌曲關閉,觸發finish事件 if (dispatcher !== undefined) dispatcher.end(); } 重播歌曲//?replay function replayMusic() { if (musicList.length > 0) { //把當前曲目再推一個到最前面 musicList.unshift(musicList[0]); //將歌曲關閉,觸發finish事件 //finish事件將清單第一首歌排出,然後繼續播放下一首 if (dispatcher !== undefined) dispatcher.end(); } } 顯示歌曲清單//?queue async function queueShow(channelID) { try { if (musicList.length > 0) { let info; let message = ''; for (i = 0; i < musicList.length; i++) { //從連結中獲取歌曲資訊 標題 總長度等 info = await ytdl.getInfo(musicList[i]); //歌曲標題 title = info.videoDetails.title; //串字串 message = message + `\\n${i+1}. ${title}`; } //把最前面的\\n拿掉 message = message.substring(1, message.length); client.channels.fetch(channelID).then(channel => channel.send(message)) } } catch (err) { console.log(err, 'queueShowError'); } } 顯示當前歌曲//?np async function nowPlayMusic(channelID) { try { if (dispatcher !== undefined && musicList.length > 0) { //從連結中獲取歌曲資訊 標題 總長度等 const info = await ytdl.getInfo(musicList[0]); //歌曲標題 const title = info.videoDetails.title; //歌曲全長(s) const songLength = info.videoDetails.lengthSeconds; //當前播放時間(ms) const nowSongLength = Math.floor(dispatcher.streamTime / 1000); //串字串 const message = `${title}\\n${streamString(songLength,nowSongLength)}`; client.channels.fetch(channelID).then(channel => channel.send(message)) } } catch (err) { console.log(err, 'nowPlayMusicError'); } } //▬▬▬▬▬▬▬▬▬?▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ function streamString(songLength, nowSongLength) { let mainText = '?'; const secondText = '▬'; const whereMain = Math.floor((nowSongLength / songLength) * 100); let message = ''; for (i = 1; i <= 30; i++) { if (i * 3.3 + 1 >= whereMain) { message = message + mainText; mainText = secondText; } else { message = message + secondText; } } return message; } 這樣,我們的音樂系統的基本教學就告一段落 其實音樂功能除了本篇教學的基本以外,還有很多花樣可以玩這個寫法有個很大的問題是,有多個群組同時在使用時,機器人的歌單會掛掉 筆者明天想先說其他主題,音樂系統的教學先告一段落因為繼續完善下去的話,程式的可讀性會降低,現在的音樂系統筆者認為是最純粹,最好理解的了同學們不訪想想該怎麼完善這些問題,我們明天見~","link":"/2020/09/11/12thDay11/"},{"title":"Day13 - 嵌入式訊息embed與 bot 的指令表(額外)","text":"今天說說Discord的另一種訊息方式,embed 在Discord.js中被稱為MessageEmbed,訊息嵌入元素,總之就是嵌入式訊息 跟一般傳送訊息的手段一樣是使用 send ,但是傳送的屬性從原本的文字(string)轉成是嵌入元素(Embed) embed宣告後,透過其文檔底下的各個方法(methods)來賦予嵌入式訊息本身,要放入哪些資訊 將一個一個訊息嵌入embed元素後,最後從send方法傳送到discord上,就是一則embed訊息了 底下附上標準版的embed,請各位嘗試在最初教學的 ! 方法中新增一則help觸發句,將底下範例套入後,試著替換成自己的指令文檔吧! const embed = new Discord.MessageEmbed() .setColor('#0099ff') .setTitle('Some title') .setURL('https://discord.js.org/') .setAuthor('Some name', 'https://i.imgur.com/wSTFkRM.png', 'https://discord.js.org') .setDescription('Some description here') .setThumbnail('https://i.imgur.com/wSTFkRM.png') .addField('Regular field title', 'Some value here') .addField('\\u200B', '\\u200B') .addField('Inline field title', 'Some value here', true) .addField('Inline field title', 'Some value here', true) .addField('Inline field title', 'Some value here', true) .setImage('https://i.imgur.com/wSTFkRM.png') .setTimestamp() .setFooter('Some footer text here', 'https://i.imgur.com/wSTFkRM.png'); msg.channel.send(embed);","link":"/2020/09/13/12thDay13/"},{"title":"Day14 - Heroku與Git,介紹與安裝","text":"好勒,上禮拜我們終於把基本的音樂系統做好現在你的機器人支援多種功能,加上你自己做的其他系統,已經是一個強大的機器人了! 之後如果我要使用機器人,只要打開cmd運行node bot,然後電腦一直開著就好 一直開著就好 自己不在其他人就沒辦法使用bot 程序如果掛掉要自已重啟 其實這就是將自己的電腦做為後端主機在運行bot而這方法對於平常沒有長時間開機習慣的使用者來說是略為有些負擔了就算是順便開著也要擔心程式會不會自己崩潰跳閘 也因此,今天我們就會來解決這個問題,同時也是終於能提到我們的主題啦 後端0負擔一個面向使用者的服務都會有一個前端視覺介面(Discord)一個後端(bot.js)再在後端的後面獨立出一個資料庫主機就更好了(SQL) 後端0負擔的意思是我們不必再支付運行程序的電費等維護費用那麼該怎麼做才能實現呢?其實也很簡單,我們把程序給其他人的電腦跑,讓他幫你管就好,也就是 雲端託管 Heroku官方網站 Heroku是支援多語言的全自動託管平台只要將你的程序放上去並且做好相關設定,程序就會在他們的主機上自動運行 註冊Heroku帳號請先到這個網址註冊帳號資訊 First name姓Last name名Email Address信箱Role職業Primary Development Language開發語言 基本上只要信箱跟開發語言沒打錯選錯就好,填好後我們直接送出 到這個步驟就代表前面填的資料ok,請去收信 點擊信中的連結 這邊他要你重複輸入密碼,需8碼,由英數符號組成 一路確認後就會來到這裡了這裡是heroku的後台,左側是創建我們的應用程序專案,右側是成立小組 不過我們通常會透過 Heroku CLI 來操作,所以不用管這兩個東東,我們繼續做 安裝Heroku CLI點擊這個網址 按照你的作業系統安裝對應的版本,通常是Windows 64-bit 一路next就好,最後Close 最後我們開啟終端機(cmd),注意必須是重新開啟的cmd,安裝前已經開啟的不行在上面輸入heroku 只要出現類似這樣的畫面就代表安裝成功了如果沒有成功出現,可以先重開機看看,再不然可能是你的安裝版本有錯,電腦不是64位元等 成功的話我們繼續輸入 heroku login 在鍵盤上輸入除了q以外的按鍵 = 同意他開啟瀏覽器提供登入 chrome會自動打開,因為我們前面剛在heroku註冊,所以網頁還存著登入訊息,直接按Log In,回頭看cmd就會顯示登入成功了! 這樣Heroku CLI就裝好了,我們再來安裝Git Git安裝Git是一種提供程式版本控制的語言,最常見的例子是GitHub透過Git語言,我們可以將專案製作成Git專案,然後透過Git將程式推送到雲端,並且保證每個版本間的修改與來源都得到紀錄 Heroku正是依循Git的規則來上傳下載程式 點擊這個網址 一樣選擇對應的版本安裝時的選項很多,請跟我底下的圖片選擇一樣 路徑跟我不一樣沒關係,總之要照他預設給你的,不要改 這樣就安裝好Git了,跟剛剛一樣,我們開一個新的cmd測試是否安裝成功 失敗的話,除了版本問題以外,有可能是PATH檔不正確,可以拿這個keyword去排除問題試試看 都沒問題的話,我們輸入以下兩個指令 git config –global user.name “smile”git config –global user.email “[email protected]“ 這是設定你的使用者名稱與信箱我的名稱是smile,你要改成你自己的稱呼,信箱與heroku一樣就可以了 之後透過git推送時,都會以這個稱呼與信箱來識別這樣Heroku跟Git的前置作業都做好了,明天將專案推上Heroku","link":"/2020/09/14/12thDay14/"},{"title":"Day12 - Discord的訊息刪除與更新事件(額外)","text":"欸…原本接下來想教後台託管與資料庫的教學的但因為筆者禮拜五把文稿跟圖片還有範例程式全都留在公司了…所以這兩天筆者先教一下其他東西 就當作是惡補前面沒講到的Discord.js功能,我們今天來說說Discord.js的其他事件監聽吧 刪除與更新事件目前我們的所有事件都是建立在使用者發送訊息 -> Discord廣播給機器人 -> 機器人的Message事件觸發 client.on('message', msg => {}) 我們的 bot 是使用登入了 bot 自身 keyValue 的 client 來監聽 message 事件的 這樣,我們在Client下的Event分類中,就會找的到message的內容 在程序中Client代表我們機器人本身,他的Event直接表示了他能對那些事情做出反應也因此,Client的Event功能非常的多 今天我們來說說MessageDelete跟MessageUpdate事件,也就是當有人刪除訊息跟更新訊息的時要做的事情 Discord.js的文庫其實也蠻簡單的,不用看那些英文在說甚麼,透過事件名稱跟屬性已經足夠我們知道當有人刪除留言時,Discord會將Message返還給我們當有人更新留言時,Discord會將oldMessage跟newMessage 請試著用console.log查看Discord分別給了我們甚麼內容吧! 之後我們把屬性中我們需要的元素拿出來組成字串,發送到原本的頻道去 //抓刪 刪除事件 client.on('messageDelete', function (message) { if (!message.guild) return; //只要是來自群組的訊息 let mStr = ''; try{ mStr = mStr+ `事件 刪除\\n`+ `使用者 ${message.member.user.username}\\n`+ `群組 ${message.channel.name}\\n`+ `刪除內容 ${message.content}`; client.channels.get(message.channel.id).send(mStr); }catch(err){ console.log("messageDeleteError",err); } }); //抓刪 更新事件 client.on('messageUpdate', function (oldMessage, newMessage) { if (!oldMessage.guild || !newMessage.guild) return; mStr = ''; try { mStr = mStr + `事件 更新\\n` + `使用者 ${oldMessage.member.user.username}\\n` + `群組 ${oldMessage.channel.name}\\n` + `舊對話 ${oldMessage.content}\\n` + `新對話 ${newMessage.content}`; client.channels.get(oldMessage.channel.id).send(mStr); } catch (err) { console.log('messageUpdateError', err); } }) 這樣,你的bot就會在頻道有人刪除訊息時接收訊息,然後把刪除的內容貼出來玩羞恥play了(","link":"/2020/09/12/12thDay12/"},{"title":"Day15 - Heroku架設&細部設定","text":"早安昨天我們把Heroku跟Git都安裝好了,今天我們要將我們的專案變成Git專案並且推上Heroku 第一次推送專案首先,跟之前一樣我們先用VSCode開啟專案資料夾 確定終端機路徑正確後,輸入git init 左側的檔案都變得綠綠的如果你有看的到隱藏資料夾的話,在專案底下還會看到一個.git的隱藏資料夾那就是用來識別git專案的檔案,也是執行git語言時的依據 這樣專案就轉換成git專案了,然後我們接下來要在heroku上創建一個專案庫專案名稱的命名方式大多為 aaa-bbb-ccc第一個字必須為小寫,且不能與其他在heroku上的專案撞名,所以越獨特越好這邊筆者的專案名稱取做 the-bot-alice-on-heroku我們輸入heroku create 你的專案名稱 這時候如果到這裡看,成功的話就會看到剛剛創建的專案了https://dashboard.heroku.com/ 我們繼續輸入 git add .git commit -m initgit push heroku master 最後有看到Build succeeded就是成功了!這三個指令的意思依序是 git add .將目錄下的所有檔案加入git控管 git commit -m init將git控管下的檔案全部加入本次要推送的版本,因為是第一次推送所以是init,之後第n次推送的話程式碼要改成 git commit -m ‘版本說明’ git push heroku master將commit起來的版本推送(上傳)到heroku的主分支,總之就是上傳檔案 所以之後如果要更新程式,都是依序使用這三個命令來上傳機器人 接著我們運行看看heroku logs –t 這是呼叫heroku的控制後台,相當於在看遠端主機運行程序時的終端機要退出時按ctrl+c退出 可以看到對方也接收到我們上傳的檔案,有一個Build succeeded但是後面都是npm ERR以及可以看到他嘗試使用 npm start 來運行我們的專案還記得我們的程序啟動試透過甚麼方式嗎?沒錯,node bot! 平台設定我們在專案的根目錄新增一個檔案,取名叫做Procfile 裡面只有一行,寫上worker: node bot 可以看到,檔案在取名為Procfile後,icon會變成Heroku的標誌這是Heroku可以辨識的檔案,作用是指定Heroku的啟動命令worker是代表我們要運行的啟動命令是worker(程序)heroku預設的啟動命令屬於web(網頁) 寫好後,我們再做一次推送git add .git commit -m ‘npm bot’git push heroku master push完,我們打開網頁,回到heroku控制台,進入我們的專案 會看到,紅框處是我們的推送日誌,這也是用git來控管程式時的日誌檔 藍框就顯示了我們程序的啟動方式,可以看到node bot已經出現在上面了,但是沒有開啟 我們點擊紫框的Resources 將 npm start給關閉,node bot開啟,這樣回去主頁面看node bot也會是開啟的狀態了 最後我們在終端機輸入 heroku restart 這是重啟專案的指令,當程序崩潰或是遇到任何問題時,都可以重啟看看 heroku logs -t 成功!到此為止,我們成功將Bot推送到Heroku上,以後不用我們自己運行機器人也會是24小時運行了,給做到這邊的自己一個鼓勵吧^^ 其他設定Heroku的免費時數預設是550小時/月,如果說是自己幾個朋友使用是沒關係當機器人在一個人數比較多的群組時,550小時是沒辦法支撐1個月的,每到月底、機器人就會想休息幾天 但是!如果你有在Heroku的帳戶綁定信用卡的話,他會免費再給你450小時,每個月的免費時數會變成1000小時,運行機器人是完全足夠的!不用擔心會扣款 我們點選右上角的設定 點Billing,然後點Add credit card 再來就是綁卡流程,相信這部份不用截圖,各位自己會的 綁完後,底下原本的550小時就會變成有1000小時的扣打了~","link":"/2020/09/15/12thDay15/"},{"title":"Day16 - 選擇你的資料庫","text":"昨天我們成功把機器人丟到雲端託管,現在機器人可以.. 24小時不關機的回應大家的文字 24小時不關機的播放音樂 24小時不關機的做任何你已經自己寫好的原創功能! 也就是說,原本機器人是前端(Discord)與後端(你的電腦) 現在是前端(Discord)與後端(Heroku)了,儼然是一個自成循環的軟體,自行運作只要不是要更新以及沒有重大bug的話,便可以減少維護,只要當一個使用者就好 不過這樣的架構實際上是不夠完整的,Day14我們有帶到,關於面向使用者的軟體,通常會有 前端 後端 與資料庫 目前我們還缺乏一個資料庫,如果是之前在本機的情況,本文章只要教各位怎麼安裝MySQL就好;但是現在機器人已經被推上Heroku了,我們對對方的主機又沒有完全的控制權限,這又該怎麼辦呢? Heroku的資料庫我們來介紹Heroku的資料庫吧因為這東西筆者的了解也不深,只是有必要提到如果介紹有錯還望各位指正,以免誤導到人,感謝^^” Heroku不只提供軟體&網站的上傳發佈,同時也支援這些程式在Heroku專案的主機上建立專屬的本地資料庫其主要使用PostgreSQL,是一種關聯性資料庫關聯性資料庫是甚麼?你當成MySQL就好,總之就是相對嚴謹(麻煩)的一種資料庫語言 免費的Heroku Postgre提供1萬則的資料容量,只要你創建的DB總資料量超過這個數字後便無法寫入,並且會跟你收費以提高額度來支援更多的資料寫入量 1萬這個數字說大不大說小不小,雖然說對於個人戶是夠用了,但如果長久維運,缺乏資料管理、造成資料堆積又或是資料儲存的方式比較複雜,紀錄一次可能就會占用許多空間的話…實際上筆者覺得1萬行是完全不夠Bot使用的 而且因為Heroku的操作都必須透過指令,只是程式的操作可能還好,在處理資料庫這種複雜的系統時,不自己寫一個圖形介面真的很難管理 替代方案結合上述,我們整理出Heroku 原生資料庫的兩大問題 免費容量低 管理困難 筆者Bot的製作訴求是自動、便利、好維護如果使用Heroku資料庫的話還要擔心或許哪天Heroku會忽然想不開,去更新他們的資料庫語法…這與筆者的需求是完全背道而馳的 因此,我們需要一個替代方案,這個做法就是GoogleAppsScript ! GoogleAppsScript,簡稱GAS,不是容易爆炸的那個 GAS是Google提供的一種程式檔,他使用JavaScript語言,並且自動引用Google提供的函式庫 因為是Google自家產品,理所當然的,GAS可以輕鬆的調用Google旗下產品的資訊(例如Google文件,試算表等) 並且GAS是一種存於Google雲端的程式,只要他正式發佈便也會是24小時自動運作的程序,不會消耗到筆者一毛錢!(重點) GAS的規格 在本主題,我們將用試算表來儲存資料藉由GAS來編寫API,供Bot調用&獲取資料 上圖是GAS提供給使用者做各種操作的具體上限,我們看第一行的基本使用者(gmail.com)即可 Properties read/write(屬性讀寫)上限50000筆/天 URL Fetch calls(URL調用)上限20000筆/天 先看URL Fetch calls,這句的意思是關於Bot一天內調用API的上限次數如果Bot一天內對API的請求超過2萬次,Google就會中斷你的請求、並且給你一個大大的Error Properties read/write代表的是你的程序對試算表讀寫的次數上限如果我的API被呼叫一次,程序就會對試算表做50000次的讀取,那這個API只要調用一次,今天的扣打就用完了(x 這樣,我們得出一個結論理論上,只要我的每個API內讀寫資料的次數平均介於2~3次那我們一天的資料請求上限就會足足有2萬筆,直接是Heroku總上限的兩倍! 當然這種做法也有他的弊端,Heroku的上限是總儲存上限,而我們的GAS上限是總調用上限雖然隔天次數就會給你加滿,但只要我調用的次數足夠頻繁,理論上每天都會面臨資料無法再調用的問題,對於如何減少機器人的資料調用會需要做些功夫 遠水救不了近火,試算表資料的調用途中經過GAS,再從住在Heroku的bot程序發送API跟GAS要資料速度理所當然沒有像使用Heroku Postgre一樣即時 因為試算表非正統SQL,SQL是一種專門為讀/寫資料做優化的體系;當資料量大到一個程度時,效能一定沒有SQL好 但同時GAS的好處也很明顯 資料的儲存幾乎是無上限的Google自帶的使用者介面讓管理十分容易,相對不用擔心資料備份等問題新增刪除資料的方式都會比Heroku Postgre來的更加直觀不用擔心未來因為GoogleAppsScript改版,資料會很難帶走,替代方案也多的是 以上,便是筆者針對資料庫方案取捨的介紹明天我們著手開始編寫基本的試算表資料跟GAS","link":"/2020/09/16/12thDay16/"},{"title":"Day17 - GAS抓表(1)","text":"今天我們來寫GAS 文件創建首先請打開google,登入你自己的google雲端,並且在喜歡的地方創建一個資料夾,取名為DiscordDataBaseApi 右鍵分別創建一個試算表跟一個GAS 兩個文件創立後都會直接開一個新視窗Excel取名為MyData,像我們一樣隨便輸入一些資料然後複製網址上面d/ 到 /edit 中間的亂碼,我的是1mQ6qTJfOs3Gv5–K9r87w56wmDc3hUcpHm5hF1YKTms 這串英數是我們Excel的ID,只要把這串亂碼給Google看,他就知道你找的是這張表 我們回到 GAS ,填入以下程式 var id = '你的表格ID'; //抓取表單 var spreadsheet = SpreadsheetApp.openById(id); // Sheet id var sheet = spreadsheet.getSheets()[0]; // 要第幾個sheet? 0 就是第一個 var rowLength = sheet.getLastRow()-1; //取行長度 var columnLength = sheet.getLastColumn(); //取列長度 var data = sheet.getRange(2,1,rowLength,columnLength).getValues(); // 取得的資料 把剛剛的id給GAS抓表,抓到表後讀取行列範圍,然後從範圍中抓取資料那一長串程式碼全都是google提供的function,雖然好像有library可以看,筆者是建議記下來就好 我們範圍是從第2行開始抓取資料到最底,這是因為我們表格的第一行屬於標題行,給使用者看的,我們就不抓取這行資料 再來幫我新增這幾行資料 var dataExport = {}; for(i in data){ if(data[i][0] != ""){ dataExport[data[i][0]] = { DATE: data[i][1], NAME: data[i][2], VALUE: data[i][3] } } } 如果對json操作有一點經驗的同學,對這幾行應該不陌生把data中的資料一個個串成json,這樣才方便我們做後續處理 最後加上這行 var dataExportFormat = JSON.stringify(dataExport); return ContentService.createTextOutput(dataExportFormat).setMimeType(ContentService.MimeType.JSON); 把整理好的資料return回去GAS回傳需要先經手過他們的方法,API接收到的資料才是正常的這部份當成return dataExport就可以了。 發布表格都填完,GAS都寫好後,我們就能抓取到表格資料了但是程式該怎麼訪問GAS呢?為了讓程式可以訪問到GAS,我們需要將GAS做成API確認程式存檔後,幫我點左上角發布->部屬為網頁應用程式 會有三個下拉框出現1. 是版本號2. 發布者3. 存取權限 因為是第一次發布,版本一定是1發布者是你就好不要動這邊要注意第三點,務必設定成Anyone,even anonymous確定無誤後我們點擊Deploy,他會要你核對權限,一路允許就好 都完成後,我們最後會拿到一組連結,我們試著將連結放在瀏覽器上 如果成功就會像筆者這樣拿到表格上的檔案了!","link":"/2020/09/17/12thDay17/"},{"title":"Day18 - GAS抓表(2)","text":"昨天我們在雲端上建好了試算表將GAS發布成API的GET方法,只要我們訪問就能成功獲取試算表資料 今天我們來寫點程式 我們來回顧一下,目前機器人在Message下可以做到的事情 文字回應 音樂系統 我們希望增加一個可以針對表格內容,動態觸發回應的功能這種功能因為不會直接知道有哪些指令,應該要是沒有前綴字的,只要字串符合就會觸發 由此可知,我們應該要將這個方法添加在所有功能的最底層只有當前綴字都不符合時,才會來辨識表格資料 觀念大致帶過,我們開始動手 先幫我在專案目錄下建立一個Script資料夾,在裡面放一個GetGas.js //#region 全域引用 const auth = require('../JSONHome/auth.json'); const request = require('request'); //#endregion 裡面請先幫我引用auth.json跟request auth目前只有存放機器人的key,跟key一樣,我們不希望自己與GAS串接的API暴露&寫死在程式裡面,所以之後要把連結寫在auth,之後透過auth來讀取連結務必注意引用auth的路徑比bot.js多了一個點,這是因為GetGas.js要先從Script路徑出來才找的到JSONHome。 request是提供給js的網路請求library,我們之後都會透過他來傳遞Get方法 auth.json目前的樣子 在原本的Key後面加上一個逗號,然後新增Gas參數,內涵一個JsonObject{}{}裡面再包一個Get參數,內涵一個JsonArray[]第一個JsonObject內包一個baseExcel參數baseExcel參數會帶回我們昨天做的API連結 包三層是為了增加程式含意,方便之後閱讀跟Get同一層之後可以再添加post等不過Get原本是包JsonObject就好,這邊為了多介紹JsonArray所以用了,原本就會的同學可以少包Array 這種架構下,如果我們要獲取Url就會是auth.Gas.Get[0].baseExcel //#region 宣告請求 const baseExcel = { 'method': 'GET', 'url': auth.Gas.Get[0].baseExcel, 'headers': {} }; //#endregion 再來我們宣告一個baseExcel常數,將http請求需要的參數帶給他method表示我們使用的是Get方法url就帶我們剛剛寫好的urlheaders是傳送時的表頭,這邊放空值就好 //#region 傳送請求 exports.getBaseExcel = function(userTalk, callback) { let backValue = new Array; request(baseExcel, function(error, response) { try { if (error) { callback(error); } else { const data = JSON.parse(response.body); //接收回傳(response)的body const keysValue = Object.keys(data); //將JsonObject的key值輸出成Array //迴圈判斷是否符合 for (let i = 0; i < keysValue.length; i++) { if (data[keysValue[i]].NAME === userTalk) { callback(data[keysValue[i]].VALUE); //正確回傳結果 } } callback(false); } } catch (err) { console.log('getBaseExcelError', err); callback('getBaseExcelError'); } }); }; //#endregion 最後我們實際創建一個callback方法,供外部調用http請求後,將回傳值定義為JSON給data之後讓data跑迴圈,判斷message是否與表格的NAME欄相符叫到名字的話,機器人就要回傳VALUE值 這樣我們就把GetGas.js做好了,剩下bot.js呼叫與傳送訊息的部分我們明天繼續","link":"/2020/09/18/12thDay18/"},{"title":"Day19 - GAS抓表(3)","text":"昨天我們的程式成功抓到API的資料並且對他做分析了現在要串回主程序(bot.js)上 請幫我在bot.js引用GetGas.js 然後之前的文字回應系統,跟音樂系統一樣用一個function包起來,比較好看 我們希望當訊息不符合任一前綴系統的情況,就要拿字串跟資料庫比對所以我們把function放在default(默認),只要前面的case都沒進去就會到default 之後新增BaseExcelFunction方法內容是執行GetGas底下的getBaseExcel元素 回來看GetGas的getBaseExcelgetBaseExcel元素指向一callback方法帶了一個參數userTalk,callback方法使用callback代表方法的結束,呼叫方會在callback欄位宣告function,其帶回參數(messageED)就是getBaseExcel的方法中callback的值 這樣寫完,機器人就能做簡單的回話了! 雖然功能做好了,但有許多問題 依嚴重性依序列舉的話 bot每從discord收到一則訊息就會使用一次API 使用JsonObject做迴圈查詢十分沒有效率 缺乏防呆&參數替換 理論上,機器人對對應的詞句回話這個動作是即時的,透過這個寫法,我們每次查詢API都必須等待2~3秒的時間,API才會將結果反饋給bot而且多次傳送API不僅降低了程序的穩定,也要考慮GAS提供的每日配額如果bot所在的群組一天訊息超過2萬筆,API就一定會被花光,而一天兩萬筆訊息、對於一個支援多群組的bot來說其實並不困難 GetGas中對於data的處理方式也是極其低效的,雖然在捨棄SQL這種專為優化資料存取的系統時,就難以追求最高效的方法,但目前的做法也仍是相對低效的 以及程序目前只是簡單的判A給B,功能十分單一我沒辦法針對特定群組,有該群組專屬的詞彙,或是對特定回應帶tag等 明天我們會將程序做一次翻新","link":"/2020/09/19/12thDay19/"},{"title":"Day2 - DiscordBot - 小愛麗絲","text":"在今天的文章開始前,想先跟大家介紹我家的女兒 我是連結 小愛麗絲,生日是 2020 年 6 月 1 日 明明感覺已經做了很久,其實是六月時才開始做到現在 興趣是吐槽別人,喜歡唱歌,希望大家能多多認識她 這篇鐵人賽的文章內容基本會依照小愛麗絲現有的功能做講解,需要範例時可以參考看看~ 那麼,今天主要是要來講講本次鐵人賽的大鋼 關於一個機器人,為什麼想要做機器人,該做哪些功能,以及後台零負擔是甚麼意思等 哼哼,很好奇吧 這麼重要的問題就要從最開始講起,各位客官且聽我娓娓道來 程式的產生是建立在需求上的,就像人需要吃早餐所以這世界上有了早餐店 我希望可以製作一隻 Bot 供我認識的人們娛樂,所以小愛麗絲誕生了 最初,小愛只會針對 2~3 個句子回應固定的答案 想供大家娛樂->小愛學會說話 大家希望可以教小愛說話,不再是我自己新增,小愛開始學會不同的詞 出現了希望新增詞彙的聲音->所以小愛開始背單字 跟朋友的認識是圍繞著某款遊戲,為了讓查詢遊戲資料方便,希望小愛能幫忙查表 想在聊天時可以直接找資料->增加爬蟲功能 因為後來指令增加,為了方便閱讀所以增加說明書 其他還有音樂,圖片,權限控制等等… 舉了些例子,想說的是、機器人的功能是多樣化的,想做甚麼取決於需求;本文會圍繞著 Node.js 該如何製作一隻機器人為主軸,分享目前的程式寫法&講解以及如何做到 24HR 的主機&資料庫託管等 礙於篇幅限制,之後文章沒辦法把機器人的功能一個一個寫成教學,有特別希望學甚麼的話再問看看吧~ 最後,希望各位有空可以多跟小愛聊天,我們明天見~","link":"/2020/09/02/12thDay2/"},{"title":"Day20 - GAS抓表(4)","text":"昨天我們成功把API跟程式做了連結,並且可以在dc使用 但從GAS到程序寫法都存在問題,今天筆者會一一修正 首先請開啟GAS,這是我們目前的樣子 為了讓搜尋方式從JSONObject轉成JSONArray,第9~19行請改寫如下 重點在於宣告dataExport的時候,從{}變成了[]這就是JSONObject跟JSONArray的差別了{}表示JSONObject,而[]表示JSONArray 做好後我們跟上次一樣發布成網頁應用程式 以後要記得,只要想修改GAS,修改完就一定要發布,然後版本一定要+1版本只會越來越高,如果選擇舊的版號的話,API是抓不到你最新的修改的喔! 成功改成JSONArray後,原本的寫法就不適用了,不過我們也不打算繼續使用舊的邏輯先來整理目前程序接收到API後的邏輯 discord訊息事件觸發 -> 沒有前綴字,進入API字串比對 -> 比對完成,反饋結果 -> 將結果反饋回discord 這樣做最明顯的問題就是每有一個訊息事件,bot就要打一次API上去花費的時間過長,容易增加bot錯誤且沒有考量過GAS每日免費額度問題 那麼該怎麼解決這問題呢?其實也很簡單,只要讓抓取API的行為只要執行一次就好 整個DiscordBot,唯一只會執行一次的地方就在ready事件 當程序啟動,程序自動執行login方法,login成功就會收到唯一一次的ready 將原本在下面的API事件拉上ready,並且將messageED改成dataED我們之後就不讓GetGas做字串比對了,只要幫我們打API並且整理好資料後反饋就好 處理好上面後,做字串比對 //BaseExcel字串比對 function GetBaseExcelData(msg) { try { if (BaseExcelData) { const userMessage = msg.content; e = BaseExcelData.filter(element => { return element.NAME === userMessage; }) if (e) return e[0].VALUE; else return false; } } catch (err) { console.log('GetBaseExcelDataError', err); } } 然後將字串比對的function拉到原本請求API的地方 都完成後,我們試著執行看看 成了!這樣我們的bot只在執行時會去取API解決了GAS限制的問題,並且每次的讀寫速度也提升許多 到此,DiscordBot後台0負擔這個主題的基本設置大致說完了這邊附上完整的教學專案https://supr.link/MePIY 剩下十天會教一些額外的內容,例如昨天提到GAS的訊息應該是分群組的,音樂系統如何分群使用等,以及GitHub使用….如果讀者有想看的也可以留言給筆者知道,筆者會的話再做安排 最後我們將檔案推上Heroku,記得怎麼推嗎? git add .git commit -m ‘版本說明’git push heroku master","link":"/2020/09/20/12thDay20/"},{"title":"Day21 - 認識GitHub","text":"今天想先說該怎麼把專案推上github,可以順便複習與heroku配套的git指令 heroku會用到git是因為heroku推程序這個動作跟github一樣,都是將專案推到網路上某個地方,推到github或heroku的差別而已 既然如此,heroku理所當然也可以做到跟github一樣的事情那又為什麼還要額外放在github呢? GitHub GitHub是基於Git語言的開源專案庫他提供任何人將自己的程序打包成專案,透過Git推上GitHub進行開發&版本紀錄GitHub對開源有著良好的圖形介面支持,所有人都可以在GitHub上看到對方的專案,並且提出協作要求、讓多位工程師來協同完成一件專案 一言以蔽之、將我們的機器人推上GitHub就像是展示你自己的作品,讓所有人都看的到你做過哪些東西,又是何時更新、更新了甚麼,是資訊人要讓人了解自己最快速的一步 推GitHub專案雖然也要使用Git語言,但因為GitHub是圍繞著Git開發的網站,其對Git語言的支援十分強大,除了網站本身按一按就能把專案推上去以外,他還推出了專案管理的圖形使用者介面 GitHub Desktophttps://desktop.github.com/請點擊連結並且下載GitHub Desktop 安裝完開啟後會要求你登入,請直接登入可以選擇亮或暗主題一開始會問你要不要直接新增專案,請拒絕,想辦法進到這個畫面 點擊左上角,拉出Add下拉框,點開後有個Create new repository 這是新增一個新專案,點下去後他會先要你選擇本機上的路徑 第一個是資料夾名稱,我們取DiscordBot第二個是專案簡介,可以隨便寫、但注意不要太多,一行就好第三個是路徑,請放在原專案外面Initialize this repository with a README記得打勾下面兩個是使用語言之類的,這部份GitHub上傳後會自動判斷,可以不管 都好了之後我們按…..不對!還不能按Create repository!我們先進到專案資料夾 我們把.git資料夾改名成Herokugit如果看不到.git資料夾請上網查一下怎麼看到隱藏資料夾 好了之後我們回去按Create repository按完會發現資料夾內多了 README.md.gitattributes.git 記得我們一開始建Heroku有提到嗎,只要是Git專案都會有.git檔案因為使用Heroku的同時,他就是屬於Heroku的專案了,我們如果也要推上GitHub的話,就要先讓他不是Heroku的專案,不然會覆蓋掉! 然後我們新增一個.gitignore,注意沒有副檔名喔 auth.json .gitignore Herokugit 這是給GitHub看的文件,可以讓GitHub在將專案commit前,選擇要忽略哪些檔案我們的私密資料都在auth.json,所以auth.json自然不能推到任何人都能看得GitHub上Herokugit是讓GitHub不會上傳到Heroku的.git檔案 這時我們回到GitHubDesktop,可以看到左下角告訴你,專案commit好了,並且版號是init(初始化) 在宣告一個新專案庫時,相當於他幫你下了 git add .git commit init 這兩個指令,我們可以點左上角的history看到我們有哪些檔案被commit,只要等等再下push就會被推上網際網路 那我們點一下右上角的Publish repository 第一次push時會有像這樣的提示框,他會二次確認你在GitHub上的專案要叫甚麼名字 Keep this code private打勾的話,這個專案就會是私人的,只有你登錄帳號時看的見我們希望程序是可以被人看見的,所以我們要把打勾取消掉 好了之後我們點Publish repository,他就會開始上傳專案,第一次比較久,我們等一下,可以去到杯水再回來看看 畫面長這樣就是成功了,左下角的commit消失(被推上去) History可以看到我們的歷史版本,以及做了那些變動","link":"/2020/09/21/12thDay21/"},{"title":"Day23 - 音樂系統的歌單批量載入(額外)","text":"昨天我們把音樂系統的多群組支援做好了今天想講一下怎麼直接導入歌單 首先請在專案目錄下的終端機安裝 npm install ytpl 安裝完成後,我們打開看package.json 最後一行出現了ytpl請到這個網站比對ytpl的版本,如果像筆者一樣版本過低的話,請將package.json內的ytpl版本拉高,然後更新一次ytplhttps://www.npmjs.com/package/ytpl 官方文檔版本1.0.1 手動把0.3.0改成1.0.1然後下指令 npm update ytpl 這樣就會更新你的ytpl函式庫 一安裝完就去確認版本是否最新,是因為舊版本的ytpl在抓取歌單資料時十分不穩,甚至有可能直接被yt擋下 原因不明,但這道理可以套到ytdl-core上,之後同學們有任何問題都可以先更新版本看看 更新好後,我們在bot.js引用ytpl 在音樂指令底下加入歌單載入功能 //?playList async function playListMusic(guildID, msg) { try { //沒在音樂廳不能使用此功能 if (!client.voice.connections.get(guildID)) { msg.channel.send(`請先正常啟用音樂指令後,再使用歌單載入喔`); return false; } //網址 const valueED = msg.content.split(' '); //先用library自帶的方式檢查一次能不能用 const canPlay = await ytpl.validateID(valueED[1]); //讀取到幾首歌,上限默認100首 let a = 0; //幾首成功放入歌單 let b = 0; if (canPlay) { const listED = await ytpl(valueED[1]); await listED.items.forEach(async function(element) { a = a + 1; if (element.title !== '[Deleted video]') { canPlay2 = await ytdl.validateURL(element.url_simple); if (canPlay2) { b = b + 1; musicList.get(guildID).push(element.url_simple); } } }); //回傳統計資訊 msg.channel.send(`歌單 ${listED.title}\\n共載入${b}首歌曲\\n${a-b}首載入失敗`); } else { msg.channel.send(`This Url isn't working in function.`); } } catch (err) { console.log(err, 'playListMusicError'); } } 由上而下依序說明… 因為歌單功能僅提供將yt歌單放入bot歌單的功能正常使用play指令,不在語音廳的情況下是會直接進入語音廳並開始播放歌曲筆者這邊寫成不能從歌單指令開始播歌 宣告了四份參數 valueED第一個單純是使用空白做字串分割,valueED[0]是前綴字+playListvalueED[1]則是一格空白後加上網址 canPlay使用ytpl自帶的檢查語法,會根據帶入的url回傳布林 a載入迴圈的每一次都會+1,代表著載入幾首歌 b載入迴圈的每一次,當成功將歌曲加入歌單時+1,表示成功抓取幾首歌 當canPlay等於ture後,正式查詢歌單並且將資料回傳給listEDlistED底下有一items為JSONArray,他就是歌單的集合對他使用迴圈,並在迴圈內用ytdl驗證一次網址是否可用驗證全部通過後將歌曲加入該群組歌單最後統計數字 因為加入批量歌曲載入的緣故,當機器人在列出queueShow時,極有可能回傳大量文字discord單封文字的上限數是2000,我們取1900就好 都好了後,試著運行看看 這樣音樂系統也能做到批量載入音樂了其餘還剩一些瑕疵,如歌單功能有限制,歌曲詳細資訊載入偏慢,沒有過濾私人影片還有更多可能的問題等…就讓各位自己嘗試看看吧 那音樂系統就教到這,我們明天見","link":"/2020/09/23/12thDay23/"},{"title":"Day22 - 音樂系統的多群組化(額外)","text":"這兩天來把音樂系統教完好了 目前為止的音樂系統只支援一隻機器人 for 一個群組的模式如果有多群組同時要使用音樂系統,會導致歌單列表共用 這是因為機器人的系統中,並沒有將群組納入判斷要改起來並不難,但邏輯要清晰 不知道當時有沒有小夥伴自己搞定這一塊的?我們今天會再帶過 程式碼是依照之前的進度,不會重頭開始,如果需要但沒有基礎程式碼的話可以回去看音樂系統的教學 MusicFunction 首先請把宣告成全域變數的兩個參數,初始化都設為Map() 再來我們在音樂指令的入口提取guildID,並且放入每一個function內 playMusic 修改了151,155跟171行(可以根據左側顏色判斷) 第一次進入語音廳的群組需要先以群組ID宣告一個歌曲列表原本歌曲列表放入資料的方法是這樣 musicList.push(網址) 現在變成 music.get(群組id).push(網址) 也就是根據群組id提取歌曲列表 playMusic2 改了185,195,202,204跟206行(可以根據左側顏色判斷) 原則上都跟剛剛一樣,注意歌曲清單跟播放遙控器應該是一個群組一個而已 disconnectMusic 修改了222行(可以根據左側顏色判斷) replayMusic 改了235,237與240行(可以根據左側顏色判斷) skipMusic 只有一行 nowPlayMusic 修改了253,255跟261行(可以根據左側顏色判斷) 字串串接部分拿的是已經處理好的參數,所以不用修改streamString queueShow 修改了291,294跟296行(可以根據左側顏色判斷) 這樣基本就都改好了,我們試著運行看看 運行前,因為之前我們已經上傳機器人到heroku上,理論上現在機器人是在運行狀態的這時候如果我們使用node bot,雖然不會有bug,但會造成bot裡面同時有兩隻程序登入,會造成很有趣的現象,各位有興趣可以試試 那這邊筆者為了繞過這問題,想直接上傳至heroku,這樣就可以只跑一個程序,也剛好介紹怎麼用heroku瀏覽程序歷程 測試我們先回到專案資料夾底下,將.git改名gitHub,然後將gitHeroku改回.git 回到vsCode,將專案推上heroku 推完看到Build succeeded後幫我下 heroku log -t 之前應該有提到這是觀察專案在heroku上的託管狀態如果我們要透過heroku來直接跑程序,或是之後出問題都是來這邊看error訊息 開好訊息後,我們試著測試看看機器人是不是真的可以分群播音樂了 大致如此,我們可以看到,機器人確實在兩個群組收到指令時,不會影響到對方了 音樂系統的多群組支援教到這明天看看要不要教一些額外的功能,我們明天見 主程序//#region 音樂系統 //歌曲控制器 let dispatcher = new Map(); //歌曲清單 let musicList = new Map(); function MusicFunction(msg) { //將訊息內的前綴字截斷,後面的字是我們要的 const content = msg.content.substring(prefix[1].Value.length); //指定我們的間隔符號 const splitText = ' '; //用間隔符號隔開訊息 contents[0] = 指令,contents[1] = 參數 const contents = content.split(splitText); //因為會持續使用到,將群組ID獨立成參數 const guildID = msg.guild.id; switch (contents[0]) { case 'play': //點歌&播放歌曲功能 playMusic(guildID, msg, contents); break; case 'replay': //重播當前歌曲 replayMusic(guildID); break; case 'np': //當前歌曲資訊 nowPlayMusic(guildID, msg.channel.id); break; case 'queue': //歌曲清單 queueShow(guildID, msg.channel.id); break; case 'skip': //中斷歌曲 skipMusic(guildID); break; case 'disconnect': //退出語音頻道並且清空歌曲清單 disconnectMusic(guildID, msg.channel.id); break; } } //?play async function playMusic(guildID, msg, contents) { //定義我們的第一個參數必需是網址 const urlED = contents[1]; try { //第一個參數不是連結就要篩選掉 if (urlED.substring(0, 4) !== 'http') return msg.reply('The link is not working.1'); //透過library判斷連結是否可運行 const validate = await ytdl.validateURL(urlED); if (!validate) return msg.reply('The link is not working.2'); //獲取歌曲資訊 const info = await ytdl.getInfo(urlED); //判斷資訊是否正常 if (info.videoDetails) { //指令下達者是否在語音頻道 if (msg.member.voice.channel) { //判斷bot是否已經連到語音頻道 是:將歌曲加入歌單 不是:進入語音頻道並且播放歌曲 if (!client.voice.connections.get(msg.guild.id)) { //因為是第一次加入,宣告新的歌曲列表 musicList.set(guildID, new Array()); //將歌曲加入歌單 musicList.get(guildID).push(urlED); //進入語音頻道 msg.member.voice.channel.join() .then(connection => { msg.reply('來了~'); //const guildID = msg.guild.id; const channelID = msg.channel.id; //播放歌曲 playMusic2(connection, guildID, channelID); }) .catch(err => { msg.reply('bot進入語音頻道時發生錯誤,請再試一次'); console.log(err, 'playMusicError2'); }) } else { //將歌曲加入歌單 musicList.get(guildID).push(urlED); msg.reply('已將歌曲加入歌單!'); } } else return msg.reply('請先進入頻道:3...'); } else return msg.reply('The link is not working.3'); } catch (err) { console.log(err, 'playMusicError'); } } //?play 遞迴函式 async function playMusic2(connection, guildID, channelID) { try { //播放前歌曲清單不能沒有網址 if (musicList.get(guildID).length > 0) { //設定音樂相關參數 const streamOptions = { seek: 0, volume: 0.5, Bitrate: 192000, Passes: 1, highWaterMark: 1 }; //讀取清單第一位網址 const stream = await ytdl(musicList.get(guildID)[0], { filter: 'audioonly', quality: 'highestaudio', highWaterMark: 26214400 //25ms }) //播放歌曲,並且存入dispatcher dispatcher.set(guildID, connection.play(stream, streamOptions)); //監聽歌曲播放結束事件 dispatcher.get(guildID).on("finish", finish => { //將清單中第一首歌清除 if (musicList.get(guildID).length > 0) musicList.get(guildID).shift(); //播放歌曲 playMusic2(connection, guildID, channelID); }) } else disconnectMusic(guildID, channelID); //清空歌單並且退出語音頻道 } catch (err) { console.log(err, 'playMusic2Error'); } } //?disconnect function disconnectMusic(guildID, channelID) { try { //判斷bot是否在此群組的語音頻道 if (client.voice.connections.get(guildID)) { //清空歌曲清單 musicList.set(guildID, new Array()); //退出語音頻道 client.voice.connections.get(guildID).disconnect(); client.channels.fetch(channelID).then(channel => channel.send('晚安~')); } else client.channels.fetch(channelID).then(channel => channel.send('可是..我還沒進來:3')) } catch (err) { console.log(err, 'disconnectMusicError'); } } //?replay function replayMusic(guildID) { if (musicList.get(guildID).length > 0) { //把當前曲目再推一個到最前面 musicList.get(guildID).unshift(musicList[0]); //將歌曲關閉,觸發finish事件 //finish事件將清單第一首歌排出,然後繼續播放下一首 if (dispatcher.get(guildID) !== undefined) dispatcher.get(guildID).end(); } } //?skip function skipMusic(guildID) { //將歌曲關閉,觸發finish事件 if (dispatcher.get(guildID) !== undefined) dispatcher.get(guildID).end(); } //?np async function nowPlayMusic(guildID, channelID) { try { if (dispatcher.get(guildID) !== undefined && musicList.get(guildID).length > 0) { //從連結中獲取歌曲資訊 標題 總長度等 const info = await ytdl.getInfo(musicList.get(guildID)[0]); //歌曲標題 const title = info.videoDetails.title; //歌曲全長(s) const songLength = info.videoDetails.lengthSeconds; //當前播放時間(ms) const nowSongLength = Math.floor(dispatcher.get(guildID).streamTime / 1000); //串字串 const message = `${title}\\n${streamString(songLength,nowSongLength)}`; client.channels.fetch(channelID).then(channel => channel.send(message)) } } catch (err) { console.log(err, 'nowPlayMusicError'); } } //▬▬▬▬▬▬▬▬▬?▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ function streamString(songLength, nowSongLength) { let mainText = '?'; const secondText = '▬'; const whereMain = Math.floor((nowSongLength / songLength) * 100); let message = ''; for (i = 1; i <= 30; i++) { if (i * 3.3 + 1 >= whereMain) { message = message + mainText; mainText = secondText; } else { message = message + secondText; } } return message; } //?queue async function queueShow(guildID, channelID) { try { if (musicList.get(guildID).length > 0) { let info; let message = ''; for (i = 0; i < musicList.get(guildID).length; i++) { //從連結中獲取歌曲資訊 標題 總長度等 info = await ytdl.getInfo(musicList.get(guildID)[i]); //歌曲標題 title = info.videoDetails.title; //串字串 message = message + `\\n${i+1}. ${title}`; } //把最前面的\\n拿掉 message = message.substring(1, message.length); client.channels.fetch(channelID).then(channel => channel.send(message)) } } catch (err) { console.log(err, 'queueShowError'); } } //#endregion","link":"/2020/09/22/12thDay22/"},{"title":"Day24 - 愛麗絲安靜!","text":"今天想教怎麼讓機器人安靜/啟動這樣才能說之後想教的東西 情境描述機器人需要有個控制是否啟動的開關可設定在該群組 or 頻道是否可以接收指令 因為我們的機器人會有多組系統(文字回答&音樂系統)會希望再額外設定可以在指定的群組 or 頻道是否可以接收特定系統的指令 實作開始首先請先幫我新建一個JSON檔案,叫做shup.json…或是你喜歡的名字xD 記得JSON檔案都要放在JSONHome喔 type型態,代表這一筆JSONObject是甚麼類型目前還用不到,都設1就好 GroupID群組ID,主要給後續判定的部份 GroupName群組名稱,用處不大、主要給使用者好分辨的 Power代表不可使用的權限,後面會一邊寫一邊解說 禁言指令,顧名思義、希望機器人是否被禁言的開關這開關需要放在所有指令之前,這樣才可以判斷出內容是否需要被禁止 老樣子,我們要先引入json 放在message事件的上方,當判斷權限為false,就停止後續行為 接著我們實作IsShutIsShut根據shup.json,先判斷訊息群組&訊息有沒有資料,再來判斷資料中存不存在tempPrefix,存在的話就要禁用功能,因為默認不存在時文檔中不會有資料,自然就不該設限 //禁言系統判斷 function IsShut(msg, tempPrefix) { //群組id const guildID = msg.guild.id; //頻道id const channelID = msg.channel.id; //當前狀態 let status = true; //先判斷群組,群組判斷完判斷頻道(頻道權限優先於群組) const guildIF = shup.Group.find(element => { if (element.GroupID == guildID) { return element.Power.indexOf(tempPrefix) !== -1; } return false; }) //找到資料 = 此群組存在Group中且Power存在此次指令代碼 if (guildIF !== undefined) { status = false; } //頻道 const channelIF = shup.Channel.find(element => { if (element.ChannelID == channelID) { return true; } return false; }) //找到資料 = 此頻道存在Channel中 if (channelIF !== undefined) { //Power有此資料=>禁用功能 無資料=>不設限 if (channelIF.Power.indexOf(tempPrefix) !== -1) { status = false; } else { status = true; } } return status; } 接著我們將資料實際key入shup.json看看 不知道同學有沒有看出Power的判斷依據了? 沒錯,就是依據prefix時設置的各系統代表ID 我們試著跑看看 另一個頻道 大成功~這樣就做到各頻道各功能權限設置了 不過存在一些問題例如放在預設(default)的資料庫文字比對功能要怎麼判,應該是預設的-1吧如果是-1要怎麼判斷呢? 以及目前這樣的做法只做到判斷禁言功能的實作,還沒有辦法在前台讓使用者手動新增 這部份筆者先賣個關子,各位可以嘗試寫看看,明天我們繼續做別的功能,之後再繞回來","link":"/2020/09/24/12thDay24/"},{"title":"Day26 - tag控管機制(1)","text":"昨天我們描繪了權限系統的架構,今天來建立身份組環境 打開我們之前的試算表,新增兩個table UserPower代表成員table userIDdiscord的userID,主要用來辨識訊息方是否是此用戶 userName用處一樣不大,給人看的 Joins表示此用戶有哪些身份組的權限,筆者打算之後把所有身份都寫在這欄,用分號來做區隔 IsAdmin管理員開關,開啟後不做任何身份組判斷,可以使用任何功能 下圖中,筆者的userID有E有+的,這是Excel自動給予的格式,可以在左上角看到實際數值,讀取時仍然是讀取165753385385984000,不用修改 PartyPower代表身份組table ID該身份組的ID,使用者透過這個ID來判斷自己有哪些權限 type代表這個身份組的類型目前暫定1是禁言類身份組,2是tag權限身份組 Power代表實際可行駛的權限,會根據type的不同有不同的含意在tag權限下,Power帶入tagID,代表可以行使此tag這邊帶入Power的是身份組ID 教一下手動獲取身份組ID 把人點開,對身份組右鍵 或是先拉出tag,然後在tag前方加上一個反斜線 如果以上操作遇到問題,甚至是UserID也抓不到可以看一下這篇文章 或是找找怎麼開啟Discord的開發者模式 再來我們要新增兩個GAS檔案 function doGet(e) { var id = '你的ID'; //抓取表單 var spreadsheet = SpreadsheetApp.openById(id); // Sheet id var sheet = spreadsheet.getSheetByName("UserPower"); // 根據表格名稱取表 var rowLength = sheet.getLastRow()-1; //取行長度 var columnLength = sheet.getLastColumn(); //取列長度 var data = sheet.getRange(2,1,rowLength,columnLength).getValues(); // 取得的資料 var dataExport = []; for(i in data){ if(data[i][0] != ""){ dataExport[i] = { userID: data[i][0], userName: data[i][1], Joins: data[i][2], IsAdmin: data[i][3] } } } var dataExportFormat = JSON.stringify(dataExport); return ContentService.createTextOutput(dataExportFormat).setMimeType(ContentService.MimeType.JSON); } function doGet(e) { var id = '你的ID'; //抓取表單 var spreadsheet = SpreadsheetApp.openById(id); // Sheet id var sheet = spreadsheet.getSheetByName("PartyPower"); // 根據表格名稱取表 var rowLength = sheet.getLastRow()-1; //取行長度 var columnLength = sheet.getLastColumn(); //取列長度 var data = sheet.getRange(2,1,rowLength,columnLength).getValues(); // 取得的資料 var dataExport = []; for(i in data){ if(data[i][0] != ""){ dataExport[i] = { ID: data[i][0], type: data[i][1], Power: data[i][2] } } } var dataExportFormat = JSON.stringify(dataExport); return ContentService.createTextOutput(dataExportFormat).setMimeType(ContentService.MimeType.JSON); } 記得都要存檔後,發佈成網路應用,獲取URL (之前示範JSONArray的[],可以拿掉)跟baseExcel一樣,我們會希望bot在啟動時就把表都讀取進來,從雲端下載成本地db的感覺,順便做資料二次處理 const userPower = { 'method': 'GET', 'url': auth.Gas.Get[0].UserPower, 'headers': {} }; const partyPower = { 'method': 'GET', 'url': auth.Gas.Get[0].PartyPower, 'headers': {} }; exports.getUserPower = function(callback) { let backValue = new Array; request(userPower, function(error, response) { try { if (error) { console.log('getUserPowerError1', error); callback(false); } else { const data = JSON.parse(response.body); //接收回傳(response)的body for (i in data) { backValue.push(data[i]); backValue[i].Joins = backValue[i].Joins.split(';'); } callback(backValue); } } catch (err) { console.log('getUserPowerError2', err); callback(false); } }); }; exports.getPartyPower = function(callback) { let backValue = new Array; request(partyPower, function(error, response) { try { if (error) { console.log('getPartyPowerError1', error); callback(false); } else { const data = JSON.parse(response.body); //接收回傳(response)的body for (i in data) { backValue.push(data[i]); backValue[i].Power = backValue[i].Power.split(';'); } callback(backValue); } } catch (err) { console.log('getPartyPowerError2', err); callback(false); } }); }; (開始變成callback地獄了) 資料都接到也處理好了,再來要用這些資料實作功能 增加指令列表的一個新系統 在message事件新增入口 然後做出實際功能 //#region tag系統 function TagFunction(msg, tempPrefix) { const cmd = msg.content.substring(prefix[tempPrefix].Value.length).split(' '); //以空白分割前綴以後的字串 switch (cmd[0]) { case '其餘指令': break; default: //身份組ID CheckID(msg, cmd, CheckParty); break; } } //判斷此人有沒有登記資料 function CheckID(msg, cmd, OtherFunction) { const haveUserData = UserPowerData.find(element => { return element.userID == msg.author.id; }) if (haveUserData !== undefined) { //有資料 if (haveUserData.IsAdmin) { //是管理員,直接做後續事情 return SendTagMessage(msg, `<@&${cmd[1]}>\\n來自管理員<@${msg.author.id}>的指令呼叫`); } else { //不是管理員,先看有甚麼群組 return OtherFunction(msg, cmd, haveUserData); } } } //根據UserPower獲得Party function CheckParty(msg, cmd, haveUserData) { let havePartyPower; havePartyPower = PartyPowerData.filter(element => { if (haveUserData.Joins[i].indexOf(element.ID) != -1) { return element.Power.indexOf(cmd[1]) != -1 } }) if (isEmptyObject(havePartyPower)) { SendTagMessage(msg, '無權限,請確認參數是否正確'); } else { SendTagMessage(msg, `<@&${cmd[1]}>\\n來自<@${msg.author.id}>的指令呼叫`); } } //傳送訊息單獨實例 function SendTagMessage(msg, data) { msg.channel.send(data); } 最後補個判斷Array是不是空集合的小function 大致解說一下 Tag系統的入口function跟其他系統一樣,判斷要使用甚麼指令今天先把預設(default)指令,也就是tag身分組做出來 檢查UserPower中是否有此人資料,以及是否是管理員如果有資料且不是管理員,繼續檢查其所屬身份組權限 檢查身份組中是否有權限符合這次要tag的對象id,有的話代表此次指令滿足權限,給予tag 我們跑看看 成功","link":"/2020/09/26/12thDay26/"},{"title":"Day27 - tag控管 - 續行方法解說","text":"昨天我們實作了tag身份組功能使用者透過機器人tag一整個身份組的功能我們希望還可以有 創建身分組的指令 將使用者加入身份組指令 刪除身分組的指令 將使用者從某個身份組中刪除的指令 這些功能與以往的做法不同API會從原本的Get改成使用Post方法bot程序會需要用到續行方法,來彌補當前機器人框架無法實現的功能 續行方法程式上並沒有這種寫法的稱呼,只是在當前框架下,我給這個寫法的一種叫法而已 目前我們機器人指令的呼叫方式基本模式是 前綴字 + 指令 + 間隔符 + 參數(如果有) 如果前綴字沒有匹配,就對資料庫做判斷,是否有相同觸發字來自動回應(卡米狗模式) 這種做法會面臨到一個問題,指令都是在一行內完成的 如果希望先下達指令,等待機器人給予回饋,再繼續輸入指令呢? 沒錯,使用者的操作會變得相對複雜假設一個指令叫做 !AFK ,然後要輸入三次且三次的參數都正確,機器人才會醒來那使用者就要連續輸入三次 !AFK 1!AFK 2!AFK 3 而不是 !AFK123 讓使用者不斷重複的輸入指令顯然不是我們希望的 因此我們會需要使用續行,讓BOT觸發到特定指令後,綁定此用戶進行接下來的行為 明天我們會實作postAPI,以及將 將使用者加入特定身分組的功能 寫好如果篇幅足夠會再講 將使用者從某個身份組中刪除","link":"/2020/09/27/12thDay27/"},{"title":"Day28 - tag控管機制(2)","text":"今天來把postAPI跟續行的框架與加入使用者至身份組的功能寫好 請打開之前再google雲端上創建的GAS , getUserPower 原本的程式寫在doGet方法,我們在doGet方法下新增一個doPost方法,然後寫上這些東西 function doPost(e){ var para = e.parameter; // 存放 post 所有傳送的參數 var id = '1mQ6qTJfOs3Gv5--K9r87w56wmDc3hUcpHm5hF1YKTms'; //抓取表單 var spreadsheet = SpreadsheetApp.openById(id); // Sheet id var sheet1 = spreadsheet.getSheetByName("UserPower"); // 根據表格名稱取表 var rowLength = sheet1.getLastRow()-1; var columnLength = sheet1.getLastColumn(); var data = sheet1.getRange(2,1,rowLength,columnLength).getValues(); // 取得的資料 var userID = para.userID, userName = para.userName, Joins = para.Joins, IsAdmin = para.IsAdmin; var upData = []; for(i=0;i<=rowLength-1;i++){ upData = data[i] if((upData[0]==userID) == false){ upData = undefined; } if(upData != undefined){ sheet1.getRange(i+2, 1).setValue(userID); sheet1.getRange(i+2, 2).setValue(userName); sheet1.getRange(i+2, 3).setValue(Joins); sheet1.getRange(i+2, 4).setValue(IsAdmin); return ContentService.createTextOutput(upData).setMimeType(ContentService.MimeType.JSON); } } sheet1.appendRow([userID,userName,Joins,IsAdmin]); // 插入一列新的資料 var dataExportFormat = JSON.stringify(para); return ContentService.createTextOutput(dataExportFormat).setMimeType(ContentService.MimeType.JSON); } doGet與doPost是GAS默認的Get與Post方法使用他的Url執行Get請求就會進doGet反之post就會進doPost 回到程序,一樣在auth加入url雖然值跟Get方法時的Url一樣,不過這樣寫會比較好分辨,之後想改成兩個檔案也可以 因為我們要在post請求帶入參數,這邊將請求URL的宣告直接放到實作裡面這樣在創建的同時也會帶入參數 exports.postUserPower = function(bodyData, callback) { const userPowerPost = { 'method': 'POST', 'url': auth.Gas.Post.UserPower, 'headers': {}, form: { 'userID': bodyData[0], 'userName': bodyData[1], 'Joins': bodyData[2], 'IsAdmin': bodyData[3] } }; request(userPowerPost, function(error, response) { try { if (error) { console.log('postUserPowerError1', error); callback(false); } else { callback(true); } } catch (err) { console.log('postUserPowerError2', err); callback(false); } }); }; 在全域變數(最上方)新增這四個東西 在子類方法(最下方)加入初始化方法 在onMessage中間加入續行方法入口 //續行方法 if (nowDoFunction && msg.author.id === DoUserID) { nowDoFunction(msg); return; } 在tag系統入口加入addUser 實例addUserFunction //將xxx加入身分組 function addUserFunction(msg) { try { if (DoUserID === '') { nowDoFunction = addUserFunctionNow; DoUserID = msg.author.id; DoData = new Array; msg.channel.send('請輸入要加入的使用者id'); } else { msg.channel.send('有其他人正在使用續行中,請稍等'); } } catch (err) { console.log('addUserFunctionError', err); } } 實例addUserFunctionNow(續行方法) //將xxx加入身份組(續行方法) function addUserFunctionNow(msg) { try { switch (DoingCount) { case 0: DoData.push(msg.content); //加入使用者id DoData.push(msg.author.username); //加入申請者名字 msg.channel.send(`請輸入要加入的群組`); break; case 1: DoData.push(msg.content); //加入群組 DoData.push(false); //IsAdmin預設False不可修改 msg.channel.send(`申請資料如下:\\n申請人 <@${msg.author.id}>\\n使用者 <@${DoData[0]}>\\n加入權限組 ${DoData[2]}\\n正確 Y / 錯誤 N`); break; case 2: if (msg.content === 'Y') { msg.channel.send('已確認,輸入資料中...'); //與舊資料比對,已有此人資料變進行更新 CheckID(msg, null, EditOldUserPower, DoData[0]); GetGas.postUserPower(DoData, function(dataED) { if (dataED) { //bot內變數不會更新,手動更新 UserPowerData.unshift({ 'userID': DoData[0], 'userName': DoData[1], 'Joins': DoData[2], 'IsAdmin': DoData[3] }); msg.channel.send('輸入完畢!'); } else { msg.channel.send('資料輸入失敗,請重新嘗試'); } CloseAllDoingFunction(); }); } else if (msg.content === 'N') { CloseAllDoingFunction(); msg.channel.send('已取消行為,請重新下達指令') } else { DoingCount--; msg.channel.send('無法辨識訊息,請輸入Y/N來選擇'); } break; } if (DoUserID !== '') DoingCount++; } catch (err) { CloseAllDoingFunction(); client.channels.fetch(msg.channel.id).then(channel => channel.send('發生意外錯誤,中斷指令行為,請重新下達指令!')) console.log('addUserFunctionNowError', err); } } 微調CheckID與CheckParty將userID的取值獨立,將admin判斷拉到CheckParty 實例EditOldUserPower 測試環節 完成 因為今天有修改到Day26的東西,這可能導致教學有點雜亂底下附上tag系統目前的程式碼,當作彌補 //#region tag系統 function TagFunction(msg, tempPrefix) { const cmd = msg.content.substring(prefix[tempPrefix].Value.length).split(' '); //以空白分割前綴以後的字串 switch (cmd[0]) { case 'addUser': //將使用者加入身份組 addUserFunction(msg); break; default: //身份組ID CheckID(msg, cmd, CheckParty, msg.author.id); break; } } //判斷此人有沒有登記資料 function CheckID(msg, cmd, OtherFunction, userID) { const haveUserData = UserPowerData.find(element => { return element.userID == userID; }) if (haveUserData !== undefined) { //有資料 return OtherFunction(msg, cmd, haveUserData); } else { return false; } } //根據UserPower獲得Party function CheckParty(msg, cmd, haveUserData) { if (haveUserData.IsAdmin) { //是管理員,直接做後續事情 return SendTagMessage(msg, `<@&${cmd[1]}>\\n來自管理員<@${msg.author.id}>的指令呼叫`); } let havePartyPower; havePartyPower = PartyPowerData.filter(element => { if (haveUserData.Joins[i].indexOf(element.ID) != -1) { return element.Power.indexOf(cmd[1]) != -1 } }) if (isEmptyObject(havePartyPower)) { SendTagMessage(msg, '無權限,請確認參數是否正確'); } else { SendTagMessage(msg, `<@&${cmd[1]}>\\n來自<@${msg.author.id}>的指令呼叫`); } } //傳送訊息單獨實例 function SendTagMessage(msg, data) { msg.channel.send(data); } //將xxx加入身分組 function addUserFunction(msg) { try { if (DoUserID === '') { nowDoFunction = addUserFunctionNow; DoUserID = msg.author.id; DoData = new Array; msg.channel.send('請輸入要加入的使用者id'); } else { msg.channel.send('有其他人正在使用續行中,請稍等'); } } catch (err) { console.log('addUserFunctionError', err); } } //將xxx加入身份組(續行方法) function addUserFunctionNow(msg) { try { switch (DoingCount) { case 0: DoData.push(msg.content); //加入使用者id DoData.push(msg.author.username); //加入申請者名字 msg.channel.send(`請輸入要加入的群組`); break; case 1: DoData.push(msg.content); //加入群組 DoData.push(false); //IsAdmin預設False不可修改 msg.channel.send(`申請資料如下:\\n申請人 <@${msg.author.id}>\\n使用者 <@${DoData[0]}>\\n加入權限組 ${DoData[2]}\\n正確 Y / 錯誤 N`); break; case 2: if (msg.content === 'Y') { msg.channel.send('已確認,輸入資料中...'); //與舊資料比對,已有此人資料變進行更新 CheckID(msg, null, EditOldUserPower, DoData[0]); GetGas.postUserPower(DoData, function(dataED) { if (dataED) { //bot內變數不會更新,手動更新 UserPowerData.unshift({ 'userID': DoData[0], 'userName': DoData[1], 'Joins': DoData[2], 'IsAdmin': DoData[3] }); msg.channel.send('輸入完畢!'); } else { msg.channel.send('資料輸入失敗,請重新嘗試'); } CloseAllDoingFunction(); }); } else if (msg.content === 'N') { CloseAllDoingFunction(); msg.channel.send('已取消行為,請重新下達指令') } else { DoingCount--; msg.channel.send('無法辨識訊息,請輸入Y/N來選擇'); } break; } if (DoUserID !== '') DoingCount++; } catch (err) { CloseAllDoingFunction(); client.channels.fetch(msg.channel.id).then(channel => channel.send('發生意外錯誤,中斷指令行為,請重新下達指令!')) console.log('addUserFunctionNowError', err); } } //用戶舊資料更新 function EditOldUserPower(msg, cmd, haveUserData) { //二次確認 if (DoData[0] == haveUserData.userID) { DoData[2] = haveUserData.Joins + ';' + DoData[2]; DoData[3] = haveUserData.IsAdmin; return true; } else return false; } //#endregion","link":"/2020/09/28/12thDay28/"},{"title":"Day29 - tag控管機制(3)","text":"昨天我們做好了 將使用者加入身份組指令今天把 創建身分組的指令做好 跟昨天一樣,請先開啟GAS,身分組的GAS叫做getPartyPower新增以下 function doPost(e){ var para = e.parameter; // 存放 post 所有傳送的參數 var id = '1mQ6qTJfOs3Gv5--K9r87w56wmDc3hUcpHm5hF1YKTms'; //抓取表單 var spreadsheet = SpreadsheetApp.openById(id); // Sheet id var sheet1 = spreadsheet.getSheetByName("PartyPower"); // 根據表格名稱取表 var rowLength = sheet1.getLastRow()-1; var columnLength = sheet1.getLastColumn(); var data = sheet1.getRange(2,1,rowLength,columnLength).getValues(); // 取得的資料 var ID = para.ID, type = para.type, Power = para.Power; var upData = []; for(i=0;i<=rowLength-1;i++){ upData = data[i] if((upData[0]==ID) == false){ upData = undefined; } if(upData != undefined){ sheet1.getRange(i+2, 1).setValue(ID); sheet1.getRange(i+2, 2).setValue(type); sheet1.getRange(i+2, 3).setValue(Power); return ContentService.createTextOutput(upData).setMimeType(ContentService.MimeType.JSON); } } sheet1.appendRow([ID,type,Power]); // 插入一列新的資料 var dataExportFormat = JSON.stringify(para); return ContentService.createTextOutput(dataExportFormat).setMimeType(ContentService.MimeType.JSON); } 加到auth.json GetGas.js bot.js //創建身分組&增加身分組可tag對象(續行) function CreatePartyFunctionNow(msg) { try { switch (DoingCount) { case 0: DoData.push(msg.content); //身分組ID DoData.push('2'); //type 2 msg.channel.send(`請輸入要加入的tagID`); break; case 1: DoData.push(msg.content); //加入tagID msg.channel.send(`申請資料如下:\\n申請人 <@${msg.author.id}>\\n權限組 <@${DoData[0]}>\\ntagID ${DoData[2]}\\n正確 Y / 錯誤 N`); break; case 2: if (msg.content === 'Y') { msg.channel.send('已確認,輸入資料中...'); //與舊資料比對,已有此人資料變進行更新 EditOldPartyPower(); GetGas.postPartyPower(DoData, function(dataED) { if (dataED) { //bot內變數不會更新,手動更新 PartyPowerData.unshift({ 'ID': DoData[0], 'type': DoData[1], 'Power': DoData[2] }); msg.channel.send('輸入完畢!'); } else { msg.channel.send('資料輸入失敗,請重新嘗試'); } CloseAllDoingFunction(); }); } else if (msg.content === 'N') { CloseAllDoingFunction(); msg.channel.send('已取消行為,請重新下達指令') } else { DoingCount--; msg.channel.send('無法辨識訊息,請輸入Y/N來選擇'); } break; } if (DoUserID !== '') DoingCount++; } catch (err) { CloseAllDoingFunction(); client.channels.fetch(msg.channel.id).then(channel => channel.send('發生意外錯誤,中斷指令行為,請重新下達指令!')) console.log('CreatePartyFunctionNowError', err); } } 作法基本上跟上次新增使用者一樣試著運行看看 成功明天就是最後一篇了,筆者會把刪除的指令做好其實跟現在新增的作法是大同小異的,各位不彷試試看 為了將CheckID與CheckParty的分工化更明確一點,因此有稍微修改程式碼這邊貼上程式碼 //#region tag系統 function TagFunction(msg, tempPrefix) { const cmd = msg.content.substring(prefix[tempPrefix].Value.length).split(' '); //以空白分割前綴以後的字串 switch (cmd[0]) { case 'AddUser': //將使用者加入身份組 addUserFunction(msg); break; case 'CreateParty': //創建身分組&增加身分組可tag對象 CreatePartyFunction(msg); break; default: //身份組ID tagOther(msg, cmd); break; } } //tag人 function tagOther(msg, cmd) { CheckID(msg, cmd, msg.author.id, (msg, cmd, haveUserData) => { CheckParty(msg, cmd, haveUserData, SendTagMessage); }); } //判斷此人有沒有登記資料 function CheckID(msg, cmd, userID, OtherFunction) { const haveUserData = UserPowerData.find(element => { return element.userID == userID; }) if (haveUserData !== undefined) { //有資料 return OtherFunction(msg, cmd, haveUserData); } else { return OtherFunction(msg, cmd, false); } } //根據UserPower獲得Party function CheckParty(msg, cmd, haveUserData, OtherFunction) { let havePartyPower; havePartyPower = PartyPowerData.filter(element => { if (haveUserData.Joins[i].indexOf(element.ID) != -1) { return element.Power.indexOf(cmd[1]) != -1 } }) if (isEmptyObject(havePartyPower)) { OtherFunction(msg, cmd, haveUserData, false); } else { OtherFunction(msg, cmd, haveUserData, havePartyPower); } } //傳送訊息單獨實例 function SendTagMessage(msg, cmd, haveUserData, havePartyPower) { if (haveUserData.IsAdmin) { msg.channel.send(`<@&${cmd[1]}>\\n來自管理員<@${msg.author.id}>的指令呼叫`); } else if (havePartyPower) { msg.channel.send(`<@&${cmd[1]}>\\n來自<@${msg.author.id}>的指令呼叫`); } else { msg.channel.send('無權限,請確認參數是否正確'); } } //將xxx加入身分組 function addUserFunction(msg) { try { if (DoUserID === '') { tempIsAdmin = CheckID(msg, null, msg.author.id, function(msg, cmd, haveUserData) { if (haveUserData.IsAdmin) return true; else return false; }); if (tempIsAdmin) { nowDoFunction = addUserFunctionNow; DoUserID = msg.author.id; DoData = new Array; msg.channel.send('請輸入要加入的使用者id'); } else { msg.channel.send('此指令只有管理員可執行'); } } else { msg.channel.send('有其他人正在使用續行中,請稍等'); } } catch (err) { console.log('addUserFunctionError', err); } } //將xxx加入身份組(續行方法) function addUserFunctionNow(msg) { try { switch (DoingCount) { case 0: DoData.push(msg.content); //加入使用者id DoData.push(msg.author.username); //加入申請者名字 msg.channel.send(`請輸入要加入的群組`); break; case 1: DoData.push(msg.content); //加入群組 DoData.push(false); //IsAdmin預設False不可修改 msg.channel.send(`申請資料如下:\\n申請人 <@${msg.author.id}>\\n使用者 <@${DoData[0]}>\\n加入權限組 ${DoData[2]}\\n正確 Y / 錯誤 N`); break; case 2: if (msg.content === 'Y') { msg.channel.send('已確認,輸入資料中...'); //與舊資料比對,已有此人資料變進行更新 CheckID(msg, null, DoData[0], EditOldUserPower); GetGas.postUserPower(DoData, function(dataED) { if (dataED) { //bot內變數不會更新,手動更新 UserPowerData.unshift({ 'userID': DoData[0], 'userName': DoData[1], 'Joins': DoData[2], 'IsAdmin': DoData[3] }); msg.channel.send('輸入完畢!'); } else { msg.channel.send('資料輸入失敗,請重新嘗試'); } CloseAllDoingFunction(); }); } else if (msg.content === 'N') { CloseAllDoingFunction(); msg.channel.send('已取消行為,請重新下達指令') } else { DoingCount--; msg.channel.send('無法辨識訊息,請輸入Y/N來選擇'); } break; } if (DoUserID !== '') DoingCount++; } catch (err) { CloseAllDoingFunction(); client.channels.fetch(msg.channel.id).then(channel => channel.send('發生意外錯誤,中斷指令行為,請重新下達指令!')) console.log('addUserFunctionNowError', err); } } //用戶舊資料更新 function EditOldUserPower(msg, cmd, haveUserData) { //二次確認 if (haveUserData) { if (DoData[0] == haveUserData.userID) { DoData[2] = haveUserData.Joins + ';' + DoData[2]; DoData[3] = haveUserData.IsAdmin; return true; } else return false; } else return false; } //創建身分組&增加身分組可tag對象 function CreatePartyFunction(msg) { try { if (DoUserID === '') { tempIsAdmin = CheckID(msg, null, msg.author.id, function(msg, cmd, haveUserData) { if (haveUserData) { if (haveUserData.IsAdmin) return true; else return false; } else return false; }); if (tempIsAdmin) { nowDoFunction = CreatePartyFunctionNow; DoUserID = msg.author.id; DoData = new Array; msg.channel.send('請輸入身份組名稱'); } else { msg.channel.send('此指令只有管理員可執行'); } } else { msg.channel.send('有其他人正在使用續行中,請稍等'); } } catch (err) { console.log('CreatePartyFunctionError', err); } } //創建身分組&增加身分組可tag對象(續行) function CreatePartyFunctionNow(msg) { try { switch (DoingCount) { case 0: DoData.push(msg.content); //身分組ID DoData.push('2'); //type 2 msg.channel.send(`請輸入要加入的tagID`); break; case 1: DoData.push(msg.content); //加入tagID msg.channel.send(`申請資料如下:\\n申請人 <@${msg.author.id}>\\n權限組 ${DoData[0]}\\ntagID ${DoData[2]}\\n正確 Y / 錯誤 N`); break; case 2: if (msg.content === 'Y') { msg.channel.send('已確認,輸入資料中...'); //與舊資料比對,已有此人資料變進行更新 EditOldPartyPower(); GetGas.postPartyPower(DoData, function(dataED) { if (dataED) { //bot內變數不會更新,手動更新 PartyPowerData.unshift({ 'ID': DoData[0], 'type': DoData[1], 'Power': DoData[2] }); msg.channel.send('輸入完畢!'); } else { msg.channel.send('資料輸入失敗,請重新嘗試'); } CloseAllDoingFunction(); }); } else if (msg.content === 'N') { CloseAllDoingFunction(); msg.channel.send('已取消行為,請重新下達指令') } else { DoingCount--; msg.channel.send('無法辨識訊息,請輸入Y/N來選擇'); } break; } if (DoUserID !== '') DoingCount++; } catch (err) { CloseAllDoingFunction(); client.channels.fetch(msg.channel.id).then(channel => channel.send('發生意外錯誤,中斷指令行為,請重新下達指令!')) console.log('CreatePartyFunctionNowError', err); } } //權限組舊資料更新 function EditOldPartyPower() { if (PartyPowerData) { const tempPartyData = PartyPowerData.find(element => { return element.ID == DoData[0]; }) if (tempPartyData !== undefined) { DoData[2] = tempPartyData.Power + ';' + DoData[2]; } } } //#endregion","link":"/2020/09/29/12thDay29/"},{"title":"Day30 - tag控管機制(4)","text":"今天把tag的最後一件事做完 將使用者從指定權限組移除將指定權限組移除 在tag入口新增Delete方法 創建實例 //從權限組中刪除使用者 OR 刪除權限組 function DeleteTag(msg) { try { if (DoUserID === '') { tempIsAdmin = CheckID(msg, null, msg.author.id, function(msg, cmd, haveUserData) { if (haveUserData.IsAdmin) return true; else return false; }); if (tempIsAdmin) { nowDoFunction = DeleteTagNow; DoUserID = msg.author.id; DoData = new Array; msg.channel.send('請問要編輯使用者權限還是權限組?\\n1 使用者權限 / 2 權限組'); } else { msg.channel.send('此指令只有管理員可執行'); } } else { msg.channel.send('有其他人正在使用續行中,請稍等'); } } catch (err) { console.log('DeleteTagError', err); } } 創建續行實例 //從權限組中刪除使用者 OR 刪除權限組(續行) function DeleteTagNow(msg) { try { switch (DoingCount) { case 0: switch (msg.content) { case '1': msg.channel.send('請輸入要編輯的使用者ID'); break; case '2': DoingCount = 10; msg.channel.send('請輸入要編輯的權限組'); break; default: DoingCount--; msg.channel.send('無法辨識訊息,請輸入1/2來選擇'); break; } break; case 1: if (msg.content == 'N') { CloseAllDoingFunction(); msg.channel.send('指令關閉'); } else { if (CheckID(msg, null, msg.content, (msg, cmd, haveUserData) => { return haveUserData })) { DoData.push(msg.content); //userID DoData.push(msg.author.id); //userName msg.channel.send('請輸入要刪除的群組權限'); } else { DoingCount--; msg.channel.send('此用戶不存在資料,請確認,如果要關閉指令請輸入 N'); } } break; case 2: DoData.push(msg.content); // Power DoData.push(false); // IsAdmin msg.channel.send(`申請資料如下:\\n申請人 <@${msg.author.id}>\\n使用者 <@${DoData[0]}>\\n刪除權限組 ${DoData[2]}\\n正確 Y / 錯誤 N`); break; case 3: if (msg.content === 'Y') { msg.channel.send('已確認,輸入資料中...'); //與舊資料比對,已有此人資料變進行更新 CheckID(msg, null, DoData[0], DeleteOldUserPower); GetGas.postUserPower(DoData, function(dataED) { if (dataED) { //bot內變數不會更新,手動更新 UserPowerData.unshift({ 'userID': DoData[0], 'userName': DoData[1], 'Joins': DoData[2], 'IsAdmin': DoData[3] }); msg.channel.send('輸入完畢!'); } else { msg.channel.send('資料輸入失敗,請重新嘗試'); } CloseAllDoingFunction(); }); } else if (msg.content === 'N') { CloseAllDoingFunction(); msg.channel.send('已取消行為,請重新下達指令') } else { DoingCount--; msg.channel.send('無法辨識訊息,請輸入Y/N來選擇'); } break; case 11: DoData.push(msg.content); //身分組ID DoData.push('2'); //type 2 DoData.push(''); msg.channel.send(`申請資料如下:\\n申請人 <@${msg.author.id}>\\n刪除權限組 ${DoData[0]}\\n正確 Y / 錯誤 N`); break; case 12: if (msg.content === 'Y') { msg.channel.send('已確認,輸入資料中...'); //與舊資料比對,沒有此身分組資料清除 DeleteOldPartyPower(); if (DoData[0] != '') { GetGas.postPartyPower(DoData, function(dataED) { if (dataED) { //bot內變數不會更新,手動更新 PartyPowerData.unshift({ 'ID': DoData[0], 'type': DoData[1], 'Power': DoData[2] }); msg.channel.send('輸入完畢!'); } else { msg.channel.send('資料輸入失敗,請重新嘗試'); } CloseAllDoingFunction(); }); } else { msg.channel.send('輸入完畢!'); CloseAllDoingFunction(); } } else if (msg.content === 'N') { CloseAllDoingFunction(); msg.channel.send('已取消行為,請重新下達指令') } else { DoingCount--; msg.channel.send('無法辨識訊息,請輸入Y/N來選擇'); } break; } if (DoUserID !== '') DoingCount++; } catch (err) { CloseAllDoingFunction(); client.channels.fetch(msg.channel.id).then(channel => channel.send('發生意外錯誤,中斷指令行為,請重新下達指令!')) console.log('DeleteTagNowError', err); } } 創建刪除類方法 //用戶舊資料更新 function DeleteOldUserPower(msg, cmd, haveUserData) { //二次確認 if (haveUserData) { if (DoData[0] == haveUserData.userID) { let str = haveUserData.Joins; DoData[2] = str.toString().replace(DoData[2], ''); DoData[3] = haveUserData.IsAdmin; return true; } else return false; } else return false; } //權限組舊資料更新 function DeleteOldPartyPower() { if (PartyPowerData) { const tempPartyData = PartyPowerData.find(element => { return element.ID == DoData[0]; }) if (tempPartyData == undefined) { DoData[0] = ''; DoData[1] = ''; DoData[2] = ''; } } } 運行看看 成功 到此,番外的部分也說完了 與基本的內容不同,多說了post的API,以及程式碼的部份相對複雜且比起前面的篇幅,後續的文章大多都是直接貼了程式碼的順序,很少講解 想必讀起來十分艱澀吧?能讀到這裡的你是十分了不起的,恭喜你看完了這篇文章 儘管如此,這支程式仍然是不成熟的,筆者對每個功能盡量都只是點到為止,希望能把大部份的應用都帶到,後面便是要靠各位讀者根據自己遇到的需求,來改善加強他吧,相信只要努力堅持,完成後的機器人一定會帶給各位程式能力上的提升的 那麼,用Node.js製作後台零負擔的DiscordBot到此結束祝各位中秋佳節愉快 底下附上完整的bot.js,供參考 //#region 全域變數 const Discord = require('discord.js'); const client = new Discord.Client(); const ytdl = require('ytdl-core'); const ytpl = require('ytpl'); const auth = require('./JSONHome/auth.json'); const prefix = require('./JSONHome/prefix.json'); const GetGas = require('./Script/GetGas.js'); const shup = require('./JSONHome/shup.json'); //存放BaseExcelAPI資料 let BaseExcelData = false; let UserPowerData = false; let PartyPowerData = false; //持續執行方法 let nowDoFunction = false; let DoingCount = 0; let DoUserID = ''; let DoData = undefined; //#endregion //#region 登入 client.login(auth.key); client.on('ready', () => { GetGas.getBaseExcel(function(dataED) { if (dataED) { BaseExcelData = dataED //有資料 } GetGas.getUserPower(function(dataED) { if (dataED) { UserPowerData = dataED; } GetGas.getPartyPower(function(dataED) { if (dataED) { PartyPowerData = dataED; } console.log(`Logged in as ${client.user.tag}!`); }); }) }) }); //#endregion //#region message事件入口 client.on('message', msg => { //前置判斷 try { if (!msg.guild || !msg.member) return; //訊息內不存在guild元素 = 非群組消息(私聊) if (!msg.member.user) return; //幫bot值多拉一層,判斷上層物件是否存在 if (msg.member.user.bot) return; //訊息內bot值為正 = 此消息為bot發送 } catch (err) { return; } //續行方法 if (nowDoFunction && msg.author.id === DoUserID) { nowDoFunction(msg); return; } //字串分析 try { let tempPrefix = '-1'; const prefixED = Object.keys(prefix); //前綴符號定義 prefixED.forEach(element => { if (msg.content.substring(0, prefix[element].Value.length) === prefix[element].Value) { tempPrefix = element; } }); //禁言系統判斷 if (!IsShut(msg, tempPrefix)) return; //實作 switch (tempPrefix) { case '0': //文字回應功能 BasicFunction(msg, tempPrefix); break; case '1': //音樂指令 MusicFunction(msg); break; case '2': //機器人tag指令 TagFunction(msg, tempPrefix); break; default: BaseExcelFunction(msg); break; } } catch (err) { console.log('OnMessageError', err); } }); //#endregion //#region 基本指令系統 function BasicFunction(msg, tempPrefix) { const cmd = msg.content.substring(prefix[tempPrefix].Value.length).split(' '); //以空白分割前綴以後的字串 switch (cmd[0]) { case 'ping': msg.channel.send('pong'); break; case '老婆': msg.reply('你沒有老婆!!'); break; case 'myAvatar': const avatar = GetMyAvatar(msg); if (avatar.files) msg.channel.send(`${msg.author}`, avatar).catch(err => { console.log(err) }); break; // case 'test': // const testStr2 = msg.content.split(' '); // console.log(client.users.fetch(testStr2[1]).then(element => console.log(element.displayAvatarURL()))); // break; } } //#endregion //#region 音樂系統 //歌曲控制器 let dispatcher = new Map(); //歌曲清單 let musicList = new Map(); function MusicFunction(msg) { //將訊息內的前綴字截斷,後面的字是我們要的 const content = msg.content.substring(prefix[1].Value.length); //指定我們的間隔符號 const splitText = ' '; //用間隔符號隔開訊息 contents[0] = 指令,contents[1] = 參數 const contents = content.split(splitText); //因為會持續使用到,將群組ID獨立成參數 const guildID = msg.guild.id; switch (contents[0]) { case 'play': //點歌&播放歌曲功能 playMusic(guildID, msg, contents); break; case 'replay': //重播當前歌曲 replayMusic(guildID); break; case 'np': //當前歌曲資訊 nowPlayMusic(guildID, msg.channel.id); break; case 'queue': //歌曲清單 queueShow(guildID, msg.channel.id); break; case 'skip': //中斷歌曲 skipMusic(guildID); break; case 'disconnect': //退出語音頻道並且清空歌曲清單 disconnectMusic(guildID, msg.channel.id); break; case 'playList': //載入歌單 playListMusic(guildID, msg); break; } } //?play async function playMusic(guildID, msg, contents) { //定義我們的第一個參數必需是網址 const urlED = contents[1]; try { //第一個參數不是連結就要篩選掉 if (urlED.substring(0, 4) !== 'http') return msg.reply('The link is not working.1'); //透過library判斷連結是否可運行 const validate = await ytdl.validateURL(urlED); if (!validate) return msg.reply('The link is not working.2'); //獲取歌曲資訊 const info = await ytdl.getInfo(urlED); //判斷資訊是否正常 if (info.videoDetails) { //指令下達者是否在語音頻道 if (msg.member.voice.channel) { //判斷bot是否已經連到語音頻道 是:將歌曲加入歌單 不是:進入語音頻道並且播放歌曲 if (!client.voice.connections.get(msg.guild.id)) { //因為是第一次加入,宣告新的歌曲列表 musicList.set(guildID, new Array()); //將歌曲加入歌單 musicList.get(guildID).push(urlED); //進入語音頻道 msg.member.voice.channel.join() .then(connection => { msg.reply('來了~'); //const guildID = msg.guild.id; const channelID = msg.channel.id; //播放歌曲 playMusic2(connection, guildID, channelID); }) .catch(err => { msg.reply('bot進入語音頻道時發生錯誤,請再試一次'); console.log(err, 'playMusicError2'); }) } else { //將歌曲加入歌單 musicList.get(guildID).push(urlED); msg.reply('已將歌曲加入歌單!'); } } else return msg.reply('請先進入頻道:3...'); } else return msg.reply('The link is not working.3'); } catch (err) { console.log(err, 'playMusicError'); } } //?play 遞迴函式 async function playMusic2(connection, guildID, channelID) { try { //播放前歌曲清單不能沒有網址 if (musicList.get(guildID).length > 0) { //設定音樂相關參數 const streamOptions = { seek: 0, volume: 0.5, Bitrate: 192000, Passes: 1, highWaterMark: 1 }; //讀取清單第一位網址 const stream = await ytdl(musicList.get(guildID)[0], { filter: 'audioonly', quality: 'highestaudio', highWaterMark: 26214400 //25ms }) //播放歌曲,並且存入dispatcher dispatcher.set(guildID, connection.play(stream, streamOptions)); //監聽歌曲播放結束事件 dispatcher.get(guildID).on("finish", finish => { //將清單中第一首歌清除 if (musicList.get(guildID).length > 0) musicList.get(guildID).shift(); //播放歌曲 playMusic2(connection, guildID, channelID); }) } else disconnectMusic(guildID, channelID); //清空歌單並且退出語音頻道 } catch (err) { console.log(err, 'playMusic2Error'); } } //?disconnect function disconnectMusic(guildID, channelID) { try { //判斷bot是否在此群組的語音頻道 if (client.voice.connections.get(guildID)) { //清空歌曲清單 musicList.set(guildID, new Array()); //退出語音頻道 client.voice.connections.get(guildID).disconnect(); client.channels.fetch(channelID).then(channel => channel.send('晚安~')); } else client.channels.fetch(channelID).then(channel => channel.send('可是..我還沒進來:3')) } catch (err) { console.log(err, 'disconnectMusicError'); } } //?replay function replayMusic(guildID) { if (musicList.get(guildID).length > 0) { //把當前曲目再推一個到最前面 musicList.get(guildID).unshift(musicList[0]); //將歌曲關閉,觸發finish事件 //finish事件將清單第一首歌排出,然後繼續播放下一首 if (dispatcher.get(guildID) !== undefined) dispatcher.get(guildID).end(); } } //?skip function skipMusic(guildID) { //將歌曲關閉,觸發finish事件 if (dispatcher.get(guildID) !== undefined) dispatcher.get(guildID).end(); } //?np async function nowPlayMusic(guildID, channelID) { try { if (dispatcher.get(guildID) !== undefined && musicList.get(guildID).length > 0) { //從連結中獲取歌曲資訊 標題 總長度等 const info = await ytdl.getInfo(musicList.get(guildID)[0]); //歌曲標題 const title = info.videoDetails.title; //歌曲全長(s) const songLength = info.videoDetails.lengthSeconds; //當前播放時間(ms) const nowSongLength = Math.floor(dispatcher.get(guildID).streamTime / 1000); //串字串 const message = `${title}\\n${streamString(songLength,nowSongLength)}`; client.channels.fetch(channelID).then(channel => channel.send(message)) } } catch (err) { console.log(err, 'nowPlayMusicError'); } } //▬▬▬▬▬▬▬▬▬?▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ function streamString(songLength, nowSongLength) { let mainText = '?'; const secondText = '▬'; const whereMain = Math.floor((nowSongLength / songLength) * 100); let message = ''; for (i = 1; i <= 30; i++) { if (i * 3.3 + 1 >= whereMain) { message = message + mainText; mainText = secondText; } else { message = message + secondText; } } return message; } //?queue async function queueShow(guildID, channelID) { try { if (musicList.get(guildID).length > 0) { let info; let message = ''; for (i = 0; i < musicList.get(guildID).length; i++) { //從連結中獲取歌曲資訊 標題 總長度等 info = await ytdl.getInfo(musicList.get(guildID)[i]); //歌曲標題 title = info.videoDetails.title; //串字串 message = message + `\\n${i+1}. ${title}`; } //把最前面的\\n拿掉 message = message.substring(1, message.length); if (message.length > 1900) message = message.substring(0, 1900); client.channels.fetch(channelID).then(channel => channel.send(message)) } } catch (err) { console.log(err, 'queueShowError'); } } //?playList async function playListMusic(guildID, msg) { try { //沒在音樂廳不能使用此功能 if (!client.voice.connections.get(guildID)) { msg.channel.send(`請先正常啟用音樂指令後,再使用歌單載入喔`); return false; } //網址 const valueED = msg.content.split(' '); //先用library自帶的方式檢查一次能不能用 const canPlay = await ytpl.validateID(valueED[1]); //讀取到幾首歌,上限默認100首 let a = 0; //幾首成功放入歌單 let b = 0; if (canPlay) { const listED = await ytpl(valueED[1]); await listED.items.forEach(async function(element) { a = a + 1; if (element.title !== '[Deleted video]') { canPlay2 = await ytdl.validateURL(element.url_simple); if (canPlay2) { b = b + 1; musicList.get(guildID).push(element.url_simple); } } }); //回傳統計資訊 msg.channel.send(`歌單 ${listED.title}\\n共載入${b}首歌曲\\n${a-b}首載入失敗`); } else { msg.channel.send(`This Url isn't working in function.`); } } catch (err) { console.log(err, 'playListMusicError'); } } //#endregion //#region tag系統 function TagFunction(msg, tempPrefix) { const cmd = msg.content.substring(prefix[tempPrefix].Value.length).split(' '); //以空白分割前綴以後的字串 switch (cmd[0]) { case 'AddUser': //將使用者加入身份組 addUserFunction(msg); break; case 'CreateParty': //創建身分組&增加身分組可tag對象 CreatePartyFunction(msg); break; case 'Delete': //從權限組中刪除使用者 OR 刪除權限組 DeleteTag(msg); break; default: //身份組ID tagOther(msg, cmd); break; } } //tag人 function tagOther(msg, cmd) { CheckID(msg, cmd, msg.author.id, (msg, cmd, haveUserData) => { CheckParty(msg, cmd, haveUserData, SendTagMessage); }); } //判斷此人有沒有登記資料 function CheckID(msg, cmd, userID, OtherFunction) { const haveUserData = UserPowerData.find(element => { return element.userID == userID; }) if (haveUserData !== undefined) { //有資料 return OtherFunction(msg, cmd, haveUserData); } else { return OtherFunction(msg, cmd, false); } } //根據UserPower獲得Party function CheckParty(msg, cmd, haveUserData, OtherFunction) { let havePartyPower; havePartyPower = PartyPowerData.filter(element => { if (haveUserData.Joins[i].indexOf(element.ID) != -1) { return element.Power.indexOf(cmd[1]) != -1 } }) if (isEmptyObject(havePartyPower)) { return OtherFunction(msg, cmd, haveUserData, false); } else { return OtherFunction(msg, cmd, haveUserData, havePartyPower); } } //傳送訊息單獨實例 function SendTagMessage(msg, cmd, haveUserData, havePartyPower) { if (haveUserData.IsAdmin) { msg.channel.send(`<@&${cmd[1]}>\\n來自管理員<@${msg.author.id}>的指令呼叫`); } else if (havePartyPower) { msg.channel.send(`<@&${cmd[1]}>\\n來自<@${msg.author.id}>的指令呼叫`); } else { msg.channel.send('無權限,請確認參數是否正確'); } } //將xxx加入身分組 function addUserFunction(msg) { try { if (DoUserID === '') { tempIsAdmin = CheckID(msg, null, msg.author.id, function(msg, cmd, haveUserData) { if (haveUserData.IsAdmin) return true; else return false; }); if (tempIsAdmin) { nowDoFunction = addUserFunctionNow; DoUserID = msg.author.id; DoData = new Array; msg.channel.send('請輸入要加入的使用者id'); } else { msg.channel.send('此指令只有管理員可執行'); } } else { msg.channel.send('有其他人正在使用續行中,請稍等'); } } catch (err) { console.log('addUserFunctionError', err); } } //將xxx加入身份組(續行方法) function addUserFunctionNow(msg) { try { switch (DoingCount) { case 0: DoData.push(msg.content); //加入使用者id DoData.push(msg.author.username); //加入申請者名字 msg.channel.send(`請輸入要加入的群組`); break; case 1: DoData.push(msg.content); //加入群組 DoData.push(false); //IsAdmin預設False不可修改 msg.channel.send(`申請資料如下:\\n申請人 <@${msg.author.id}>\\n使用者 <@${DoData[0]}>\\n加入權限組 ${DoData[2]}\\n正確 Y / 錯誤 N`); break; case 2: if (msg.content === 'Y') { msg.channel.send('已確認,輸入資料中...'); //與舊資料比對,已有此人資料變進行更新 CheckID(msg, null, DoData[0], EditOldUserPower); GetGas.postUserPower(DoData, function(dataED) { if (dataED) { //bot內變數不會更新,手動更新 UserPowerData.unshift({ 'userID': DoData[0], 'userName': DoData[1], 'Joins': DoData[2], 'IsAdmin': DoData[3] }); msg.channel.send('輸入完畢!'); } else { msg.channel.send('資料輸入失敗,請重新嘗試'); } CloseAllDoingFunction(); }); } else if (msg.content === 'N') { CloseAllDoingFunction(); msg.channel.send('已取消行為,請重新下達指令') } else { DoingCount--; msg.channel.send('無法辨識訊息,請輸入Y/N來選擇'); } break; } if (DoUserID !== '') DoingCount++; } catch (err) { CloseAllDoingFunction(); client.channels.fetch(msg.channel.id).then(channel => channel.send('發生意外錯誤,中斷指令行為,請重新下達指令!')) console.log('addUserFunctionNowError', err); } } //用戶舊資料更新 function EditOldUserPower(msg, cmd, haveUserData) { //二次確認 if (haveUserData) { if (DoData[0] == haveUserData.userID) { DoData[2] = haveUserData.Joins + ';' + DoData[2]; DoData[3] = haveUserData.IsAdmin; return true; } else return false; } else return false; } //用戶舊資料更新 function DeleteOldUserPower(msg, cmd, haveUserData) { //二次確認 if (haveUserData) { if (DoData[0] == haveUserData.userID) { let str = haveUserData.Joins; DoData[2] = str.toString().replace(DoData[2], ''); DoData[3] = haveUserData.IsAdmin; return true; } else return false; } else return false; } //創建身分組&增加身分組可tag對象 function CreatePartyFunction(msg) { try { if (DoUserID === '') { tempIsAdmin = CheckID(msg, null, msg.author.id, function(msg, cmd, haveUserData) { if (haveUserData) { if (haveUserData.IsAdmin) return true; else return false; } else return false; }); if (tempIsAdmin) { nowDoFunction = CreatePartyFunctionNow; DoUserID = msg.author.id; DoData = new Array; msg.channel.send('請輸入身份組名稱'); } else { msg.channel.send('此指令只有管理員可執行'); } } else { msg.channel.send('有其他人正在使用續行中,請稍等'); } } catch (err) { console.log('CreatePartyFunctionError', err); } } //創建身分組&增加身分組可tag對象(續行) function CreatePartyFunctionNow(msg) { try { switch (DoingCount) { case 0: DoData.push(msg.content); //身分組ID DoData.push('2'); //type 2 msg.channel.send(`請輸入要加入的tagID`); break; case 1: DoData.push(msg.content); //加入tagID msg.channel.send(`申請資料如下:\\n申請人 <@${msg.author.id}>\\n權限組 ${DoData[0]}\\ntagID ${DoData[2]}\\n正確 Y / 錯誤 N`); break; case 2: if (msg.content === 'Y') { msg.channel.send('已確認,輸入資料中...'); //與舊資料比對,已有此人資料變進行更新 EditOldPartyPower(); GetGas.postPartyPower(DoData, function(dataED) { if (dataED) { //bot內變數不會更新,手動更新 PartyPowerData.unshift({ 'ID': DoData[0], 'type': DoData[1], 'Power': DoData[2] }); msg.channel.send('輸入完畢!'); } else { msg.channel.send('資料輸入失敗,請重新嘗試'); } CloseAllDoingFunction(); }); } else if (msg.content === 'N') { CloseAllDoingFunction(); msg.channel.send('已取消行為,請重新下達指令') } else { DoingCount--; msg.channel.send('無法辨識訊息,請輸入Y/N來選擇'); } break; } if (DoUserID !== '') DoingCount++; } catch (err) { CloseAllDoingFunction(); client.channels.fetch(msg.channel.id).then(channel => channel.send('發生意外錯誤,中斷指令行為,請重新下達指令!')) console.log('CreatePartyFunctionNowError', err); } } //權限組舊資料更新 function EditOldPartyPower() { if (PartyPowerData) { const tempPartyData = PartyPowerData.find(element => { return element.ID == DoData[0]; }) if (tempPartyData !== undefined) { DoData[2] = tempPartyData.Power + ';' + DoData[2]; } } } //權限組舊資料更新 function DeleteOldPartyPower() { if (PartyPowerData) { const tempPartyData = PartyPowerData.find(element => { return element.ID == DoData[0]; }) if (tempPartyData == undefined) { DoData[0] = ''; DoData[1] = ''; DoData[2] = ''; } } } //從權限組中刪除使用者 OR 刪除權限組 function DeleteTag(msg) { try { if (DoUserID === '') { tempIsAdmin = CheckID(msg, null, msg.author.id, function(msg, cmd, haveUserData) { if (haveUserData.IsAdmin) return true; else return false; }); if (tempIsAdmin) { nowDoFunction = DeleteTagNow; DoUserID = msg.author.id; DoData = new Array; msg.channel.send('請問要編輯使用者權限還是權限組?\\n1 使用者權限 / 2 權限組'); } else { msg.channel.send('此指令只有管理員可執行'); } } else { msg.channel.send('有其他人正在使用續行中,請稍等'); } } catch (err) { console.log('DeleteTagError', err); } } //從權限組中刪除使用者 OR 刪除權限組(續行) function DeleteTagNow(msg) { try { switch (DoingCount) { case 0: switch (msg.content) { case '1': msg.channel.send('請輸入要編輯的使用者ID'); break; case '2': DoingCount = 10; msg.channel.send('請輸入要編輯的權限組'); break; default: DoingCount--; msg.channel.send('無法辨識訊息,請輸入1/2來選擇'); break; } break; case 1: if (msg.content == 'N') { CloseAllDoingFunction(); msg.channel.send('指令關閉'); } else { if (CheckID(msg, null, msg.content, (msg, cmd, haveUserData) => { return haveUserData })) { DoData.push(msg.content); //userID DoData.push(msg.author.id); //userName msg.channel.send('請輸入要刪除的群組權限'); } else { DoingCount--; msg.channel.send('此用戶不存在資料,請確認,如果要關閉指令請輸入 N'); } } break; case 2: DoData.push(msg.content); // Power DoData.push(false); // IsAdmin msg.channel.send(`申請資料如下:\\n申請人 <@${msg.author.id}>\\n使用者 <@${DoData[0]}>\\n刪除權限組 ${DoData[2]}\\n正確 Y / 錯誤 N`); break; case 3: if (msg.content === 'Y') { msg.channel.send('已確認,輸入資料中...'); //與舊資料比對,已有此人資料變進行更新 CheckID(msg, null, DoData[0], DeleteOldUserPower); GetGas.postUserPower(DoData, function(dataED) { if (dataED) { //bot內變數不會更新,手動更新 UserPowerData.unshift({ 'userID': DoData[0], 'userName': DoData[1], 'Joins': DoData[2], 'IsAdmin': DoData[3] }); msg.channel.send('輸入完畢!'); } else { msg.channel.send('資料輸入失敗,請重新嘗試'); } CloseAllDoingFunction(); }); } else if (msg.content === 'N') { CloseAllDoingFunction(); msg.channel.send('已取消行為,請重新下達指令') } else { DoingCount--; msg.channel.send('無法辨識訊息,請輸入Y/N來選擇'); } break; case 11: DoData.push(msg.content); //身分組ID DoData.push('2'); //type 2 DoData.push(''); msg.channel.send(`申請資料如下:\\n申請人 <@${msg.author.id}>\\n刪除權限組 ${DoData[0]}\\n正確 Y / 錯誤 N`); break; case 12: if (msg.content === 'Y') { msg.channel.send('已確認,輸入資料中...'); //與舊資料比對,沒有此身分組資料清除 DeleteOldPartyPower(); if (DoData[0] != '') { GetGas.postPartyPower(DoData, function(dataED) { if (dataED) { //bot內變數不會更新,手動更新 PartyPowerData.unshift({ 'ID': DoData[0], 'type': DoData[1], 'Power': DoData[2] }); msg.channel.send('輸入完畢!'); } else { msg.channel.send('資料輸入失敗,請重新嘗試'); } CloseAllDoingFunction(); }); } else { msg.channel.send('輸入完畢!'); CloseAllDoingFunction(); } } else if (msg.content === 'N') { CloseAllDoingFunction(); msg.channel.send('已取消行為,請重新下達指令') } else { DoingCount--; msg.channel.send('無法辨識訊息,請輸入Y/N來選擇'); } break; } if (DoUserID !== '') DoingCount++; } catch (err) { CloseAllDoingFunction(); client.channels.fetch(msg.channel.id).then(channel => channel.send('發生意外錯誤,中斷指令行為,請重新下達指令!')) console.log('DeleteTagNowError', err); } } //#endregion //#region 對話資料庫系統 function BaseExcelFunction(msg) { const messageED = GetBaseExcelData(msg); if (messageED) msg.channel.send(messageED); } //#endregion //#region 子類方法 //獲取頭像 function GetMyAvatar(msg) { try { return { files: [{ attachment: msg.users.author.displayAvatarURL('png', true), name: 'avatar.jpg' }] }; } catch (err) { console.log('GetMyAvatar,Error'); } } //BaseExcel字串比對 function GetBaseExcelData(msg) { try { if (BaseExcelData) { const userMessage = msg.content; e = BaseExcelData.filter(element => { return element.NAME === userMessage; }) if (e.length != 0) return e[0].VALUE; else return false; } } catch (err) { console.log('GetBaseExcelDataError', err); } } //禁言系統判斷 function IsShut(msg, tempPrefix) { //群組id const guildID = msg.guild.id; //頻道id const channelID = msg.channel.id; //當前狀態 let status = true; //先判斷群組,群組判斷完判斷頻道(頻道權限優先於群組) const guildIF = shup.Group.find(element => { if (element.GroupID == guildID) { return element.Power.indexOf(tempPrefix) !== -1; } return false; }) //找到資料 = 此群組存在Group中且Power存在此次指令代碼 if (guildIF !== undefined) { status = false; } //頻道 const channelIF = shup.Channel.find(element => { if (element.ChannelID == channelID) { return true; } return false; }) //找到資料 = 此頻道存在Channel中 if (channelIF !== undefined) { //Power有此資料=>禁用功能 無資料=>不設限 if (channelIF.Power.indexOf(tempPrefix) !== -1) { status = false; } else { status = true; } } return status; } //ArrayIsEmpty function isEmptyObject(obj) { return !Object.keys(obj).length; } //關閉續行方法 function CloseAllDoingFunction() { nowDoFunction = false; DoingCount = 0; DoUserID = ''; DoData = undefined; } //#endregion","link":"/2020/09/30/12thDay30/"},{"title":"Day4 - 機器人的家","text":"昨天我們成功創建了自己的機器人並且加入群組 可是機器人加入後怎麼都在睡覺,我怎麼找不到地方操作呢? 那是因為我們昨天做的事情是在Discord申請一個機器人帳戶而已~就像我們的Discord帳戶需要透過Discord登入,Bot其實也需要登入喔 不過Bot的登入路口是要”自己寫”的,今天我們就來叫機器人起床吧! 環境安裝首先請確保你已經安裝 Node.js安裝教學 Node.js是我們機器人會使用到的後端語言Node.js屬於弱型別語言,相較於其他語言,Node.js編譯前的限制較少,寫起來通常會感到較為自由不過如果對於自己寫的東西不夠了解,弱型別語言並不一定能幫你抓出問題,導致你的問題直到實際運行時才會發生;所以使用Node.js時、知道自己寫的東西具備甚麼效果是重要的 VSCode下載路徑中文化教學 VSCode是筆者愛用的編輯器VSCode本身體積很小,同時又支援許多的擴充套件,上面的中文化教學正是其中之一因此VSCode可以輕鬆的做到客製化,且不改變他的體積簡約。 如果同學們原本就有在使用的編輯器,VSCode可以跳過不安裝 蓋一個機器人的家首先我們要先替機器人做一個”家”,我們先在自己喜歡的地方建一個資料夾,名字先取作DiscordBot就可以了 一個機器人的家裡面,我們基本需要…機器人的工具箱(node_modules)機器人與工具箱的說明書(package.json)一隻機器人本體(bot.js)門牌號碼(auth.json) 像這樣子(看到package-lock.json是你業障重,別刪掉也別管他) 其中,node_modules跟package.json是透過node.js自動生成的,要生成這兩個東西,我們需要先在DiscordBot這個資料夾打開vscode 成功的話,左邊的路徑就會顯示資料夾名稱喔!然後選擇上方的開啟終端機->新增終端確認終端上面顯示的路徑是正確的後,在終端上面鍵入npm init 這是node的初始化行為會要你輸入一些關於這個project的基本資料,之後輸出在package.json 就像這樣! 然後我們再手動創建bot.js檔與auth.json檔 如果說bot.js是機器人的本體,那auth.json就是機器人的內部授權碼auth.json裡面的key代表的是機器人的啟動鑰匙,把鑰匙插進對應的地方才能啟動(有找到bot.js裡面有一行auth.key嗎?) 這邊我們把昨天在bot頁存下來的key放到your key value裡面,注意不要刪掉”” 兩個檔案都創建好後記得存檔,我們回到終端機輸入npm install discord.js 輸入 npm install discord.js後,node.js就會幫我們安裝discord.js這個工具然後把檔案放在node_modules裡面,再到package.json底下紀錄我們使用了哪些工具 沒問題的話,最後我們在終端機輸入node bot 成功!明天我們會再針對今天的程式碼做講解","link":"/2020/09/04/12thDay4/"},{"title":"Day5 - 函式庫文檔與基本範例講解","text":"昨天我們成功叫醒了自己的兒子/女兒 在繼續教育小孩(增加功能)前,今天想說說昨天安裝的 discord.js 這個工具 該怎麼使用,以及應用後的例子 discord.js 是 node.js 中的一個 library,也就是其他人寫好的程式集我們可以安裝他人發佈的程式並且引用,進而降低開發的難度與作業性 https://discord.js.org/#/ 這是 discord.js 的文檔庫,裡面有此庫作者撰寫的使用說明,涵蓋了從以前到現在的發行版本,以及各種小細節 進來後,我們點擊最下面的 Get Started 左側是 discord.js 的可用資源與方法,預設會在 welcome 頁面,這裡會介紹 discord.js 的功能與基本知識,我們先往下拉到 Example usage 這是 discord.js 的基本範例,拿來跟昨天的 bot.js 比對一下,是不是完全一樣呢?只是範例中的 client.login(‘token’); 被我拉上去了; 這是因為在筆者的腦中,給機器人輸入 key 值是最優先的事情,再來依次進入 ready(登入完成)->message(訊息接收)…不然各區塊的上下順序在這邊其實是沒有差別的。 下面說回來目前 bot.js 每一行的功效 const Discord = require('discord.js'); 這行的意思是引用 discord.js 這個工具,然後賦予到 Discord 這個常數(const)上之後如果要引用 discord.js 的程式碼,都可以直接調用 Discord 來實現! const client = new Discord.Client(); 新宣告一個 Discord(discord.js)下的 Client 方法,然後將 Client 方法的結果賦予到 client 這個常數上之後如果要引用 discord.js 底下的 Client,可以直接呼叫 client。 這邊我們額外從 Discord 中拉出 Client()是因為這個 client 是要用來當 bot 本體的,也就是我們的遙控器(x const auth = require('./auth.json'); 引用同目錄(./)下的 auth.json,賦予給 auth 這個常數之後想調用 auth.json 底下的資源,可以直接呼叫 auth。 client.login(auth.key); 執行 client 的登入行為,登入的 key 我們放入 bot 的 key client.on('ready', () => { console.log(`Logged in as ${client.user.tag}!`); }); 執行 client 的監聽(on)行為,要監聽的事件是 ready(準備完成)只要 client 收到 ready 事件,就執行右邊的箭頭函式( () => {} )箭頭函式的內容為 console.log(`Logged in as ${client.user.tag}!`); 在控制台打印(console.log) 出字串 Logged in as ${client.user.tag}!Logged in as 就是單純的字串其中 client.user.tag,我們可以從小數點來了解到,user 是 client 底下的一個可用變數,tag 則是 user 底下的一個可用變數最後輸出的結果就是機器人的名字與 id如果要仔細了解 client 的內容物,可以將 console.log 裡面的東西改成 client 看看 client.on('message', msg => { if (msg.content === 'ping') { msg.reply('pong'); } }); 監聽 client 的 message(收到訊息)事件,觸發後執行箭頭函式 msg =>{} 這邊的 msg 是每當 client 收到 message 時,discord.js 會給予我們的變數,我們將變數稱作 msg因為 discord.js 會回傳的變數是固定的,如果我們這邊像上面一樣寫成() => {}的話,雖然也可以執行但就不會將 discord.js 回傳的值再做處理。反過來說,如果我們宣告了 msg1 跟 msg2 兩個變數來接回傳值,因為 discord.js 的 message 事件並沒有給我們這麼多參數,所以 msg2 是接收不到東西的 那麼要怎麼知道 message 事件下到底回傳了哪些參數呢?這就要用到剛剛說的 discord.js 使用說明書了 https://discord.js.org/#/docs/main/stable/class/Client?scrollTo=e-message 我們監聽 message 事件的說明在左側元素列表的 client 分類中的 Events 中可以找到,可以看到他回傳了 Message Type 的變數,而這就是我們接收的內容。 繼續說箭頭函數內要做的事情 if (msg.content === 'ping') { msg.reply('pong'); } 當 msg 變數底下的 content 元素,等於 ping 字串時,執行方法 msg.reply('pong'); 使用 msg 底下的 replay 方法,並傳入 pong 字串。 以上的文言文翻譯過來就是機器人(client)接收到訊息(message)的時候,去判斷訊息的內容(content)是不是 ping如果是,回傳 pong(msg.reply) 先說到這,今天根據程式一行行做解釋,雖然很基本但對第一次觸碰這部分的人來說應該還是有點艱澀 請自行斟酌閱讀即可,明天我們說說如何讓機器人變得更聰明。","link":"/2020/09/05/12thDay5/"},{"title":"Day7 - 你的Bot需要一個前綴字","text":"今天接續昨天的主題,進一步修改message方法 正常我們在Discord上看到別人玩bot的指令都是有一個前綴字,後面附帶著指令的例如: !help $dice 之類的,今天我們要來完成這個需求,並且把前置防呆做好 (終於要開始來爆改啦) 前置判斷除了判斷訊息是否是機器人以外,我希望機器人只回應來自群組的消息 因為message物件屬於discord包好給我們的,擔心有哪一層物件的錯誤導致整個機器人崩潰,我希望在前置判斷增加嚴謹性與try catch 如果這一段出錯的話,可以在catch中log錯誤訊息喔! 字串分析我們希望可以在定義出前置符號後,只接取來自前置符號正確的內容,再判斷後面的內容 功能實作最後我們把之前的行為修改一下後放回去實作區 從原本的if改成switch,這樣我們如果要新增新的判斷式就會快速許多原本判斷的msg.content改成了cmd[0] 運行一下,結果就跟我們要的一樣了! 最後再稍微加一些範例 今天的完整程式碼如下 const Discord = require('discord.js'); const client = new Discord.Client(); const auth = require('./auth.json'); client.login(auth.key); client.on('ready', () => { console.log(`Logged in as ${client.user.tag}!`); }); client.on('message', msg => { //前置判斷 try { if (!msg.guild || !msg.member) return; //訊息內不存在guild元素 = 非群組消息(私聊) if (!msg.member.user) return; //幫bot值多拉一層,判斷上層物件是否存在 if (msg.member.user.bot) return; //訊息內bot值為正 = 此消息為bot發送 } catch (err) { return; } //字串分析 try { const prefix = '!' //前綴符號定義 if (msg.content.substring(0, prefix.length) === prefix) //如果訊息的開頭~前綴字長度的訊息 = 前綴字 { const cmd = msg.content.substring(prefix.length).split(' '); //以空白分割前綴以後的字串 //功能實作 switch (cmd[0]) { case 'ping': msg.channel.send('pong'); break; case '老婆': msg.reply('你沒有老婆!!'); break; case 'myAvatar': const avatar = GetMyAvatar(msg); if (avatar.files) msg.channel.send(`${msg.author}`, avatar); break; } } } catch (err) { console.log('OnMessageError', err); } }); //獲取頭像 function GetMyAvatar(msg) { try { return { files: [{ attachment: msg.author.displayAvatarURL, name: 'avatar.jpg' }] }; } catch (err) { console.log('GetMyAvatar,Error'); } } 完工~!","link":"/2020/09/07/12thDay7/"},{"title":"Day6 - 防呆觀念","text":"昨天我們大致說明了關於bot.js的運作今天對library文檔做一些補充,以及程序防呆概念 昨天我們介紹bot.js時,有提到client在監聽message事件時,會回傳一message物件 我們將message物件取名為msg,並且從msg中撈出content來檢查訊息內容,用reply來回傳訊息到訊息原本的頻道。 這些功能都可以從library文檔中找到,我們開啟昨天的discord.js文檔,然後在左側的功能列表中找到Message 這就是client監聽事件後帶回給我們的物件了,左側是變數,右側是方法可以看到content在左側,在右側可以找到reply 所以如果要對msg有進一步的調用,我們都必須來看文檔,了解這個library提供了哪些功能給開發者。 在前面,我多次強調client監聽,獲得message事件這件事情discord.js的功能使用是基於物件,也就是想做到甚麼事情、要先了解這件事情該調用哪個物件合適 這是我們昨天使用的回傳訊息的方式,也是discord.js包好給我們的方式 這是透過message物件獲得他所屬的channel(頻道)物件,再指定我要從這個channel底下send(發送)我要發送的訊息。 除非原本要做的行為很簡單,以及需要tag訊息來源user,這種情況才會使用reply不然正常我們是使用msg.channel.send,這種寫法就不會再在訊息前方自動tag使用者了,且傳入的內容也不局限於文字;之後如果要新增甚麼功能,也都是依這個邏輯下去文檔尋找。 讓我們回來到今天的主題,我們要稍微的修改一下我們的程式 為了讓bot之後更好修改內容,我們將msg.reply統一改成msg.channel.send 眼尖的同學應該注意到了,筆者除了修改reply以外,還把pong改成ping了這是為了後續的測試,同學們可以運行起來,看看效果 機器人的訊息傳送停不下來了!這是因為機器人傳送訊息的同時,也代表著client會再接到一個message事件,這是機器人自己的訊息,同時他也觸發了機器人的下一個訊息回應,這就導致了無限迴圈 同學們可以先在終端機上面用ctrl+c來強制中止程序 那麼,我們該怎麼迴避這個問題呢? 讓程序的回應不要跟判斷的句子一樣 判斷訊息來源 第一種作法就是說改成像之前一樣ping回pong因為判斷與觸發的句子不一樣,就不會有問題了但如果之後程序變得龐大,或是我們的觸發與回應句可以供其他人添加的話,第一種作法就會比較沒辦法達成我們的需求 第二種作法就是在client監聽到message事件時,先判斷訊息來源是否符合條件,套在現在的問題的話就是我們要判斷是不是bot這樣做,除了我們的程序不會再因為自己的話無限自閉以外,對於有多機器人的群組,也就不會去理會其他機器人的訊息了,大家各自服務。 只要msg.member.user.bot這個參數是true,就代表訊息來自於機器人,不會再處理下面的事情,是不是很簡單呢?","link":"/2020/09/06/12thDay6/"},{"title":"Day8 - 呃...他會需要更多前綴字","text":"昨天我們對message事件做了完善的前置判斷 如果你做的跟範例一樣,這時我們可以在 ! 後面加入任何字串,來命令機器人做對應的事情 假設之後機器人的指令不斷增加,除了單純的文字回覆,可能還會有查表,投票,管理員指令,權限控制與音樂功能等 這種時候、比起單純的只使用!,機器人支援多種前綴詞顯然是更好的分類手段今天我們進一步修改昨天已經完善的message框架,並且做出音樂功能(假) 首先,請幫我在DiscordBot資料夾內新增一個JSONHome資料夾,把auth.json放進去,然後新增一個prefix.json檔 (prefix.json的內容) bot.js的最上方幫我加載prefix.json (auth.json的路徑記得也要一併修改喔!) 我們把前綴字整理成了JSONArray物件這樣一來,我們就做到了前綴字的統整,之後不管是新增或調用參數都會方便許多 然後我們把下面的message事件改成這樣 簡單來說就是從原本只判斷驚嘆號,變成只要前綴詞符合prefix.json內的任一Value就給過,並且由tempPrefix來接受符合條件的參數同學們可以參考昨天的範例,來比對每一行的作用。 做到這裡,我們已經可以判斷多種前綴了,不過還沒在實作區判斷成功的是哪一個前綴原本我們打!ping,機器人會回pong現在打@ping也會通過了,如果prefix.json內的值不是@而是#或$$#@#$@甚麼的也一樣,依此類推切割字串的方式也是可以動態化的,不過筆者不在此贅述。我們繼續完善後續判斷 這樣,如果使用者輸入!xxx,就會進入上方的文字回應功能輸入@xxx,就會進入下方的音樂指令了 音樂指令的部份我們明天繼續製作,以下是今天的完整程式碼: const Discord = require('discord.js'); const client = new Discord.Client(); const auth = require('./JSONHome/auth.json'); const prefix = require('./JSONHome/prefix.json'); client.login(auth.key); client.on('ready', () => { console.log(`Logged in as ${client.user.tag}!`); }); client.on('message', msg => { //前置判斷 try { if (!msg.guild || !msg.member) return; //訊息內不存在guild元素 = 非群組消息(私聊) if (!msg.member.user) return; //幫bot值多拉一層,判斷上層物件是否存在 if (msg.member.user.bot) return; //訊息內bot值為正 = 此消息為bot發送 } catch (err) { return; } //字串分析 try { let tempPrefix = '-1'; const prefixED = Object.keys(prefix); //前綴符號定義 prefixED.forEach(element => { if (msg.content.substring(0, prefix[element].Value.length) === prefix[element].Value) { tempPrefix = element; } }); //實作 switch (tempPrefix) { case '0': //文字回應功能 const cmd = msg.content.substring(prefix[tempPrefix].Value.length).split(' '); //以空白分割前綴以後的字串 switch (cmd[0]) { case 'ping': msg.channel.send('pong'); break; case '老婆': msg.reply('你沒有老婆!!'); break; case 'myAvatar': const avatar = GetMyAvatar(msg); if (avatar.files) msg.channel.send(`${msg.author}`, avatar).catch(err => { console.log(err) }); break; } break; case '1': //音樂指令 msg.channel.send('music'); break; } } catch (err) { console.log('OnMessageError', err); } }); //獲取頭像 function GetMyAvatar(msg) { try { return { files: [{ attachment: msg.author.displayAvatarURL('png', true), name: 'avatar.jpg' }] }; } catch (err) { console.log('GetMyAvatar,Error'); } }","link":"/2020/09/08/12thDay8/"},{"title":"Day9 - 註解摺疊與音樂系統介紹","text":"昨天我們把機器人的架構做了些修改,對功能做了分類今天我們稍微整理一下程式,並且說說音樂系統;如果正在觀看文章的小夥伴不需要音樂系統可以跳過接下來三天的內容,不會影響後續教學的! region 開始寫音樂前,因為程式碼準備要開始增加了筆者先對所有程式做了分類#region#endregion被這兩段覆蓋的程式碼可以摺疊,摺疊後只看的到region後的字 這個功能必須IDE有支援才可以使用,VSCode支援這個好用的收納方法,所以我們就好好利用一下! 函式庫安裝那麼我們準備要開始音樂功能了喔!在正式開始撰寫前,我們先開啟終端機依序安裝npm install ffmpeg-staticnpm install opusscript跟npm install ytdl-core 記得路徑要在project的根目錄下 這些都是discordBot要播放音樂時需要的插件其中ytdl-core是後續抓取youtube歌曲的library,會需要透過程式來調用所以安裝完之後,我們還要在最上面引用ytdl-core 這樣一來,撰寫音樂系統的前置作業都算完成了!那麼接下來就該開始撰寫程式囉?nonono 實際上我們在撰寫程式前應該先去參考網路上是否有重複的功能可以參考 既然要寫音樂系統,那當然必需先參考其他人的音樂機器人是怎麼寫的囉! 音樂系統整理Discord 教學 - 如何簡單加音樂機器人進伺服器 (Rythm)https://fightwennote.blogspot.com/2018/06/discord-rythm.html Rythm是筆者在研究音樂系統前,看過最多次的音樂BotRythm的功能非常完善,觀察他的指令對於描繪心目中那個接收音樂指令的Bot架構十分有幫助 !play (網址) 提供youtube音樂的網址,bot需判斷網址是否符合規範,是不是抓得到歌並且判斷使用者是不是在語音頻道,一切都正常無誤後反饋音樂資訊並且播放歌曲 !replay 輸入此指令後,讓歌曲重頭開始播放與play一樣,如果使用者不在音樂頻道中,則此指令失效 !np 顯示當前播放歌曲資訊 !queue 顯示歌曲清單 !skip 跳過當前播放曲目 其他還有循環播放,單曲循環,請機器人退出語音廳等… 我們大概知道了,一個音樂系統需要前綴詞(!),表示我現在下的指令是音樂系統 音樂指令(play),表示我在音樂系統中要使用哪一個功能 內容(xxx.com.tw),不是一定會有,當指定功能需要參數時,我們需要給予他對應的內容 間隔符號(空格) 用來將音樂指令與內容分隔開的決定性符號","link":"/2020/09/09/12thDay9/"},{"title":"Day25 - 權限系統規格","text":"最近在編寫群組權限相關的功能,就說說這個吧 先說明此功能需求情境: 群組人數過多,管理層不希望群組人員可以使用 every 或 身分組 或 頻道等會群體呼叫的tag 但又希望在必要的時候,其他人可以使用此功能 因此希望將此權限關閉,並且給予機器人此權限透過機器人做二次權限管理,並且對使用人與時間等進行紀錄 為了完成需求,我們假設機器人權限是admin,我們需要… 三層身份組群主->管理員->自定義身份不是dc的身份組,是寫在機器人內的身份組身分組內有此人id->可以行使此身份組下所開通的功能例如管理員身份組下的人可以指派新的身份組,此身份組可以使用哪幾種tag要把誰加入身份組等 指派管理員可以將指定人員加入管理員身份組此指令只有群主身份組可以行使 創建新身份組創建自定義的身份組,會給予一組id,後續此id代表身份組 修改身份組名稱修改身份組名稱,便於管理,參數需帶入身份組id 新增身份組可tag內容新增身份組內可以tag的類型,參數需帶入身份組id與要tag內容的id 刪除身分組可tag內容同上 新增身份組成員將群組成員加入身份組,參數帶入身份組id與成員id 刪除身份組成員同上 使用tag透過bot tag指定id,參數需帶入tagID,可額外帶入要說的話","link":"/2020/09/25/12thDay25/"},{"title":"沉沉入睡的回憶小姐,匆匆揚帆的時間先生","text":"最初看到鐵人賽是大學二年級的時候,距離現在也就兩年,其實還挺短的 因為早早為專題做準備…需要查資料?造訪了這個地方 當時想都沒想過自己會寫鐵人賽(笑 都說大學的人生是最多采多姿的 中間遇到了許多事情 時常為了專題留校 遇到可以一起努力的人 偶而一起吃午餐 每天都感到充實 雖然不全是快樂的事情 有許多寶貴的回憶呀 想必有更多有趣的事情沒有去挖掘 有更多的日子可以去揮霍 不過也畢業了 也有許多事是已經錯過的 不會有機會再去嘗試了 最初參加鐵人賽的契機,表面上是想分享機器人的寫法,想增加自己履歷上的談資 現在想來,只是希望可以嘗試更多事情吧 想著也許這樣子做,我也會有甚麼改變 雖然到後來,鐵人賽真的讓我感覺蠻辛苦的 時常覺得,反正也不會有人來看我的文章 並不是說不努力,我花了很多時間來想要教甚麼才好 但就像自暴自棄一般的寫起流水帳了 十分抱歉 那麼,時間也差不多到了 有許多事情都還沒做 有得必有失,想必之後也會繼續過著得一失一的日子吧 儘管如此,我也會努力的 因為想變得比現在更好。","link":"/2020/09/30/12thDayEnd/"},{"title":"關於discord.js升級至13版本","text":"discord.js 是基於 node.js ,提供開發人員架設 discord bot 的一套 library 在去年的這個月,我寫了一篇關於使用 discord.js 架設 bot 的教學文章 而在今年的 8 月 13 號, discord.js 從 12 版升至 13 版 此改動影響了不少功能,聲明了以 message 事件為首的許多功能即將遭到廢棄(目前的 13 版仍然可用),且使用前必須先行宣告 partials 與 intents 至此,使用最新版本的開發者,理所當然的無法在網路上找到較為全面的教學,因為 13 版在一開始初始化時的寫法就與以往大相逕庭 針對於現況,繼續使用 12 版 library ,又或是試著自己摸索 13 版的寫法都不失為一種辦法 對此我也寫了一個模板,提供最基礎的套用 此專案最初的起因,出自於不斷重構 bot 的過程 考慮到 library 版本汰換的不方便,想將邏輯與函式分隔,中間用自己的方法重新宣告 如此,往後 library 的更新,我們可以最大限度的僅更新自身提供的方法即可,而不修改邏輯 截止於本文為止,此庫提供 12.5.3 與 13.1.0 的模板,可以直接輸入 bot key 套用 由於此模板偏向基礎的重構,目前尚未添加額外的功能 因此,此模板適合給 原本就對 discord.js 有一定了解的人 針對重構的方法沒有頭緒,同時又希望可以解決版本汰換問題的使用者 較不適合於 原本對 discord.js 較不熟悉的人 希望啟動後可以立即實現多種功能的人 不過,關於功能的部分,目前也有計畫開發;諸如基本指令,音樂系統等功能,未來預計會以插件的形式,另外放在其他 project 中。 Git","link":"/2021/09/27/aboutDiscordJs13/"},{"title":"關於 algolia 在 vitepress 集成,踩雷過程","text":"因為想增強網站的搜尋力度,希望連文章的內文都可以搜索到剛好 vitepress 是支援 algolia 的,試著配置不料遇到的問題比想像的多,網路上的教學像是缺了幾塊拼圖似的索性趁著剛完成腦袋還熱呼的現在,記錄一下遇到的問題。 官方文檔首先我們看到 vitepress 的文檔 去 Algolia 申請 api,然後填入,完成恩,看起來挺簡單的,再看看 Algolia 怎麼說 註冊完後,我們審核完畢就會寄信到你的信箱,裡面可以拿到 apikey恩…這邊也很方便阿,註冊完等大概五分鐘就收到信了 Hi there 👋 Thanks for your interest and trust in Algolia DocSearch. We've received your request for https://smilin.net/LoM-wiki/, and will get back to you soon. DocSearch is built in two parts: - A crawler which we run in our own infrastructure every week (configurable). It follows every link in your website and extracts content from every page it traverses. It then pushes this content to an Algolia index. (Read more: https://www.algolia.com/doc/tools/crawler/getting-started/overview/ ) - A JavaScript snippet to be inserted in your website that will bind this Algolia index to your search input and display its results in a modal UI. (Read more: https://github.com/algolia/docsearch ) If you want to find more details on how DocSearch works, take a look at the docs: https://docsearch.algolia.com/ Meanwhile, let us know if you have any other questions. Have a great day, The DocSearch team. DocSearch is powered by Algolia. See more at https://www.algolia.com/ 大意是說 algolia 分成兩個部分 他們會配置爬蟲,每周瀏覽網站 為了使用 algolia,必須在網站配置他們的 js 第一部份 algolia 會協助,第二部份也有 vitepress 集成,一切看起來都很美好 但是去 algolia 後台要拿 api 的時候,問題來了。 問題? (註:由於筆者已經踩完雷了,圖中是已解決的樣子) 只要進入後台,他就會彈出 Get Start大意是要求我們自己寫爬蟲讀取自己網站的資料,然後透過他們的工具上傳給 algolia 不對呀?爬蟲的部份algolia不是幫我們做好了嗎?上網找了下,其他人似乎沒有遇到這個狀況,甚至可以在後台要求 algolia 主動重爬 恩…回頭去看信中的第一點,點進他提供的網址,看看爬蟲相關的問題 痾,不知道為什麼,我的帳戶不能使用他們家的爬蟲服務難怪進後台就要求我主動提供資料,那沒辦法,只好自己來 隔天(8/13)更新:今天收到審核通過的信件,可以使用他們家的爬蟲了,應該是筆者當天查看時,還在審核網站是否符合他們的免費支援對象 往下就是自行串接爬蟲的部份了,如果還能利用他們家的爬蟲的話,以下的方案就不是必要的 github action由於筆者的網站放在 github 上想到要寫 for 網站的爬蟲後,第一時間想到的就是 action 啦 name: Algolia DocSearch Scraper on: push: branches: [release-algolia] jobs: scrape: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Prepare Algolia DocSearch config run: | echo '${{ secrets.ALGOLIA_CONFIG }}' > config.json cat config.json - name: Run DocSearch Scraper env: APPLICATION_ID: ${{ secrets.ALGOLIA_ID }} API_KEY: ${{ secrets.ALGOLIA_KEY }} run: | docker run \\ -e APPLICATION_ID=$APPLICATION_ID \\ -e API_KEY=$API_KEY \\ -e CONFIG="$(cat config.json | jq -c .)" \\ algolia/docsearch-scraper 透過 action 執行 algolia/docsearch-scraperid跟key可以在前面的algolia後台獲得ALGOLIA_CONFIG 則是爬蟲的相關 config,設置方式可以參考這裡 全都必須放在 Repository secrets,根據使用的環境不同,載入環境變量的方式略有差異 以上做完,action成功執行後,algolia上就會有資料囉 後續步驟 前面爬完資料,第一步會自動打勾由於我們的前端都由 vitepress 自動配置好了,剩下三步驟並不需要設定,一直送出讓他打勾就好 終於來到後台,依序點選 Search -> CONFIGURE -> Index找到 Create Index ,輸入 IndexName這裡設定的,就是最前面vitepress要求輸入的第三個參數 都設定完畢,網頁的搜尋功能連內文都可以搜到,更加強大囉~ 閒談文中提到使用 vitepress 的網站是 活俠傳 wiki,也是最近筆者在休息時間把玩的小專案 網站在 2024-07-01 建立,當時是打算在這寫點文章的,恰巧近期工作也忙,沒甚麼時間更新這邊 其實連這篇文都更新的很吃力,不過想了想,過幾天恐怕連這篇文怎麼動筆都不知道,還是寫吧 xD 建立這個網站,一方面是我自己對vue + vite體系全家桶不太熟悉,趁機學習 另一方面是活俠傳真的很好玩,這邊推薦大家都可以去玩。 關於wiki的建立心路歷程甚麼的,之後會再另外寫一篇的 很感謝同樣喜歡活俠傳的朋友,願意一同維護這個 wiki,也歡迎志同道合的新夥伴加入。","link":"/2024/08/12/algoliaOnVitepress/"},{"title":"Alist 單檔太大上傳失敗,思路整理","text":"註:本文並沒有完全解決遇到的上傳問題筆者只想到替代方案,曲線救國 筆者是自建雲端的愛用者,目前使用 Alist 前一陣子因為自身需求,添加了 Cloudflare 反向代理 原本一切看起來都很美好,但某天上傳檔案時才發現不對,只要檔案大於 100 MB 就有機會遇到 413 問題。 官方的反向代理配置 可以看到,文檔下大多也是哀鴻遍野,看來只要配置了反代很容易就會碰到這個問題 尤其文檔不支援 Cloudflare,實在頭痛.. 問題排查NGINX 設定 client_max_body_size參考資料時,大多資料都指向是 NGINX 設定的問題只要將 client_max_body_size 上限拉高即可解決 server { ......... location / { .... client_max_body_size 10G; .... } ......... } 不過筆者並沒有使用到 NGINX,此解顯然並非這次遇到的問題 Cloudflare 緩存問題爬文發現有人提到可能是 Cloudflare 緩存的問題 在 Rule -> Page Rules -> Create Page Rule 之後再到 Caching -> Cache Rules -> Create rule 設定完後再上傳,成功迴避掉 413 問題! 新的問題甜美的日子沒過多久雖然照著上述配置後,不會再出現 413 了,但.. 馬上就遇到新的錯誤了 QQ… 這個問題問 google 大神也沒甚麼好辦法沒辦法囉,只好再次自己動手檢查 測試過程Error Log 只寫了網路問題,偶而會提示 {"message":"A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received"} 猜測是 Cloudflare 提早關閉了連線 有趣的是只要同時下載檔案,上傳不會失敗 開啟 Cloudflare Development Mode ,上傳失敗 Cloudflare 免費版限制 Cloudflare 免費版用戶上傳檔案時,有著 100MB 的上限 Alist 僅支援單檔上傳github issue 想來或許就是在這關上卡住了 如開發者所說, Alist 的上傳存在一定的缺陷 就算不使用 Cloudflare,直連上傳過大的檔案也有機會失敗 好在除此之外功能正常,頂多不透過網頁,自行額外實現上傳方法即可(Ex:webdav/ftp/nasGui/local..) 文章參考:https://lanwp.org/12-cloudreve-nextcloud-alist-and-cloudflare_cdn/","link":"/2023/09/11/alist-file-error/"},{"title":"Alist 好用的自建雲端分享","text":"隨著 GOOGLE / ONEDRIVE 等空間限制增多 筆者轉為使用自架雲端方案一段時間,其中特別中意 Alist 的畫面 高自定義的 UI,強大的用戶管理最重要的是在目錄下默認讀取 readme.md 做介紹! 整理雲端檔案,最常遇到的難題就是多年後很難輕易在一堆檔案中找到自己需要的資料 除此以外還支援 元數據(載入特定目錄會跳出的訊息) 文件搜索 雲端掛載(GD/OD/MEGA/還有一堆..) 使用 Alist 不僅能讓雲端變得美觀,還能很輕鬆的管理文件 Alist 官方文檔 從文檔首頁可以感受到,Alist 有著強大的功能 因為支援中文,在閱讀文檔時不會遇到障礙 環境建置筆者使用 Docker 運行 Alist docker pull xhofe/alist:v3.13.2 docker run -d --restart=always -v {你的本機目錄}:/opt/alist/data -p 5244:5244 -e PUID=0 -e PGID=0 -e UMASK=022 --name="alist" xhofe/alist:v3.13.2 記得將上方的 {你的本機目錄} 替換成你的環境 Container run 起後使用這段指令查看預設帳號密碼 docker exec -it alist ./alist admin 登入成功後我們會回到首頁,這裡目前甚麼都沒有 在網址後方加入 @manage 進入控制台 這邊先將管理員帳密改成你好記的樣子 在存儲可以添加需要加入到 Alist 的空間從基本的本地環境到雲端都可以放在 Alist 管理 設置雲端掛載時記得根據 Alist 版本,查看對應的文檔,留意任何留言,記得備份~ 筆者這邊以本地環境做示範 首先到設定 {你的本機目錄} 的地方,創建一個資料夾 這會做為未來我們掛載本地檔案的路徑 驅動選擇 本地存儲 掛載路徑 /{資料夾名稱} 根資料夾路徑 /opt/alist/data/{資料夾名稱} 設定完大致如上 回首頁看就會有空間了! 實用 CSSAlist 後台的 “設置” ,可以調整絕大多數的 UI 畫面不過如果想自定義一些細節,推薦使用 CSS 達成 設置 -> 全域設定 -> 自定義頭部 可以在這做自定義 CSS 的添加以下提供一些不錯的樣式 去除網站圖標與搜索<style>.hope-stack.hope-c-dhzjXW.hope-c-PJLV.hope-c-PJLV-iiOacaA-css {display: none!important;}</style> 站點公告去除 X 關閉按鈕<style>.notify-render .hope-close-button{display: none;}</style> 使用背景圖(亮色背景)(GIF 可用)<style>.hope-ui-light{background-image: url("")!important;background-repeat:no-repeat;background-size:cover;background-attachment:fixed;background-position-x:center;}</style> 使用背景圖(暗色背景)(GIF 可用)<style>.hope-ui-dark {background-image: url("") !important;background-repeat:no-repeat;background-size:cover;background-attachment:fixed;background-position-x:center;}</style> 列表改透明(亮色背景)<style>.obj-box.hope-stack.hope-c-dhzjXW.hope-c-PJLV.hope-c-PJLV-igScBhH-css{background-color: rgba(255, 255, 255, 0.5) !important;}</style> 列表改透明(暗色背景)<style>.obj-box.hope-stack.hope-c-dhzjXW.hope-c-PJLV.hope-c-PJLV-iigjoxS-css{background-color:rgb(0 0 0 / 50%) !important;}</style> 元信息改透明(亮色背景)<style>.hope-c-PJLV.hope-c-PJLV-ikSuVsl-css{background-color: rgba(255, 255, 255, 0.5)!important;}</style> 元信息改透明(暗色背景)<style>.hope-c-PJLV.hope-c-PJLV-iiuDLME-css{background-color:rgb(0 0 0 / 50%)!important;}</style> 去除尾頁<style>.footer {display: none !important;}]</style> 移除下載選項<style>.hope-select__trigger.hope-c-kvTTWD.hope-c-huZphZ.hope-c-kvTTWD-hYRNAb-variant-filled.hope-c-kvTTWD-gfwxhr-size-md.hope-c-huZphZ-cIGthf-cv.hope-c-PJLV.hope-c-PJLV-ijSQbqe-css{display: none !important;}</style> 推薦一些網站:Alist 魔改代碼分享CSS 參考","link":"/2023/09/06/alist-started/"},{"title":"自動領取 Pixai 每日獎勵","text":"工程網址GithubDocker Hub 近幾年 AI 工具日新月異我也有幸接觸了 AI 繪圖的一鱗半爪 其中 pixai 是少有的雲端免費算圖網站 在 pixai 如果對產出來的圖感到滿意的話可以透過 AI 進一步運算,產出會動的圖 說是免費,其實還是有些限制的 在網站上的運算行為都需要消耗點數,無論是靜態或動態 而點數除了透過付費與活動取得以外pixai 每日都有一萬點數可供會員領取,也就是每日獎勵 只要每天領取就可以免費算圖了,整個佛心來著對吧 不過筆者最喜歡花費幾個小時來搞定原本一分鐘可以做到的事情(x auto-pixai 輸入帳號跟密碼,該腳本執行一次就會自動進網站領取每日獎勵 使用 node.js 撰寫,另有 Docker 容器化,開箱即用 docker pull smile0301/auto-pixai docker run -e LOGINNAME=<你的帳號> -e PASSWORD=<你的密碼> --name <container-name> smile0301/auto-pixai","link":"/2024/04/13/autoPixai/"},{"title":"BDB更新日誌#2 - discord.js v14.6.0","text":"主要更新 將專案適配到 discord.js v14.6.0 版本 全版本專案棄用 auth.json ,改成 .env 次要更新 message 邏輯整合 修改部分註解 githubgithub 頁面 一些話大家好,我是微笑 這次版本更新,在基本架構上跟 13 並沒有差太多主要是修改了之前 code 的一些架構設計,讓主體更加精簡了一些,以及使用更加正規的方式儲存私密數值 之後如果時間允許,希望可以將自己的 bot 提升到 14.6.0 的版本(12 遇到的 bug 越來越多了)屆時或許會再將功能拆分,更新到這邊吧,不過因為已經真鹿太多次了,已經有點不好意思給承諾了,各位看看就好吧,哈哈哈 主要更新說明原本專案使用 json 做參數管理最近筆者因為換了上雲平台,重新研究了一次相關資料,這次索性將 auth.json 棄用,統一改成 .env利用 dotenv 的效果,就可以用 process.env. 的方式載入各種環境參數了 次要更新說明另外比較重要的修改,就是原本將 message 入口放在主程序,看起來挺奇怪的,就跟 prefix.json 一起重新統合到獨立的分類了以及在註解上,也重新做了一輪調整,讓文件間的註解存在統一性,相對不會太過雜亂","link":"/2022/10/31/bdb2/"},{"title":"BDB更新日誌#1","text":"主要更新 bot 啟動時自動註冊斜線命令 次要更新 斜線命令相關邏輯調整 修改 js 檔名 修改部分註解 githubgithub 頁面 一些話大家好,我是微笑 總覺得很久沒發文,自上次簡單介紹 discord.js 升版後有四個月了… 與以往渾渾噩噩不同,架設了 blog、有了實際的紀錄後更可以感覺到自己必需努力(雖然平常更多時間都在放電 haha) 原本新年後第一篇文章,希望可以寫點想說的話;可之前沒有相關經驗,一到真的要動筆時總是不知道該寫些甚麼。結果就拖到現在了,實在是有點慚愧(抹臉 雖然不是 2022 第一篇,相關的雜談就留待日後吧~這邊做為更新日誌,會盡量以介紹更新為主的,恩恩。 主要更新說明敘述上挺好懂的,BDB(BaseDiscordBot)原本並不會在註冊時自動跑斜線指令。原本的 code 有點問題,這次改了寫法,並且拉進了Client#ready事件內。以後執行時,就會將 bot 所在的所有群組都註冊一次斜線指令 次要更新說明原本 BDB 在升級到 discord.js 13 時,目標著重在保全 功能模組化 此一優點上,斜線指令的兼容,並沒有花費過多的時間去研究。 此次除了主要更新中提到的內容,還修改了斜線指令被觸發時的邏輯,將 reply 一同寫在 json 內,以利於往後擴充。 改動簡易,優點也是顯而易見的,有效避免日後斜線指令過多,註冊與回傳列表不同步的風險。 基於 功能模組化 訴求,針對這一塊的下一步優化,預計會是在程式碼上做出子母 json 檔案;可以根據要使用的功能別,來直接套用各個功能對應的 json。 原本 BDB 的啟動檔取名做 bot.js,因筆者自身寫的 bot,檔名都是 alice(女兒)。BDB 畢竟也是我的專案,怎麼可以有差別待遇呢(不是乾脆就一併統一成愛稱了。 另外調整了一點點註解上的形容。","link":"/2022/01/17/bdb1/"},{"title":"淺談 js 深拷貝與淺拷貝的差異","text":"Deep copy 和 Shallow copy 先來個考題: a = { foo: "bar" }; b = a; b.foo = "baz"; console.log(a.foo); // 印出? 答案 baz 下一題: a = { foo: "bar" }; b = structuredClone(a); // 深拷貝 b.foo = "baz"; console.log(a.foo); // 印出? 答案 bar 深拷貝(Deep Copy) 可以將內層對象一併拷貝 Shallow copy 淺拷貝(Shallow Copy) 與深拷貝同樣是用來拷貝物件層級,避免指向同一記憶體位置 與深拷貝不同的是,淺拷貝只會複製第一層的對象,如果是 Object.Object 的結構就沒轍。 Object.assign Object.assign 屬於淺拷貝(Shallow Copy)在上述案例中,可以得到跟深拷貝一樣的結果 a = { foo: { fpp: "bar" } }; b = Object.assign({}, a); b.foo.fpp = "baz"; console.log(a.foo.fpp); // 印出baz 解構賦值 解構賦值是 ES6 以後的語法糖,同樣屬於淺拷貝 const a = { b: 1 }; const c = { ...a }; // 解構賦值 c.b = 2; console.log(a); // { b: 1 } 得益於其精簡的代碼,實務上很常使用。 Deep copy 與前面提到的淺拷貝不同,深拷貝對於深層結構也能一併複製 早期的深拷貝JSON.parse(JSON.stringify()); 這個寫法大致上有以下缺點: 忽略 function 忽略原形鏈 忽略 undefined 子層太多會導致 stack overflow 儘管如此,由於已經可以處理大多狀況如果不是為了性能或是特殊邏輯,此寫法已經夠用,是常見的深拷貝實現。 structuredClonestructuredClone 是 node.js 17 版以後支援的官方深拷貝實現 目前各大瀏覽器默認支援此語法 structuredClone 存在一些限制 不允許結構中存在 Error 、 Function 以及 DOM 對象 不保留 RegExp 對象的 lastIndex 不保留 read-only 等描述符,即無法限制 setters getters 不保留原形鏈","link":"/2023/09/15/deepCopyAndShallowCopy/"},{"title":"BDB更新日誌#3","text":"主要更新 DiscordJSmySelf 更名為 BaseDiscordBot discord.js 的所有參考都塞進 BaseDiscordBot 斜線 / 選項 / 按鈕 / 菜單 框架完成 次要更新 env 更新 readMe 更新 githubgithub 頁面 一些話 嗨,昨天才見面呢 最近比較閒,忽然就可以比較常更新日誌了 其實原本有點懶得寫,但 BDB 目前的狀態,跟之前相比算是有了非常大的改變所以就稍微紀錄一下,雖然、大概、沒人看就是了 xD ~~ 主要更新說明 首先,最重要的就是,DBD 的核心文件做了一次更名啦 ~~ 筆者實在是對命名很不在行,原本的想法很單純,想寫一套屬於我的翻譯文件 用來翻譯 discord.js 的 API ,這樣以後 discord.js 改版的時候,就不用再把原有的邏輯拆掉重組了 新的名字與專案相同,也算是重新確立了本專案的方向 (啪嘰啪嘰~) 雖然認真的朋友應該早就看出來了,其實筆者的程式水平並不怎樣呢,也難怪會當受薪階級了 (x)不過筆者也沒有因此放棄,目標一直都是在程式的道路上磨練,所以相較於以往,對程式的理解還是有提高的喔 這次花了些時間整理,正式將所有與 discord.js 有關的 import 都塞入 BDB 內了也就是以後使用 BDB,就真正可以做到換一個檔案 -> 升級完畢,的這種事情了 ~~雖然只是初衷一般的事情,也是最近稍微閒下來才終於可以整理好啊..感覺審視了一次自己的作業效率阿 (汗) 以及相比前兩個比較小咖,但也算是主要更新的discord.js 13 版引入,14 版改過一次實例方式的各種功能都做出框架了雖然沒能在 13 版時就做出來有點遺憾,但筆者對目前的框架很有自信,相信等 15 版出來的時候,這些 code 也會很容易維護吧! 次要更新說明 在env的部分加上了 `MASTER_ID`,並沒有實際功能 更多是用於 DEBUG,或是往後要開一些只有自己能用的開發人員指令時可以使用 因為更新內容眾多, readMe 也做了一次更新,改了不少,但還是缺很多東西,只交代了最基本的內容畢竟使用 BDB 相當於重新認識一種 API ,未來想開一份專屬於 BDB 的文檔不知道還要多久就是了,請大家等等我囉 xD ~","link":"/2022/11/01/bdb3/"},{"title":"discord.js 升上 14 版,架構說明","text":"github 連結 從 2021 年,discord.js 升上 13 版heroku 改成收費youtube 不喜歡 discord 蹭他們的服務音樂機器人相繼關閉 yt 服務12 版許多功能時常報錯discord.js 升上 14 版.. 期間不管是工作又或是休假時,都很希望能升級以前寫的機器人不斷想重構出更好維護的程式架構,也一再推翻之前的程式 終於..!在最近 Alice 也正式升上了 discord.js 14.11.0 版本不會總是因為舊版本不支援而爆炸啦!(誤) 安裝套件必備 Node.js v16.9.0 或以上 Discord.js v14.11.0discord.js 核心套件npm install [email protected] dotenv v16.0.3讀取 .env ,即 token 的套件npm install [email protected] 點我展開BDB(baseDiscordBot.js)所需套件 @discordjs/builders v1.3.0discord.js 提供的類別產生器類型npm install @discordjs/[email protected] 點我展開音樂系統所需套件 @discordjs/voice v0.16.0控制 discord 語音的核心套件p.s.使用舊版本極度容易出現問題,如果播放過程發生 bug 可以先檢查 voice 是不是最新版npm install @discordjs/[email protected] @discordjs/opus v0.9.0Opus 編碼器npm install @discordjs/[email protected] ffmpeg-static v5.1.0ffmpeg 轉碼器npm install [email protected] libsodium-wrappers v0.7.11串流加密工具npm install [email protected] play-dl v1.9.6 串流套件,取代 ytdl-corenpm install [email protected] 點我展開Render託管推薦套件 axios v1.4.0打 http 使用的套件npm install [email protected] node-schedule v2.1.0定時任務套件npm [email protected] 前置動作如果是舊版 discord bot ,要先去 discordDeveloper選中自己的 bot 後,選擇左邊 Bot 選項,然後將這邊的開關都打開 這是一些限制機器人存取特定資訊的開關,默認是關閉的,如果沒有打開,就算在程式中要求存取權,也是拿不到這些資訊的喔! 之後在專案根目錄創建一個 .env 檔案,性質類似於以前教學中的 auth.json差別在於,放在 Environment 的參數意味著參數不該被公開,不會在任何的公開場合獲得此類 value (例如 github),僅在執行專案時會被注入 .env 預覽TOKEN="your bot token" MASTER_ID="your client ID" 專案結構 點我展開專案結構 AliceZero/├─ baseJS/│ ├ BaseDiscordBot.js│ ├ CatchF.js│ ├ CronTask.js│ ├ HealthCheck.js├─ manager/│ ├ buttonManager/│ ├ ├ commands/│ ├ ├ ├ helpNowQueue.js│ ├ ├ ├ helpPause.js│ ├ ├ ├ helpPlay.js│ ├ ├ ├ helpPlayFirst.js│ ├ ├ ├ helpResume.js│ ├ ├ ├ helpSkip.js│ ├ ├ ├ helpSleep.js│ ├ ├ ├ helpTrpgDice.js│ ├ ├ ├ helpTrpgSort.js│ ├ ├ ├ myKiritoSkillNicename.js│ ├ ├ ├ myKiritoSkillSkill.js│ ├ ├ ├ myKiritoSkillStatus.js│ ├ ├ buttonC.js│ ├ ├ buttonM.js│ ├ ├ buttonType.json│ ├ componentManager/│ ├ ├ componentM.js│ ├ embedManager/│ ├ ├ embedC.js│ ├ messageManager/│ ├ ├ messageC.js│ ├ ├ messageM.js│ ├ ├ messagePrefix.json│ ├ ├ messageUpdateM.js│ ├ ├ nineData.js│ ├ musicManager/│ ├ ├ musicC.js│ ├ ├ musicM.js│ ├ mykiritoManager/│ ├ ├ requests/│ ├ ├ ├ boss.js│ ├ ├ ├ level.js│ ├ ├ ├ skill.js│ ├ ├ myKiritoC.js│ ├ ├ myKiritoM.js│ ├ selectMenuManager/│ ├ ├ commands/│ ├ ├ ├ help.js│ ├ ├ selectMenuC.js│ ├ ├ selectMenuM.js│ ├ slashManager/│ ├ ├ commands/│ ├ ├ ├ help.js│ ├ ├ ├ m.js│ ├ ├ slashM.js│ ├ trpgManager/│ ├ ├ trpgC.js│ ├ ├ trpgM.js├─ .env├─ alice.js├─ package.json├─ package-lock.json 因為這篇不是教學,不會一個個講解,大概說明一下各 Manager 的作用 BaseDiscordBot.js從登入 token 到訊息傳送與 discord.js 的任何交互都在這,唯一引用 discord.js 的地方好處是當 discord.js 改版時只要更改 BDB 即可壞處是其他地方的邏輯可能會比較難以理解,都需要點進 BDB 查看 CatchF.js自定義的 log 工具,改這裡就可以一次更改所有的 log style CronTask.js託管平台用到的工具 HealthCheck.js同上 alice.jsnpm start 的執行檔,敘述了啟動時會執行的內容 slashManagerdiscord.js 13 版以後新增的斜線指令,包含其註冊與監聽的方法都寫在這commands 可以看出這個 bot 目前有多少指令(本次範例來說有 help 跟 m 指令) messageManager傳統 bot 對文字訊息回應的主要行為,messageUpdate 訊息更新觸發的行為也放在這 selectMenuManager菜單組件,commands 可以看出這個 bot 目前有多少菜單組件 buttonManager按鈕組件,commands 可以看出這個 bot 目前有多少按鈕組件 embedManager嵌入式訊息組件,@discordjs/builders 有著 EmbedBuilder 這個 embed 產生器避免往後的更新要改一堆地方,在 BDB 中被繼承完才給 embedManager 使用 componentManager組件管理器,當訊息非單純的文字訊息,有使用到 菜單 / 按鈕 / 嵌入訊息 任一組件時,會從這裡拿 musicManager音樂相關邏輯,musicM 負責定義邏輯,musicC 實例實際內容,與 play-dl 等套件互動 trpgManager派對系統,目前只會骰骰子,而且 code 還是從舊版直接搬過來的.. mykiritoManager攻略組系統,提供 mykirito 大群的資訊查詢,雖然很久沒更新,但仍然還有人在使用,所以也更新過來了。 github 連結 現在的架構算是終於確定下來,以後會在這個架構上繼續更新不過畢竟是 side project ,架構中有些地方整理的比較草率如果之後寫教學,會重新寫一個 bot 的 看到這裡的朋友,如果在寫 bot ,但苦於不知該如何下手的話這裡推薦可以看看 藍莓大大 的文章淺顯以懂,最後甚至是給了乾貨,可以直接載了拿去用~ 或是使用 我的 BDB除了像是 mykirito 這種比較偏門的功能,其他 alice 會的指令都會慢慢更新在 BDB 專案上,可以自由取用~ 感謝看到這裡的你^^!","link":"/2023/06/01/discordJs14-1/"},{"title":"Github Action 學習紀錄","text":"Auto-Pixai 之前撰寫的 auto-pixai經過多次調整,基本修復了大部分的 bug 該專案透過爬蟲,提供自動在 pixai 簽到的功能 考量到便利性,將專案打包成 docker image,實現無狀態的部屬環境,最後透過 github tag 控制版本歷程。 整合部屬需求雖然 User 用起來是方便了,但每次開發部版都需要進行複雜的手續.. CI/CD …好..好想要 CI/CD 阿..就在這麼想著的時候,想起了.. Github Action! Github Action Github Action 是 Github 提供的 CI/CD 方案 由 Github 提供整合環境,在統一的無狀態環境下進行整合 最重要的是,它對於 public repository 完全免費! Github Action 官方簡中文檔在學習 github action 的過程,官方文檔幫助了我許多 因為有官方翻譯,在專有名詞的學習上也不容易被混淆。 需求? 做為 CI/CD (自動整合/自動部屬) 的角色,我希望他可以在我推送 release 的時候,去做幾件事—— 根據 package.json 檢查版本 自動創建新版本代號 根據 Dockerfile 產出 Docker image 將 image 標上版號,推至 Docker Hub 並且由於 Github Action 還提供緩存功能,如果將 npm install 拉到 Github Action,搭配緩存可以有效縮短 Dockerfile 的產出時間與大小! 配上 Github Action 的一些格式後,我們還需要—— 指定 node 版本 緩存儲存 node_modules 緩存加載 node_modules 根據官方文檔所述,7 天沒使用的 cache 會自動回收,並且一個 repository 的所有 cache 加總不可超出 10GB - name: Cache node modules id: cache-node-modules uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- 根據 package-lock.json 的變動來決定是否新建緩存,否則就取出原本的 node_modules,以此加速 CD 流程。 過程 搭啷,經過整理後,這是目前的 CI/CD 流程 由於沒有實作測試,僅有 release 被推送時需要 CI/CD 執行首先檢查 tag ,若 package 版號有變動則創建新版本 同時進行 cache 的載入,若是找不到 cache 則重新 npm install 兩邊都做完後,進行 Dockerfile 的 building 與 pushing。 結果大功告成!原本繁瑣的整合部屬流程 Push New Version Dockerfile build Docker image tag Docker push tag Docker push latest … 上面列的事情 通通不用 只要在 main 寫完代碼,要推送版本的時候合併到 release,最後再用自動產生的 tag 生成 Release 說明即可。 結語怎麼說呢…好爽 不過所謂爬蟲簽到甚麼的,說白了只是個小工具 藉著這次 project,心血來潮地想把 repository 弄得有模有樣 結果就是,花在打扮(?)外觀的時間遠遠超出了爬蟲本身的開發時間 感覺對 CICD 有了更深刻的了解;美中不足的大概是爬蟲的測試並不好寫,沒能在這次 action 中寫入自動測試,是比較可惜的。","link":"/2024/04/30/githubAction1/"},{"title":"fluid支持pjax主題源碼分享","text":"寫在前面本主題基於 fluid 1.8.11 版本製作,在此之上參雜了許多私貨 又因為本人前端並不熟練,源碼被改得亂七八糟的,因此有任何問題在blog聯繫我詢問即可。 再此感謝開發 and 維護源碼的所有大大們。 fluid 主題主要改動 pjax 支持 添加本地音樂箱 添加右下角 Live2D 妹子 pjax JS 回調(解決大部分 fluid 不支持 pjax 之異常) 目錄頁次浮動顯示 文章列表搜出留言數 Markdown 介面修改 文章底部上下篇連結位置對調(上一篇就該在左邊,下一篇就該在右邊 = = ) 修復繁體中文字型 rss 功能 環境配置hexo -v INFO Validating config hexo: 5.4.0 hexo-cli: 4.3.0 主題下載 https://github.com/Mr-Smilin/hexo-theme-fluid.git hexo _config 配置請添加以下 rssfeed: type: atom # RSS的类型(atom/rss2) path: atom.xml # 文件路径,默认是atom.xml/rss2.xml limit: 20 # 展示文章的数量,使用0或则false代表展示全部 hub: content: # 在RSS文件中是否包含内容 ,有3个值 true/false默认不填为false content_limit: # 指定内容的长度作为摘要,仅仅在上面content设置为false和没有自定义的描述出现 content_limit_delim: ' ' # 上面截取描述的分隔符,截取内容是以指定的这个分隔符作为截取结束的标志.在达到规定的内容长度之前最后出现的这个分隔符之前的内容,,防止从中间截断. live 2Dlive2d: enable: true scriptFrom: local pluginRootPath: live2dw/ pluginJsPath: lib/ pluginModelPath: assets/ tagMode: false log: false model: use: live2d-widget-model-shizuku display: position: right width: 150 height: 300 mobile: show: false react: opacity: 0.7 更多 live2D 模組可訪問 https://smilin.net/2021/11/16/live2DShareList/ 音樂箱音樂開啟 source\\dist\\music.js const ap = new APlayer({ container: document.getElementById('aplayer'), fixed: true, mini: true, autoplay: false, loop: 'all', volume: 0.7, listFolded: true, listMaxHeight: 60, audio: [ { name: '最近在聽的歌', artist: '星茶会', url: '/music/星茶会.mp3', cover: '/img/avatar.png', }, { name: '最近在聽的歌', artist: 'Fullmetal Alchemist Brotherhood', url: '/music/Fullmetal-Alchemist-Brotherhood.mp3', cover: '/img/avatar.png', } ] }); 對應路徑檔案可替換 配置好後,做 hexo 上傳 sop $ npm install #安裝library(僅第一次執行) $ hexo clean #清除 $ hexo g #編譯 $ hexo d #上傳git 以上就是配置此 theme 的流程,使用上若有遇到問題歡迎在底下詢問 期待這篇文章可以幫助到需要的人fluid 是本 blog 第一個使用的 themefluid 的設計…非常讚!(詞窮)逛著 blog 的期間,可以從各處巧思中感受到作者對美感的一套見解 不過 fluid 因為其框架的侷限,其致命傷便是無法支援 pjax(ajax)做局部頁面更新從 issues 上可以看到,對於 pjax 的開發目前是不了了之 但這並非是 fluid 無法支援 pjax,而是因為引入 pjax 會破壞 fluid 既有框架也因此這些改動要改進正式版本是十分困難的,但如果只是魔改的話,儘管是像筆者對前端並不精熟,也能試著修改(code 不保證好看就是了 xD) 因為筆者最近可能會試著替換成其他主題,故想記錄下使用版本,算是分享。","link":"/2021/11/16/fluidThemeShare/"},{"title":"我的人生不需要英文","text":"學不會,沒動力學,沒必要學 最近,大概半年,持續沒甚麼動力學習只是乏味的原地踏步,工作著 以前明明每天都會抽空看一點知識的 原本覺得為了找到更好的工作,我應該開始加強英文,補回我以前怠惰學習的債務 事實是我對這東西真的完全沒有興趣 這幾天放棄學習英文後,逐漸找回了那個可以繼續對學習程式有熱忱的自己 感覺有點悲哀,但又很歡喜 也許這個人就是如此,既然沒辦法揠苗助長,那就順其自然吧。","link":"/2024/04/11/giveUp/"},{"title":"blog 網址搬遷 & github改名","text":"再見 Nalocal最初使用 Nalocal,做為 Github 上使用的名字 想著這個名字的發音還挺順口的,近似於貓咪的喵叫聲 拆開來看的話有非本地,支持雲端概念存在的好名字.. 不過我的名字實在太多啦! 在 FB 上叫做漣漪,網名叫微笑,換個論壇又會有別的名字 而且對於名字是否順口好記,也是近期挺在意的事情 經營 blog,似乎逐漸從原本單純的樣貌,變成必需慎重考慮該如何營造的一個個人品牌了 所以,趁此機會一併更改了網域,github page 提供的免費網域終究存在侷限 雖然替換初期會有點痛苦,往後也算是重新經營了 但我想這會是必要的,也希望以後我能堅持多寫些文章,添加柴火。","link":"/2023/03/17/goodbyeNalocal/"},{"title":"Hello Smilin","text":"20210916這是這個 blog 創建的第一天 眾所周知,”Hello World”是所有程序猿最開始學習的第一句話語 結合我自己的名字,我想把 blog 的第一篇文章標題寫成 Hello Smilin,我覺得這樣挺有意思的。 你好,我是微笑,GitHub 的名字是 Mr.Smilin 基於方便,叫我微笑就好了 身為台灣廉價碼農的一員,工作上會需要爬文,參考大神的文章 時常看到其他碼農會寫自己的 blog,不管是熱門的,或是門可羅雀的 也因此,創建 blog 的想法從在學期間就醞釀著 在此 blog 前,也有其他 微笑家 建起,之後因為各種原因又廢棄掉 最近因為 discord.js 的更新,希望有一個可以紀錄 side project 的地方,此站便應運而生 希望這次能堅持下去,這也是創建 blog 的初衷。","link":"/2021/09/16/hello-smilin/"},{"title":"將專案從heroku轉到render過程思路","text":"前言大家好,我是微笑 繼上次發文過了九個月了 很可怕啊,感覺自己好混,哈哈哈 疫情期間,遇到了不少事情,最近才終於有一點調整回正軌的感覺 剛好 Heroku 發生了一些問題,需要搬移程序到其他託管平台,這邊順便 水一點文章 關於發生在 Heroku 上的一些問題Heroku 在10月初時,寄送給了開發者一封信 裡面提到,Heroku 將於2022年11月28號以後,全面關閉免費方案的主機 如果繼續使用 Heroku 的話,根據目前定價方案,一台託管主機需要負擔一個月 7 美元的成本 對於單純在學習的學生,又或是我這種程序用愛發電沒有利益的行為,顯然是十足的噩耗 也因此,原本在 Heroku 上使用免費方案的使用者,開始到處尋找可以繼續使用的平台。 多樣化的選擇因應 Heroku 收費化的開始,有許多平台陸續浮現到開發者的眼前,其中甚至有網站的標題是「Migrate from Heroku to Railway」 可以看到,除了 Heroku 以外仍有許多平台等待著開發者發掘(Fly.io/Railway.app/GoogleCloudPlatform - CloudRun等..) 雖然根據專案不同,沒有所謂最好的選擇,不過筆者在多次嘗試後,最後決定將程式搬遷到 Render 上 render.com 當初搬遷主機時,最看中的點就是希望能盡量不用改動原本的架構,免費額度足夠 Render 目前有著每月 750 小時的免費額度,只開一台機器的話等同免費 並且他支援從 GitHub / GitLab 等開源平台專案部署的方式,功能單一讓流程簡化 除了支援許多語言直接部署,也支援 Docker 映像檔部署,這幾乎是現在主流平台都有的功能了。 那因為筆者的專案使用 Node.js,剛好在 Render 支援的語言列表內,所以設定好之後,將專案推到 Github,他就會自動做部署行為了 部署流程 第一次部署肯定會比較麻煩的 首先我們要先用 Github 帳號登入 Render 註冊帳號,進來後他會先告訴你,免費方案如以下 Render 提供許多方案,這邊我們要找到 web services 才能使用免費主機 之後取得 Github 授權 repository,就可以載入專案,選擇主機地區,語言等設置 比較需要注意的是,因為 Render 的 web 每個半小時無人訪問,會進入休眠(記得 Heroku 也有這類設定) 需要再次訪問網站才能讓他喚醒 以 Node 來說,我們可以使用 request 跟 node-schedule ,呼叫自己防止進入睡眠的方式,來讓真正需要的程序能不間斷運行 env 以前在 Heroku ,如果不透過 Github 自動部署,而是用 Heroku Git 的話,是可以直接將較為私密的 key 等資料,直接明碼上傳上去的 雖然不是很好的做法,但是對於私人專案來說,這的確是個很方便的做法,只上傳在 Heroku 也有效的保障了程序的安全性 但是 Render 只接受 Github 鏡像部署,私密的 key 是無論如何都不會放在 Github 上的 Render 在 env 的設定上也是十分方便,官方流程可參考這篇文檔 單一的 key 要放在 Environment Variables 如果原本就習慣使用 .env 管理所有參數的話,可以將檔案的內容複製進 Secret Files Render 在 env 有個好處是,一般平台設置 env 後,基於安全性,平台都不會讓使用者在前台存取 env 的真實資料 但是 Render 可以,而且還可以直接修改內容,雖然必須犧牲一些安全性,但這樣也方便了開發者對值的管理,對筆者來說是利大於弊 結語 筆者當初使用 Heroku,是因為接觸了 discord bot,為此還寫了鐵人賽 說長不長說短不短的兩年,Heroku 宣告收費化,對於筆者這樣的使用者來說,就像是一個平台的關閉一般 這兩年從後端摸到前端,再從前端學回後端,因為疫情也遇到了不少事情,原本覺得搬遷主機,對於筆者這點能力來說一定是一件艱鉅的工程 不過在搬遷的過程中,重新拾起 Node,摸索對筆者最好的平台時,感受到許多愉快,有一種 原來我還是能快樂寫 code 阿,的感想 我想 sideProject 就像是開發者們心靈的綠洲吧,能因為這次機會,重新澆灌他,我也收穫頗豐。 文末附上去年內部員工自己寫的推薦文","link":"/2022/10/25/herokuToRender/"},{"title":"競速疊屍腳本 - 台版彈射世界","text":"腳本運作環境型號redmi note 8 pro 解析度1080x2340 如果你跟筆者用的是同解析度甚至同一台手機欸、恭喜、我們貼貼 大部份體驗會很良好 適配機型(缺乏資料,歡迎各位踴躍提供)缺乏資料,歡迎各位踴躍提供 載點腳本精靈 apk 雲水小號腳本三件套 安裝教學下載上方兩個檔案後,安裝腳本精靈 開啟腳本精靈,給予權限後,點選畫面中下方的 製作直到彈回主畫面後,即可關閉腳本精靈 這時候,手機根目錄會出現一個叫做 自动精灵 的資料夾將雲水小號三件套解壓,將檔案放入該資料夾 回到腳本精靈就會看到此腳本 使用教學請使用新角色開始遊戲,並且手動玩到 1-4-2,打完貓頭鷹並解鎖 auto 系統之後運行腳本即可(不用再手動玩到貓頭鷹) 會這樣設計是因為大家解析度不同,很有可能我寫完的腳本你不能用而開啟 auto 後的腳本邏輯相對簡單,理論上相對不會有 bug,所以將腳本啟動點放在這 無論如何都希望可以從創角開始就全自動開啟腳本,請點選 雲水小號全自動,之後點右下角的編輯 腳本出現後,開啟遊戲,到主畫面 請確認當前是處於新角色且從未登入狀態,如果不是,請從右上角設定中 **刪除資料** 之後將腳本拉到第六點 **創角流程** (圖中第四點是舊的,錯誤的),長按後會出現 **從這裡開始運行** 點選後就會從創角開始自動循環了。 如果我在運行過程因為 BUG/充電/手不小心按到等原因,導致中斷執行了,可以再繼續運行腳本嗎,還是只能重刷可以繼續運行腳本,不用重刷,這邊介紹一下腳本的大概流程腳本大概可以拆成幾個區塊,由上到下依序是 1.開啟 auto&跳過&下一步 2.打雲水囉 3.重開小號 4.關閉&開啟彈射世界 5.創角流程 6.手操&跳過&下一步 7.腳本手操(以上名稱皆對應腳本描述) 開啟 auto&跳過&下一步指通關貓頭鷹(1-4-2)以後,到第 1 章全通關,這期間的所有關卡都會在這個分類內循環如果在這期間中斷了腳本,回到有 new 關卡的地方啟動腳本即可 打雲水囉指打通第一章最終 boss 並通過最後兩個故事,獲得第一章武器寶珠 精靈的微笑 後,會切換到此流程此分類涵蓋從第一章退出,進入活動頁,進雲水,被送出來結算後因此,如果打完第一章後忽然中斷,想從這裡繼續執行是可以的但因為打完雲水就要換帳號了,個人是建議各位直接手動進雲水送死,重新刷號比較快。 重開小號指被雲水送出來後,回到標題畫面刪除帳號資料這一段區間是不會無限執行的,跟上一個分類一樣,卡在這建議直接手動 關閉&開啟彈射世界指刪除完角色資料後,重新啟動彈射世界,以釋放部分記憶體跟上一個分類一樣,卡在這建議自己重啟,並且改用 記憶體負載 版本 創角流程指從主畫面進入,直到領完登入獎勵&夏日登入獎勵這段時間如果創角期間有任何原因中斷腳本,可以長按這個分類並且選擇 從這裡開始運行 手操&跳過&下一步指領完登入獎勵,1-1-1 到 1-4-2 期間的所有流程如果是在打完貓頭鷹前就發生意外,導致腳本中斷的話請回到有 new 關卡的地方,長按這個分類並且選擇 從這裡開始運行 腳本手操此分類與上一個分類連動,當運行手操流程,進關卡時,會進入此分類無論任何情況都不會從這個分類中繼續執行 若是因為解析度問題,導致這個腳本在你的環境中有一堆問題請改為使用 雲水小號半自動此版本只會協助自動通關貓頭鷹後的關卡&進雲水送頭這一段的邏輯相對簡易,如果遇到問題各位也可以試著修改。 一些話 一些廢話 想寫這個腳本的想法,從修車大開始寫的雲水行前手冊時就有了; 時常一起玩的朋友沒有完美雷拳盤,希望可以繼續一起玩這款遊戲,大概是像這樣的想法。 雖然現實似乎是連完美盤也只能哭,能不能幫到他已經是未知數了 官方在競速開始前一刻,加強了模擬器的驗證機制, 這對於腳本的普及是毀滅性的; 模擬器可以提供的是統一的硬體規格與解析度, 關閉模擬器並不能遏阻使用外掛的使用者。 對於腳本的分享,會因為要考慮到機種規格不同,遇到極大的考驗, 這是十分打擊熱情的。 儘管如此,活動開始幾天後,知道版上需要腳本的人仍佔大多數,死馬當活馬醫,姑且還是嘗試著寫了一下,雖然已經預想的到腳本會有多難推廣了(扶額); 希望各位願意點進這篇文章、願意幫助他人的你,哪怕只是想搶救下自己的排名,也別忘記保持善心, 不用理會其他人說腳本是外掛這類反智言論,純粹的理念會是你堅持使用腳本的秘訣。 常見問題Q: 腳本精靈自行停止運作A: 通常是因為記憶體回收機制,自動將腳本精靈關閉導致,這時再次開啟腳本精靈也會有問題,請重開機,並且將腳本精靈設定成不可自動回收的應用程式(每家手機廠設定方式不同)Ex: Q: 我的角色打關卡死掉,腳本就不會動了A: 欸….我測試期間至少刷了 20 隻帳號,發文當下也在刷,目前沒有死過亞里沙打 1 章跟鬼一樣,1 章的難度應該….是不會死的萬一真刷出死掉的狀況…我..我再想想辦法。 Q: 我的腳本擅自將遊戲關閉後,不會重新啟動A: 部分機種不會讓腳本可以開關遊戲程式,筆者自己也是這一類機型,請使用 雲水小號全自動(記憶體負載)使用此版本不會自動重啟遊戲,大約 1 小時(刷 1~2 隻角色後),需手動重啟遊戲,否則整隻手機會因為記憶體塞滿卡住。p.s.就算是使用 雲水小號全自動 的使用者,也推薦每隔一小時查看一次手機,尤其是一開始幾分鐘,最好一直看著,腳本終究只是腳本,只要出現突發狀況就會炸掉 當然也有筆者寫得不好的原因 Q: 動作運行失敗,已暫停A: 通常是運行期間閃退,導致部分權限拿不到,重開機解決 Q: 截屏為空,請確定是否開啟了其他截屏軟件A: 關閉其他允許顯示在應用上層的軟件後再嘗試 Q: 運行失敗&卡在奇怪的地方不繼續動了A: 請依照上方教學,從一般運行改為編輯模式,可以試著調整抓取圖片解析度,或是抓圖範圍,使腳本適配於自身機種並且提供機型&解析度&卡住的地方,在下方留言給我,又或是透過巴哈站內信給我,以利資料蒐集&幫助更多的人 不知道該怎麼留言時,可以試著使用的留言格式 機種(必填): 解析度: 卡住的地方(必填): 狀況的補充說明: 想說的話: 截圖:","link":"/2021/12/14/racingScript-worldFlipper/"},{"title":"公開Live2D模型蒐集分享","text":"此文章來源來自 https://www.cnblogs.com/strengthen/p/11112215.html此文章僅用於教學,不進行任何營利行為引用時間 2021-11-16 https://unpkg.com/[email protected]/assets/chitose.model.json https://unpkg.com/[email protected]/assets/epsilon2_1.model.json https://unpkg.com/[email protected]/assets/gf.model.json https://unpkg.com/live2d-widget-model-haru/[email protected]/assets/haru/01.model.json https://unpkg.com/live2d-widget-model-haru/[email protected]/assets/haru/02.model.json https://unpkg.com/[email protected]/assets/haruto.model.json https://unpkg.com/[email protected]/assets/hibiki.model.json https://unpkg.com/[email protected]/assets/hijiki.model.json https://unpkg.com/[email protected]/assets/izumi.model.json https://unpkg.com/[email protected]/assets/koharu.model.json https://unpkg.com/[email protected]/assets/miku.model.json https://unpkg.com/[email protected]/assets/ni.model.json https://unpkg.com/[email protected]/assets/nico.model.json https://unpkg.com/[email protected]/assets/nietzsche.model.json https://unpkg.com/[email protected]/assets/nipsilon.model.json https://unpkg.com/[email protected]/assets/nito.model.json https://unpkg.com/[email protected]/assets/shizuku.model.json https://unpkg.com/[email protected]/assets/tororo.model.json https://unpkg.com/[email protected]/assets/tsumiki.model.json https://unpkg.com/[email protected]/assets/unitychan.model.json https://unpkg.com/[email protected]/assets/wanko.model.json https://unpkg.com/[email protected]/assets/z16.model.json","link":"/2021/11/16/live2DShareList/"},{"title":"Rust 學習紀錄[0] = 前言","text":"2021 年 2 月,Rust 基金會成立 以 AWS、GOOGLE 等多家資訊巨頭為首因為看好 Rust 兼顧了高效能 & 安全性而投資使得 Rust 這兩年有了巨大的成長。 這也是為什麼,近幾年工程師無論如何不願意多少也會聽過 Rust 的名號,隱隱有與 C++ 並駕齊驅的勢頭。 筆者平常習慣 node.js or java 的開發,接觸 Rust 算是偶然剛好想嘗試所謂能真正實現高效能程式碼(沒碰過 C++) 做為學習動力,一方面會將自己的學習過程發布在 blog另一方面打算在學到一個階段後,試著用 Rust 構建一個微型社群平台,當作目標。 本次系列與其說是教學,更像是日記一類的東西,如果能激起看這篇文的你的興趣的話,我會很高興的。","link":"/2023/10/27/rust_learning_00/"},{"title":"Rust 學習紀錄[1] = 日誌中的教學","text":"工欲善其事,必先利其器想學一門語言,要從一篇教學文檔找起 官方文檔 恩,官方文檔看起來挺不錯的,就這個吧 這篇文撰寫當下,文檔對應 Rust 版本為 Rust 1.67.1 (released 2023-02-09) or later如果因為版本不同造成閱讀的困擾的話,可以在學會安裝 Rust 後自行降版學習。 安裝 說是 Rust,其實第一個遇到的是 rustup Window 安裝 rustup 安裝過程一直 Enter 就好,之後在 cmd 下 rustc --version 得到版本號 rustc 1.73.0 (cc66ad468 2023-10-03),表示安裝成功 其他比較常用的指令還有 更新 Rust 版本 - rustup update 卸載 Rust 跟 rustup - rustup self uninstall 查看 Rust Doc 本機離線版 - rustup doc 根據 Rust 自己的說明,約莫每兩周會有一次小版更新也因此,除非目標是維護專案,理論上更新版本 & 追蹤文檔改動會很頻繁。 HELLO RUST! 首先讓我們創建一個資料夾 rust_project 往後任何的 Rust 程式都會放在這個資料夾下現在在專案資料夾下新增我們要製作的第一個 Rust 程式 rust_project\\_01_hello_rust\\main.rs fn main() { println!("Hello, Rust!"); } 之後打開 cmd ,輸入以下 rustc main.rs .\\main.exe // 印出 Hello, Rust! 如此,我們完成了第一隻 Rust 程式。超快!!嘛、畢竟是 Hello World 嘛 感想 首先注意到的,是執行的指令拆成了兩個分別是 編譯 的行為與 執行 的行為 編譯出來的檔案是 .exe,意味著寫好的程式不需要借助 Rust 就能運行這在筆者之前的經驗中是比較少見的 同樣被編譯出來的還有一個 main.pdb暫時不知道是做甚麼用的,之後學到再回來更新 println!();在這段酷似 JAVASCRIPT 風格的 JAVA 式命名輸出語法上,突兀的出現了個 ! 這是 Rust 的 macro比起 Rust 的 function,macro 更接近 JAVASCRIPT 的 function Rust 中,存在 fn(function) 跟 macro_rules(macro)他們的差別主要在於 function(函式) 的參數數量是固定的而 macro(巨集) 則可以動態傳入參數 println 預期要能夠傳入多個參數,當他要做格式化傳輸時 println!("Hello, Macro! My name is {}!", "Smilin") 比起 fn,macro_rules 顯然更符合需求。 今日小結 rustc --version 驗證版本 rustup update 更新 Rust rustup self uninstall 反安裝 Rust rustup doc 運行 Rust Local Doc rustc main.rs 編譯 rs 檔 .pdb ??? macro 巨集 / 宏,可以傳入動態參數 function 函式 / 方法,宣告時就要規範好參數數量與型別 資料參考 Rust 官方文檔 Window 安裝 rustup","link":"/2023/10/27/rust_learning_01/"},{"title":"Rust 學習紀錄[2] = Rust 的 NPM","text":"讓我們接著原本的進度繼續 Cargo Cargo 是 Rust 的專案建置工具以及套件管理器恩..聽起來是個 npm 我們在安裝 rustup 時已經一併安裝了 Cargo使用 cargo --version 來確認是否正確安裝 cargo --version // cargo 1.73.0 (9c4383fb5 2023-08-26) 沒問題的話,接著使用 Cargo 創建跟昨天相似的專案。 創建專案 在專案目錄(rust_project)下輸入創建專案的指令 cargo new _02_hello_cargo 現在我們有名為 _02_hello_cargo 的資料夾,裡面結構如下 src main.rs .gitignore Cargo.toml src - 常見程式開發檔案目錄,看就知道 src\\main.rs - 主程式 .gitignore - git 的描述文件,主要功能是防止裡面提到的檔案在 git 傳輸時被包進去(Ex:log/target) Cargo.toml - 打開來看了下,應該是專案描述文件,對應 node 的 package.json 編譯(build) 試著 build 起這個專案看看cargo build 執行後,專案內多出了幾個檔案 target debug _02_hello_cargo.exe more debug files... .rustc_info.json CACHEDIR.TAG Cargo.lock Cargo.lock - 對應 node 的 package-lock.json target\\debug\\_02_hello_cargo.exe - 我們產出的執行檔,debug 大概是 building 的默認方式,之後應該會有相對嚴謹的方法 .\\target\\debug\\_02_hello_cargo // Hello, world! 編譯並運行(run) Cargo 有提供一種命令,可以將編譯與運行合併成一個指令 cargo run // Hello, world! 如果開發檔案沒有修改,cargo run 不會重建 target加上合併兩個步驟,比 cargo build 方便許多。 檢查(check) 除了編譯與運行,Cargo 當然也提供了檢查命令 cargo check /** Checking _02_hello_cargo v0.1.0(C:\\my\\01\\git\\rust\\_02_hello_cargo) Finished dev [unoptimized + debuginfo] target(s) in 0.07s **/ 單純的 check 比 build 快上許多在自動化部屬等一類場景中廣泛運用。 正式編譯(release) cargo build --release /** Compiling _02_hello_cargo v0.1.0 (C:\\my\\01\\git\\rust\\_02_hello_cargo) Finished release [optimized] target(s) in 0.36s **/ 使用 --release building 的檔案會放在 target\\release與 debug 版本不同,release 的編譯過程比較久,但會最佳化產出的結果 因此,使用 debug 開發可以有效降低等待編譯的時間需要發佈檔案時,再使用 release。 今日小結 cargo --version 驗證版本 cargo new {project_name} 創建專案 cargo build 編譯專案 cargo run 編譯&運行專案 cargo check 驗證專案 cargo build --release 正式版本的編譯","link":"/2023/10/27/rust_learning_02/"},{"title":"使用 AddToAny 分享箱,踩雷過程","text":"網站之前有配置分享箱的系統,使用 sharejs 依賴該依賴只要套上預設即可,十分方便 不過 sharejs 最新的 release 已經是 2016 年的事了… 實際上有些按鈕已經過時,無法使用 關於本主題的分享箱適配 icarus 本身是支援多種分享功能的由於 sharejs 停止維護,官方建議採用別的分享功能 AddToAny 看了一圈,決定改用 AddToAny 其一是因為該插件支援的社群足夠多,有持續在維護 與 blog 代碼沒甚麼交集,自定義客製方便 踩雷過程簡單選一下想要的按鈕,產出代碼後,扔進 jsx 內 到此就搞定了(超快~),build 看下輸出 hmmm…為什麼會是一片空白呢? 沒有 CSS ?看了一下,似乎是 css 沒有載入 不過 css 這些內容應該會隨著 js 一併輸出才對 花了些時間尋找問題,之後發現 原來是被瀏覽器擋住了,會被瀏覽器攔截主要有幾個可能性 混和內容(Mixed Content):如果網站是透過 HTTPS 協定提供服務的,而嘗試載入的資源(如 page.js)使用的是 HTTP 鏈接,那麼這種「混合內容」可能會被現代瀏覽器封鎖。 確保所有外部載入的資源都使用 HTTPS 來避免這種問題。 安全策略(CSP):瀏覽器可能會因為安全策略(Content Security Policy)而阻止某些腳本的載入。 跨域問題(CORS):服務商有可能因為沒有提供適當的跨域策略(Cross-Origin Resource Sharing),導致資源無法載入 廣告攔截器:一些廣告攔截器或安全相關的瀏覽器擴充功能可能會阻止諸如 AddToAny 這樣的第三方服務。 由於我們網站跟 AddToAny 都是 https,第一點可以排除本站並沒有設置 CSP (目前沒有)AddToAny 這類插件沒處理好 CORS 的機率是很低的 簡單排除後,比較有可能的是被廣告攔截器擋住了 喔喔喔!總算出來了! 廣告攔截器試了分享功能正常,就是按鈕有點多可能減少一些..之後客製 style… 恩..不過這樣好像沒有解決問題阿? 根據統計,全世界有至少四成的人口,常駐廣告攔截器在上網 只是我看的到而已,如果其他人看不到的話就沒意義了 qq 不過攔截的問題還算好解決,只要讓他不會被攔截就好了!(廢話) 方法很多,最簡單的是將原本被攔截的檔案納入網域下載入原本就是同域名下的資源,通常就能繞過限制了。 <script async src="https://static.addtoany.com/menu/page.js" defer={true}></script> 根據產出的代碼來看,主要是這個檔案需要拉進來 整理格式,創建一個 addtoany.js 放進去以 hexo 來說就是將 js 檔案放在 themes/{theme_name}/source/js 底下 之後回到 addtoany.jsx 修改來源 <script async src="/js/addtoany.js" defer={true}></script> RUN! 失敗了! 載入失敗的檔案反而變多了! 更多的廣告看了下失敗的內容,來源都是出自 addtoany.js,也就是剛剛新增的檔案 回頭翻代碼… 看來是原本的 page.js 引入了其他檔案然後其他檔案又被攔截了… 雖然有點亂,但還是好解決的 首先在 source 目錄新增 addtoany 資料夾將 addtoany.js 改名回 page.js ,放進 addtoany 資料夾原本 addtoany.jsx 的 script 也要修改 <script async src="/addtoany/page.js" defer={true}></script> 之後將三個攔截的檔案拷貝整理後,在 source/addtoany 創建同名檔案 然後..然後…然後在 core.js 引用了更多會被攔截的 url… 估計是進行一些第三方 icon 的載入等等… 結語 重新修改 core.js 的代碼,最終是成功了 但是修改已經壓縮過的代碼,過程是麻煩且沒營養的處理方法相當於暴力破解,也不排除往後產生其他 ERROR 的可能,故這邊不多贅述——— 如果有幸你也在想辦法處理 AddToAny 的問題,又懶得架 cdn 等方式 這邊提供整理後的檔案,歡迎參考。 addtoany.7z MD5: e4c6ac982c223e6449d1d962be077bfbSHA1: a9cc39cc5e9a7d0854d63b15a4801829c1718efb","link":"/2024/04/23/shareButton1/"},{"title":"Day3 - 你自己的...他叫甚麼名字?","text":"今天來說說怎麼創建 Bot 建立 DiscordBot,首先要有一個 Discord 帳號 到這個網站 登入帳號後,右上角會有一個[ New Application ] 這邊的名字是”應用名稱”同時也會是預設的機器人名稱,仔細想好後就送出吧~ 送出後就會進來應用介面了 點擊頭像可替換應用的大頭貼,名字就是你剛剛取的 都設定好之後,我們點選左邊列表第三排的 Bot 第一次點應該會問你是否要建立,點選是這樣 Discord 就會幫你的機器人實際創建一個帳戶點擊右邊 TOKEN 底下的 Copy,他會給你一組很長的亂碼 先存在記事本,我們明天會用到 接下來我們點左邊的 OAuth2 這是創建邀請連結的地方,SCOPES 欄的部份點 Bot,這樣下面就會出現第二欄下面是要給予機器人在群組內的權限,需要根據 Bot 實際上具備哪些功能,來判斷需要給予哪些權限如果不知道怎麼用,先選 Administrator 就可以了。 都做完後,中間的就會有完整的邀請連結,請複製後輸入在瀏覽器上 使用此連結,就可以把 Bot 拉入你的群組內,前提是你在此群組有管理者權限喔 喔耶,成功了!","link":"/2020/09/03/12thDay3/"},{"title":"最近想更換評論系統","text":"最近想把 blog 的評論系統換了 原本的 gittalk 免費,開源巧妙利用 github 的 issue,在靜態網站上也能加入評論系統 存在些許不方便,仍瑕不掩瑜。 不過既然存在限制,總有人會想開發更好的工具twikoo 就挺符合 blog 需求的 gittalk既然原本是用 gittalk,先說說 gittalk 的優點 與 github page 的高度適配 基於 issue 特性,綁定 github 帳戶,防止小白 兩邊都有的優點不提,大概是這些至於缺點 因為綁在 issue,評論必須先註冊 github 對中國使用者的支援較差(疑似) 而且本站基於 gittalk ,魔改了許多功能 撈取最新留言 撈取熱門留言 從外部撈取文章留言數 替換評論系統,不只是舊有的評論會消失也代表著這些功能都需要重新適配,或是棄用。 twikoo官方文檔 twikoo 與 gittalk 不同,資料存放於 MongoDB意味著我們需要自行架設 DB 跟 API Server 不過兩者在網路上都有許多免費資源可用,用來支持一個 blog 的運作綽綽有餘 說說這個評論系統的優點 無須登入即可留言 避免騷擾留言,分別配有多種自動偵測垃圾留言的接口,也能開啟人工審核 暱稱&信箱&網址 的填寫方式,很有幾十年前,傳統 blog 那味,我超愛 至於缺點 比起 gittalk,由於個人資料是自由填寫,相對難以得知發文者的背景 與依附著 github issue 的 gittalk 相比,twikoo 於第三方架設 DB 跟 API Server 環境,長遠來看需要消耗更多的維護成本 以前的評論會全部消失,嗚嗚嗚 基本的配置已經做好了,只是還在猶豫是否該使用 個人 blog 要提高評論數還是比較難的,至少沒辦法跟社群平台競爭在這個前提下,評論門檻相對高的 gittalk,這份缺點也會被不斷放大 但 gittalk 同樣有著他本身天然的優勢在——","link":"/2024/04/23/twikoo1/"},{"title":"前幾天有人詢問我用的啥主題","text":"前幾天收到信,信中詢問使用的主題想了下的確從沒寫過,索性紀錄一下。 Hexo 首先本站使用模板為 Hexo 該模板提供靜態網頁生成,搭配 Github Page,或是其他免費架設平台,可以輕易實現無開銷環境的長期 blog 支持 Hexo 從 2013 年開發至今,筆者發文的 2 周前推出 v7.2.0 版本的 Hexo,有著優秀的基底與長期穩定的維護。 Icarus 除了默認的設置以外,Hexo 提供自行開發主題(THEME)的接口 本站使用主題為 Icarus 該主題默認提供 default(白色) 跟 cyberpunk(黃色) 主題可供選用 由於 Icarus 後來經過多次更迭,加上筆者自己對 blog 有諸多修改,如今已與原本的 Icarus 有很多差異,不過仍看得出排版等元素皆承襲自 Icarus。 星空 黑暗主題的星空背景,參考imaegoo大大的開源代碼 imaegoo 的開源代碼同樣是從 Icarus 改進而來如果喜歡星空背景,該開源代碼可以直接套來用 不過由於筆者在此之前,已經對 blog 做過諸多修改最終是自行研究該代碼後,另外自己寫 css,想辦法移植過來的。 PJAX本站的局部加載使用 PJAX,同樣是另外加寫的 原本網站是使用 Fluid 這套 Hexo 主題 但因為 Fluid 並不支援局部加載,自己想辦法實作後發現基底的確不適合,無奈之下只好棄用。 當時為了魔改 Fluid 還寫了篇紀錄,有興趣可以看看。 Live 2D 本站的 Live 2D 小人,使用 fghrsh 大大撰寫的開源工具 該專案主要負責 live 2d 的加載,與針對網頁元素互動的邏輯撰寫 得益於開源專案的優勢,該工具有著許多變種,同時支持多項設定自定義 像本站是使用了原本的前端工具 + 後端 API 本地靜態化以此來避免 憑證過期 & 後端額外開銷 等等問題。 Gittalk & 熱門評論 & 最新評論 Gittalk 一直都是本站的評論系統他主要依賴於 Github Issue 與其 API讓原本靜態的網站,彷彿支援動態的評論系統一般 如果 blog 原本是使用 Github Page 架設,懶一點的話可以架在同一個 Repository,如此便不需花費額外的維護成本。 除了 Gittalk 原本的功能,熱門&最新評論的 API 串接則是參考辣椒的醬大大 由於過於方便,並沒有多加修改, 就是那個評論數統計,當時看代碼會偷偷灌水,所以改回了原始數值。 另外由於辣椒的醬大大的 blog 已經多年未有更新,考慮到 hexo&Icarus 在之後有多次版本更迭以剛起步來說,會更加推薦直接使用官方模板來做修改。 明亮主題的粒子特效 這部份使用 canvas-nest 使用非常簡單,在網頁引入 js 即可網路教學不少 由於跟夜晚主題適配性差,根據個人需求不同,要稍微調整 css。 音樂箱 音樂箱功能使用 Aplayer 播放器 非常低調地摺疊收納在網站角落,按照預想,就算是多次瀏覽本站的旅客也不一定能發現 畢竟音樂箱只需要服務需要聽音樂的人即可,大多人瀏覽網站時也有自己在背景播放的歌曲,這時候若太過強調網站自帶的音樂,顯得不識趣了些。 值得一提的是,本站的音樂箱,在手機可以當作歌曲列表自動播放。 RSS 本站 RSS 提供三種格式分別是預設的 RSS、ATOM 與 Json Feed 使用 RSS 訂閱,便可即時知道網站更新了文章 對筆者來說,就像是提醒自己一直都沒在更新文章 看到一排去年的文章,彷彿說著「再不更新就要死了!」這樣。 寫在後面列一列大概是這些,如果有甚麼缺漏,或是未來更新沒寫在這,想問的 歡迎提出來,有時間便會回覆。","link":"/2024/05/03/useTheme1/"}],"tags":[{"name":"node.js","slug":"node-js","link":"/tags/node-js/"},{"name":"bot","slug":"bot","link":"/tags/bot/"},{"name":"discord","slug":"discord","link":"/tags/discord/"},{"name":"discord.js","slug":"discord-js","link":"/tags/discord-js/"},{"name":"教學","slug":"教學","link":"/tags/%E6%95%99%E5%AD%B8/"},{"name":"12th鐵人賽","slug":"12th鐵人賽","link":"/tags/12th%E9%90%B5%E4%BA%BA%E8%B3%BD/"},{"name":"日記","slug":"日記","link":"/tags/%E6%97%A5%E8%A8%98/"},{"name":"vitepress","slug":"vitepress","link":"/tags/vitepress/"},{"name":"algolia","slug":"algolia","link":"/tags/algolia/"},{"name":"活俠傳","slug":"活俠傳","link":"/tags/%E6%B4%BB%E4%BF%A0%E5%82%B3/"},{"name":"Alist","slug":"Alist","link":"/tags/Alist/"},{"name":"Cloudflare","slug":"Cloudflare","link":"/tags/Cloudflare/"},{"name":"docker","slug":"docker","link":"/tags/docker/"},{"name":"javascript","slug":"javascript","link":"/tags/javascript/"},{"name":"github","slug":"github","link":"/tags/github/"},{"name":"CI/CD","slug":"CI-CD","link":"/tags/CI-CD/"},{"name":"hexo","slug":"hexo","link":"/tags/hexo/"},{"name":"fluid","slug":"fluid","link":"/tags/fluid/"},{"name":"heroku","slug":"heroku","link":"/tags/heroku/"},{"name":"render","slug":"render","link":"/tags/render/"},{"name":"render.com","slug":"render-com","link":"/tags/render-com/"},{"name":"彈射世界","slug":"彈射世界","link":"/tags/%E5%BD%88%E5%B0%84%E4%B8%96%E7%95%8C/"},{"name":"遊戲","slug":"遊戲","link":"/tags/%E9%81%8A%E6%88%B2/"},{"name":"腳本","slug":"腳本","link":"/tags/%E8%85%B3%E6%9C%AC/"},{"name":"Live2D","slug":"Live2D","link":"/tags/Live2D/"},{"name":"Rust","slug":"Rust","link":"/tags/Rust/"},{"name":"addtoany","slug":"addtoany","link":"/tags/addtoany/"}],"categories":[{"name":"鐵人賽","slug":"鐵人賽","link":"/categories/%E9%90%B5%E4%BA%BA%E8%B3%BD/"},{"name":"用Node.js製作後台零負擔的DiscordBot","slug":"鐵人賽/用Node-js製作後台零負擔的DiscordBot","link":"/categories/%E9%90%B5%E4%BA%BA%E8%B3%BD/%E7%94%A8Node-js%E8%A3%BD%E4%BD%9C%E5%BE%8C%E5%8F%B0%E9%9B%B6%E8%B2%A0%E6%93%94%E7%9A%84DiscordBot/"},{"name":"BaseDiscordBot","slug":"BaseDiscordBot","link":"/categories/BaseDiscordBot/"},{"name":"日記","slug":"日記","link":"/categories/%E6%97%A5%E8%A8%98/"},{"name":"LoM-wiki","slug":"LoM-wiki","link":"/categories/LoM-wiki/"},{"name":"程式簡記","slug":"程式簡記","link":"/categories/%E7%A8%8B%E5%BC%8F%E7%B0%A1%E8%A8%98/"},{"name":"更新日誌","slug":"BaseDiscordBot/更新日誌","link":"/categories/BaseDiscordBot/%E6%9B%B4%E6%96%B0%E6%97%A5%E8%AA%8C/"},{"name":"雲端相關","slug":"程式簡記/雲端相關","link":"/categories/%E7%A8%8B%E5%BC%8F%E7%B0%A1%E8%A8%98/%E9%9B%B2%E7%AB%AF%E7%9B%B8%E9%97%9C/"},{"name":"hexo主題分享","slug":"hexo主題分享","link":"/categories/hexo%E4%B8%BB%E9%A1%8C%E5%88%86%E4%BA%AB/"},{"name":"託管平台相關","slug":"程式簡記/託管平台相關","link":"/categories/%E7%A8%8B%E5%BC%8F%E7%B0%A1%E8%A8%98/%E8%A8%97%E7%AE%A1%E5%B9%B3%E5%8F%B0%E7%9B%B8%E9%97%9C/"},{"name":"程式相關","slug":"程式簡記/程式相關","link":"/categories/%E7%A8%8B%E5%BC%8F%E7%B0%A1%E8%A8%98/%E7%A8%8B%E5%BC%8F%E7%9B%B8%E9%97%9C/"},{"name":"Rust","slug":"程式簡記/程式相關/Rust","link":"/categories/%E7%A8%8B%E5%BC%8F%E7%B0%A1%E8%A8%98/%E7%A8%8B%E5%BC%8F%E7%9B%B8%E9%97%9C/Rust/"},{"name":"javascript","slug":"程式簡記/程式相關/javascript","link":"/categories/%E7%A8%8B%E5%BC%8F%E7%B0%A1%E8%A8%98/%E7%A8%8B%E5%BC%8F%E7%9B%B8%E9%97%9C/javascript/"}]}