diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..4d20d93 --- /dev/null +++ b/.clang-format @@ -0,0 +1,54 @@ +# https://releases.llvm.org/6.0.0/tools/clang/docs/ClangFormatStyleOptions.html + +# LLVM 스타일을 기반으로 한 C++ 코딩 스타일. +BasedOnStyle: LLVM +Language: Cpp + +# 공백 설정 +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false + +# 들여쓰기 설정 +IndentCaseLabels: true +IndentWidth: 4 +IndentWrappedFunctionNames: false +TabWidth: 4 +UseTab: false + +# 코드 정렬 설정 +AlignAfterOpenBracket: Align +# AlignConsecutiveAssignments: false +# AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignTrailingComments: true +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: false +# BinPackArguments: false +# BinPackParameters: false +# BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +SortIncludes: true +SortUsingDeclarations: true + +# 포인터 변수 스타일 설정 +DerivePointerAlignment: false +PointerAlignment: Right + +# 나머지 스타일 설정 +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +ColumnLimit: 0 +MaxEmptyLinesToKeep: 1 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6379ee5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# 로그 파일 +*.log + +# 백업 파일과 설정 파일 +*.bak +*.manifest + +# 오브젝트 파일 +*.o + +# 정적 라이브러리 파일 +*.a +*.lib + +# 빌드 결과물이 저장된 폴더 +**/.vs/ +**/bin/ +**/build/ +**/**/Debug/ +**/**/Release/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3877ae0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aba9bae --- /dev/null +++ b/Makefile @@ -0,0 +1,80 @@ +# +# Copyright (c) 2022 jdeokkim +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +.PHONY: all clean + +_COLOR_BEGIN := $(shell tput setaf 13) +_COLOR_END := $(shell tput sgr0) + +PROJECT_NAME := saerom +PROJECT_FULL_NAME := jdeokkim/saerom + +PROJECT_PREFIX := $(_COLOR_BEGIN)$(PROJECT_FULL_NAME):$(_COLOR_END) + +BINARY_PATH := bin +INCLUDE_PATH := include +RESOURCE_PATH := res +SOURCE_PATH := src + +INCLUDE_PATH += $(SOURCE_PATH)/external + +SOURCES := \ + $(SOURCE_PATH)/bot.c \ + $(SOURCE_PATH)/info.c \ + $(SOURCE_PATH)/json.c \ + $(SOURCE_PATH)/krdict.c \ + $(SOURCE_PATH)/papago.c \ + $(SOURCE_PATH)/utils.c \ + $(SOURCE_PATH)/yxml.c \ + $(SOURCE_PATH)/main.c + +OBJECTS := $(SOURCES:.c=.o) + +TARGETS := $(BINARY_PATH)/$(PROJECT_NAME) + +CC := gcc +CFLAGS := -D_DEFAULT_SOURCE -g $(INCLUDE_PATH:%=-I%) -O2 -std=gnu99 +LDLIBS := -ldl -lcurl -ldiscord -lpthread -lsigar + +all: pre-build build post-build + +pre-build: + @echo "$(PROJECT_PREFIX) Using: '$(CC)' to build this project." + +build: $(TARGETS) + +$(SOURCE_PATH)/%.o: $(SOURCE_PATH)/%.c + @echo "$(PROJECT_PREFIX) Compiling: $@ (from $<)" + @$(CC) -c $< -o $@ $(CFLAGS) + +$(TARGETS): $(OBJECTS) + @mkdir -p $(BINARY_PATH) + @echo "$(PROJECT_PREFIX) Linking: $(TARGETS)" + @$(CC) $(OBJECTS) -o $(TARGETS) $(CFLAGS) $(LDFLAGS) $(LDLIBS) $(WEBFLAGS) + +post-build: + @echo "$(PROJECT_PREFIX) Build complete." + +clean: + @echo "$(PROJECT_PREFIX) Cleaning up." + @rm -rf $(BINARY_PATH)/* + @rm -rf $(SOURCE_PATH)/*.o \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c559a69 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# saerom + + +[![version badge](https://img.shields.io/github/v/release/jdeokkim/saerom?color=orange&include_prereleases)](https://github.com/jdeokkim/saerom/releases) +[![code-size badge](https://img.shields.io/github/languages/code-size/jdeokkim/saerom?color=green)](https://github.com/jdeokkim/saerom) +[![license badge](https://img.shields.io/github/license/jdeokkim/saerom?color=brightgreen)](https://github.com/jdeokkim/saerom/blob/main/LICENSE) + +A C99 Discord bot for Korean learning servers. + +## Commands + +| Name | Description | +| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------| +| `/info` | Show information about this bot | +| `/krd` | Search the given text in the [Basic Korean Dictionary](https://krdict.korean.go.kr/), published by the National Institute of Korean Language | +| `/ppg` | Translate the given text between two languages using [NAVER™ Papago NMT API](https://developers.naver.com/docs/papago/README.md) | + +## Screenshots + +### `/info` + +
+ Screenshot + + /info +
+ +### `/krd` + +
+ Screenshot + + /krd +
+ +### `/ppg` + +
+ Screenshot + + /ppg +
+ +## Prerequisites + +- GCC version 9.4.0+ +- GNU Make version 4.1+ +- CMake version 3.22.0+ +- Git version 2.17.1+ +- libcurl4 version 7.58.0+ (with OpenSSL flavor) + +```console +$ sudo apt install build-essential cmake git libcurl4-openssl-dev +``` + +## Building + +This project uses [GNU Make](https://www.gnu.org/software/make) as the build system. + +1. Install [concord v2.0.1+](https://github.com/Cogmasters/concord). + +```console +$ git clone https://github.com/Cogmasters/concord && cd concord +$ make -j`nproc` +$ sudo make install +``` + +2. Then, install the latest version of [boundary/sigar](https://github.com/boundary/sigar). + +```console +$ git clone https://github.com/boundary/sigar && cd sigar +$ mkdir build && cd build +$ cmake .. && make -j`nproc` +$ sudo make install +$ ldconfig +``` + +3. Build this project with the following commands. + +```console +$ git clone https://github.com/jdeokkim/saerom && cd saerom +$ make +``` + +4. Configure and run the bot. + +```console +$ vim res/config.json +$ ./bin/saerom +``` + +## License + +GNU General Public License, version 3 \ No newline at end of file diff --git a/include/saerom.h b/include/saerom.h new file mode 100644 index 0000000..60008c2 --- /dev/null +++ b/include/saerom.h @@ -0,0 +1,139 @@ +/* + Copyright (c) 2022 jdeokkim + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef SAEROM_H +#define SAEROM_H + +#include + +#include +#include + +/* | 매크로 정의... | */ + +#define APPLICATION_NAME "jdeokkim/saerom" +#define APPLICATION_VERSION "v0.1.0" +#define APPLICATION_DESCRIPTION "A C99 Discord bot for Korean learning servers." +#define APPLICATION_PROJECT_URL "https://github.com/jdeokkim/saerom" +#define APPLICATION_ID 986954825176596498 + +#define DEVELOPER_NAME "Jaedeok Kim" +#define DEVELOPER_ICON_URL "https://avatars.githubusercontent.com/u/28700668" + +#define MAX_FILE_SIZE 1024 +#define MAX_STRING_SIZE 256 + +/* | 자료형 정의... | */ + +/* Discord 봇의 환경 설정을 나타내는 구조체. */ +struct sr_config { + unsigned char flags; + struct { + char api_key[MAX_STRING_SIZE]; + } krdict; + struct { + char client_id[MAX_STRING_SIZE]; + char client_secret[MAX_STRING_SIZE]; + } papago; +}; + +/* Discord 봇의 환경 설정 플래그를 나타내는 열거형. */ +enum sr_flag { + SR_FLAG_KRDICT = (1 << 0), + SR_FLAG_PAPAGO = (1 << 1) +}; + +/* | `bot` 모듈 함수... | */ + +/* Discord 봇을 초기화한다. */ +void init_bot(int argc, char *argv[]); + +/* Discord 봇을 실행한다. */ +void run_bot(void); + +/* Discord 봇에 할당된 메모리를 해제한다. */ +void deinit_bot(void); + +/* `CURLV` 인터페이스를 반환한다. */ +void *get_curlv(void); + +/* Discord 봇의 환경 설정 정보를 반환한다. */ +struct sr_config *get_sr_config(void); + +/* Discord 봇의 CPU 사용량 (단위: 퍼센트)을 반환한다. */ +double get_cpu_usage(void); + +/* Discord 봇의 메모리 사용량 (단위: 메가바이트)을 반환한다. */ +double get_ram_usage(void); + +/* Discord 봇의 작동 시간 (단위: 밀리초)을 반환한다. */ +uint64_t get_uptime(void); + +/* | `info` 모듈 함수... | */ + +/* `info` 모듈 명령어를 생성한다. */ +void create_info_command(struct discord *client); + +/* `info` 모듈 명령어에 할당된 메모리를 해제한다. */ +void release_info_command(struct discord *client); + +/* `info` 모듈 명령어를 실행한다. */ +void run_info_command( + struct discord *client, + const struct discord_interaction *event +); + +/* | `krdict` 모듈 함수... | */ + +/* `krdict` 모듈 명령어를 생성한다. */ +void create_krdict_command(struct discord *client); + +/* `krdict` 모듈 명령어에 할당된 메모리를 해제한다. */ +void release_krdict_command(struct discord *client); + +/* `krdict` 모듈 명령어를 실행한다. */ +void run_krdict_command( + struct discord *client, + const struct discord_interaction *event +); + +/* | `papago` 모듈 함수... | */ + +/* `papago` 모듈 명령어를 생성한다. */ +void create_papago_command(struct discord *client); + +/* `papago` 모듈 명령어에 할당된 메모리를 해제한다. */ +void release_papago_command(struct discord *client); + +/* `papago` 모듈 명령어를 실행한다. */ +void run_papago_command( + struct discord *client, + const struct discord_interaction *event +); + +/* | `utils` 모듈 함수... | */ + +/* 주어진 사용자의 프로필 사진 URL을 반환한다. */ +const char *get_avatar_url(const struct discord_user *user); + +/* 주어진 경로에 위치한 파일의 내용을 반환한다. */ +char *get_file_contents(const char *path); + +/* 두 문자열의 내용이 서로 같은지 확인한다. */ +bool string_equals(const char *s1, const char *s2); + +#endif \ No newline at end of file diff --git a/res/config.json b/res/config.json new file mode 100644 index 0000000..76b14ec --- /dev/null +++ b/res/config.json @@ -0,0 +1,32 @@ +{ + "logging": { + "level": "info", + "filename": "bot.log", + "quiet": false, + "overwrite": true, + "use_color": true, + "http": { + "enable": true, + "filename": "http.log" + }, + "disable_modules": ["WEBSOCKETS", "USER_AGENT"] + }, + "discord": { + "token": "YOUR-BOT-TOKEN", + "default_prefix": { + "enable": false, + "prefix": "^" + } + }, + "saerom": { + "krdict": { + "enable": true, + "api_key": "YOUR-API-KEY" + }, + "papago": { + "enable": true, + "client_id": "YOUR-CLIENT-ID", + "client_secret": "YOUR-CLIENT-SECRET" + } + } +} \ No newline at end of file diff --git a/res/images/.gitkeep b/res/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/res/images/avatar.png b/res/images/avatar.png new file mode 100644 index 0000000..2b7d516 Binary files /dev/null and b/res/images/avatar.png differ diff --git a/res/images/avatar.xcf b/res/images/avatar.xcf new file mode 100644 index 0000000..3410cc0 Binary files /dev/null and b/res/images/avatar.xcf differ diff --git a/res/images/screenshot-info.png b/res/images/screenshot-info.png new file mode 100644 index 0000000..0cd9256 Binary files /dev/null and b/res/images/screenshot-info.png differ diff --git a/res/images/screenshot-krd.png b/res/images/screenshot-krd.png new file mode 100644 index 0000000..ac608c1 Binary files /dev/null and b/res/images/screenshot-krd.png differ diff --git a/res/images/screenshot-ppg.png b/res/images/screenshot-ppg.png new file mode 100644 index 0000000..c0c2c64 Binary files /dev/null and b/res/images/screenshot-ppg.png differ diff --git a/src/bot.c b/src/bot.c new file mode 100644 index 0000000..01dc0ea --- /dev/null +++ b/src/bot.c @@ -0,0 +1,370 @@ +/* + Copyright (c) 2022 jdeokkim + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include + +#define CURLV_IMPLEMENTATION +#include + +#include "saerom.h" + +/* | `bot` 모듈 매크로 정의... | */ + +#define CONFIG_PATH "res/config.json" + +/* | `bot` 모듈 자료형 정의... | */ + +/* Discord 봇의 명령어를 나타내는 구조체. */ +struct sr_command { + char *name; + void (*on_create)(struct discord *client); + void (*on_release)(struct discord *client); + void (*on_run)( + struct discord *client, + const struct discord_interaction *event + ); +}; + +/* Discord 봇의 명령어 목록. */ +static const struct sr_command slash_commands[] = { + { + .name = "info", + .on_create = create_info_command, + .on_release = release_info_command, + .on_run = run_info_command + }, + { + .name = "krd", + .on_create = create_krdict_command, + .on_release = release_krdict_command, + .on_run = run_krdict_command + }, + { + .name = "ppg", + .on_create = create_papago_command, + .on_release = release_papago_command, + .on_run = run_papago_command + } +}; + +/* | `bot` 모듈 상수 및 변수... | */ + +/* `CURLV` 인터페이스. */ +static CURLV *curlv; + +/* `sigar` 인터페이스. */ +static sigar_t *sigar; + +/* Discord 봇의 클라이언트 객체. */ +static struct discord *client; + +/* Discord 봇의 환경 설정. */ +static struct sr_config config; + +/* Discord 봇의 시작 시간. */ +static uint64_t old_timestamp; + +/* | `bot` 모듈 함수... | */ + +/* Discord 봇이 실행 중일 때 주기적으로 호출된다. */ +static void on_idle(struct discord *client); + +/* Discord 봇의 클라이언트가 준비되었을 때 호출된다. */ +static void on_ready( + struct discord *client, + const struct discord_ready *event +); + +/* Discord 봇이 명령어 처리를 요청받았을 때 호출된다. */ +static void on_interaction_create( + struct discord *client, + const struct discord_interaction *event +); + +/* Discord 봇의 추가 정보를 초기화한다. */ +static void sr_config_init(void); + +/* Discord 봇의 추가 정보에 할당된 메모리를 해제한다. */ +static void sr_config_cleanup(void); + +/* Discord 봇의 명령어들을 생성한다. */ +static void create_slash_commands(struct discord *client); + +/* Discord 봇의 명령어들에 할당된 메모리를 해제한다. */ +static void release_slash_commands(struct discord *client); + +/* Discord 봇을 초기화한다. */ +void init_bot(int argc, char *argv[]) { + ccord_global_init(); + + client = discord_config_init((argc > 1) ? argv[1] : CONFIG_PATH); + + sr_config_init(); + + discord_set_on_idle(client, on_idle); + discord_set_on_ready(client, on_ready); + discord_set_on_interaction_create(client, on_interaction_create); + + create_slash_commands(client); +} + +/* Discord 봇을 실행한다. */ +void run_bot(void) { + discord_run(client); +} + +/* Discord 봇에 할당된 메모리를 해제한다. */ +void deinit_bot(void) { + release_slash_commands(client); + + sr_config_cleanup(); + + discord_cleanup(client); + + ccord_global_cleanup(); +} + +/* `CURLV` 인터페이스를 반환한다. */ +void *get_curlv(void) { + return (void *) curlv; +} + +/* Discord 봇의 환경 설정 정보를 반환한다. */ +struct sr_config *get_sr_config(void) { + return &config; +} + +/* Discord 봇의 CPU 사용량 (단위: 퍼센트)을 반환한다. */ +double get_cpu_usage(void) { + sigar_proc_cpu_t cpu; + + sigar_proc_cpu_get( + sigar, + sigar_pid_get(sigar), + &cpu + ); + + return cpu.percent; +} + +/* Discord 봇의 메모리 사용량 (단위: 메가바이트)을 반환한다. */ +double get_ram_usage(void) { + static const double DENOMINATOR = 1.0 / (1024.0 * 1024.0); + + sigar_proc_mem_t mem; + + sigar_proc_mem_get( + sigar, + sigar_pid_get(sigar), + &mem + ); + + return mem.resident * DENOMINATOR; +} + +/* Discord 봇의 작동 시간 (단위: 밀리초)을 반환한다. */ +uint64_t get_uptime(void) { + return discord_timestamp(client) - old_timestamp; +} + +/* Discord 봇이 실행 중일 때 주기적으로 호출된다. */ +static void on_idle(struct discord *client) { + curlv_read_requests(curlv); + + // CPU 사용량 최적화 + cog_sleep_us(1); +} + +/* Discord 봇의 클라이언트가 준비되었을 때 호출된다. */ +static void on_ready( + struct discord *client, + const struct discord_ready *event +) { + log_info( + "[SAEROM] Connected to Discord as %s#%s (in %d server(s))", + event->user->username, + event->user->discriminator, + event->guilds->size + ); + + /* + "Bots are only able to send `name`, `type`, and optionally `url`." + + - https://discord.com/developers/docs/topics/gateway#activity-object-activity-structure + */ + + struct discord_activity activities[] = { + { + .name = "/info (" APPLICATION_VERSION ")", + .type = DISCORD_ACTIVITY_GAME, + }, + }; + + old_timestamp = discord_timestamp(client); + + struct discord_presence_update status = { + .activities = &(struct discord_activities) { + .size = sizeof(activities) / sizeof(*activities), + .array = activities, + }, + .status = "online", + .afk = false, + .since = old_timestamp + }; + + discord_update_presence(client, &status); +} + +/* Discord 봇이 명령어 처리를 요청받았을 때 호출된다. */ +static void on_interaction_create( + struct discord *client, + const struct discord_interaction *event +) { + /* + "An Interaction is the message that your application receives when a user + uses an application command or a message component. For Slash Commands, + it includes the values that the user submitted." + + - https://discord.com/developers/docs/interactions/receiving-and-responding + */ + + if (event->data == NULL) return; + + const struct discord_user *user = (event->member != NULL) + ? event->member->user + : event->user; + + const char *context = NULL; + + switch (event->type) { + case DISCORD_INTERACTION_APPLICATION_COMMAND: + context = event->data->name; + + log_info( + "[SAEROM] Handling command `/%s` executed by %s#%s", + context, + user->username, + user->discriminator + ); + + for (int i = 0; i < sizeof(slash_commands) / sizeof(*slash_commands); i++) + if (string_equals(context, slash_commands[i].name)) + slash_commands[i].on_run(client, event); + + break; + + case DISCORD_INTERACTION_MESSAGE_COMPONENT: + context = event->data->custom_id; + + log_info( + "[SAEROM] Handling component interaction `#%s` invoked by %s#%s", + context, + user->username, + user->discriminator + ); + + for (int i = 0; i < sizeof(slash_commands) / sizeof(*slash_commands); i++) { + const char *name = slash_commands[i].name; + + if (strncmp(context, name, strlen(name)) == 0) + slash_commands[i].on_run(client, event); + } + + break; + } +} + +/* Discord 봇의 추가 정보를 초기화한다. */ +static void sr_config_init(void) { + if (client == NULL) return; + + curlv = curlv_init(); + + sigar_open(&sigar); + + struct ccord_szbuf_readonly field = discord_config_get_field( + client, (char *[3]) { "saerom", "krdict", "enable" }, 3 + ); + + if (strncmp("true", field.start, field.size) == 0) { + config.flags |= SR_FLAG_KRDICT; + + field = discord_config_get_field( + client, (char *[3]) { "saerom", "krdict", "api_key" }, 3 + ); + + strncpy(config.krdict.api_key, field.start, field.size); + } + + field = discord_config_get_field( + client, (char *[3]) { "saerom", "papago", "enable" }, 3 + ); + + if (strncmp("true", field.start, field.size) == 0) { + config.flags |= SR_FLAG_PAPAGO; + + field = discord_config_get_field( + client, (char *[3]) { "saerom", "papago", "client_id" }, 3 + ); + + strncpy(config.papago.client_id, field.start, field.size); + + field = discord_config_get_field( + client, (char *[3]) { "saerom", "papago", "client_secret" }, 3 + ); + + strncpy(config.papago.client_secret, field.start, field.size); + } +} + +/* Discord 봇의 추가 정보에 할당된 메모리를 해제한다. */ +static void sr_config_cleanup(void) { + curlv_cleanup(curlv); + + sigar_close(sigar); +} + +/* Discord 봇의 명령어들을 생성한다. */ +static void create_slash_commands(struct discord *client) { + for (int i = 0; i < sizeof(slash_commands) / sizeof(*slash_commands); i++) { + if (string_equals(slash_commands[i].name, "krd") + && !(config.flags & SR_FLAG_KRDICT)) continue; + + if (string_equals(slash_commands[i].name, "ppg") + && !(config.flags & SR_FLAG_PAPAGO)) continue; + + log_info("[SAEROM] Creating global application command `/%s`", slash_commands[i].name); + + slash_commands[i].on_create(client); + } +} + +/* Discord 봇의 명령어들에 할당된 메모리를 해제한다. */ +static void release_slash_commands(struct discord *client) { + for (int i = 0; i < sizeof(slash_commands) / sizeof(*slash_commands); i++) { + if (string_equals(slash_commands[i].name, "krd") + && !(config.flags & SR_FLAG_KRDICT)) continue; + + if (string_equals(slash_commands[i].name, "ppg") + && !(config.flags & SR_FLAG_PAPAGO)) continue; + + log_info("[SAEROM] Releasing global application command `/%s`", slash_commands[i].name); + + slash_commands[i].on_release(client); + } +} \ No newline at end of file diff --git a/src/external/curlv.h b/src/external/curlv.h new file mode 100644 index 0000000..b7cc537 --- /dev/null +++ b/src/external/curlv.h @@ -0,0 +1,314 @@ +/* + Copyright (c) 2013, Ben Noordhuis + Copyright (c) 2022, Jaedeok Kim + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#ifndef CURLV_H +#define CURLV_H + +#include + +#include + +/* | 자료형 정의... | */ + +/* 문자열을 나타내는 구조체. */ +typedef struct CURLV_STR { + char *str; // 문자열의 메모리 주소. + size_t len; // 문자열의 길이. +} CURLV_STR; + +/* 사용자 요청의 처리가 끝났을 때 호출될 함수.*/ +typedef void (*curlv_read_callback)(CURLV_STR res, void *user_data); + +/* 사용자의 요청을 나타내는 구조체. */ +typedef struct CURLV_REQ { + CURL *easy; // 요청에 사용할 핸들. + struct curl_slist *header; // HTTP 요청 헤더. + curlv_read_callback callback; // 응답을 받았을 때 호출될 함수. + void *user_data; // 사용자 정의 데이터. +} CURLV_REQ; + +/* `CURLV` 인터페이스를 나타내는 구조체. */ +typedef struct CURLV CURLV; + +#ifdef __cplusplus +extern "C" { +#endif + +/* | 라이브러리 함수... | */ + +/* `CURLV` 인터페이스를 초기화한다. */ +CURLV *curlv_init(void); + +/* `CURLV` 인터페이스에 할당된 메모리를 해제한다. */ +void curlv_cleanup(CURLV *cv); + +/* `CURLV` 인터페이스에 새로운 요청을 추가한다. */ +void curlv_create_request(CURLV *cv, const CURLV_REQ *req); + +/* `CURLV` 인터페이스에서 가장 먼저 추가된 요청을 제거한다. */ +void curlv_remove_request(CURLV *cv); + +/* `CURLV` 인터페이스에 추가된 모든 요청들을 처리한다. */ +void curlv_read_requests(CURLV *cv); + +#ifdef __cplusplus +} +#endif + +#endif // `CURLV_H` + +#ifdef CURLV_IMPLEMENTATION + +#include +#include + +#include + +/* | 매크로 정의... | */ + +typedef void *QUEUE[2]; + +#define QUEUE(type) QUEUE + +#define QUEUE_NEXT(q) (*(QUEUE **) &((*(q))[0])) +#define QUEUE_PREV(q) (*(QUEUE **) &((*(q))[1])) +#define QUEUE_PREV_NEXT(q) (QUEUE_NEXT(QUEUE_PREV(q))) +#define QUEUE_NEXT_PREV(q) (QUEUE_PREV(QUEUE_NEXT(q))) + +#define QUEUE_DATA(ptr, type, field) \ + ((type *) ((char *) (ptr) - offsetof(type, field))) + +#define QUEUE_FOREACH(q, h) \ + for ((q) = QUEUE_NEXT(h); (q) != (h); (q) = QUEUE_NEXT(q)) + +#define QUEUE_EMPTY(q) \ + ((const QUEUE *) (q) == (const QUEUE *) QUEUE_NEXT(q)) + +#define QUEUE_HEAD(q) \ + (QUEUE_NEXT(q)) + +#define QUEUE_INIT(q) \ + do { \ + QUEUE_NEXT(q) = (q); \ + QUEUE_PREV(q) = (q); \ + } \ + while (0) + +#define QUEUE_INSERT_TAIL(h, q) \ + do { \ + QUEUE_NEXT(q) = (h); \ + QUEUE_PREV(q) = QUEUE_PREV(h); \ + QUEUE_PREV_NEXT(q) = (q); \ + QUEUE_PREV(h) = (q); \ + } \ + while (0) + +#define QUEUE_REMOVE(q) \ + do { \ + QUEUE_PREV_NEXT(q) = QUEUE_NEXT(q); \ + QUEUE_NEXT_PREV(q) = QUEUE_PREV(q); \ + } \ + while (0) + +/* | 자료형 정의... | */ + +/* 요청 큐의 요소를 나타내는 구조체. */ +typedef struct CURLV_QE { + CURLV_REQ request; + CURLV_STR response; + QUEUE(_) entry; +} CURLV_QE; + +/* `CURLV` 인터페이스를 나타내는 구조체. */ +struct CURLV { + CURLM *multi; + QUEUE(CURLV_QE) requests; + pthread_mutex_t lock; +}; + +/* | 라이브러리 함수... | */ + +/* `CURLV` 인터페이스의 응답 처리 시에 호출되는 함수. */ +static size_t curlv_write_callback(char *ptr, size_t size, size_t num, void *write_data); + +/* 요청 큐의 요소를 초기화한다. */ +static CURLV_QE *curlv_qe_init(const CURLV_REQ *req); + +/* 요청 큐의 요소에 할당된 메모리를 해제한다. */ +static void curlv_qe_cleanup(CURLV_QE *qe); + +/* `CURLV` 인터페이스를 초기화한다. */ +CURLV *curlv_init(void) { + CURLV *result = calloc(1, sizeof(CURLV)); + + QUEUE_INIT(&result->requests); + + result->multi = curl_multi_init(); + + pthread_mutex_init(&result->lock, NULL); + + return result; +} + +/* `CURLV` 인터페이스에 할당된 메모리를 해제한다. */ +void curlv_cleanup(CURLV *cv) { + if (cv != NULL) { + while (!QUEUE_EMPTY(&cv->requests)) + curlv_remove_request(cv); + + curl_multi_cleanup(cv->multi); + + pthread_mutex_destroy(&cv->lock); + } + + free(cv); +} + +/* `CURLV` 인터페이스에 새로운 요청을 추가한다. */ +void curlv_create_request(CURLV *cv, const CURLV_REQ *req) { + if (cv == NULL || req == NULL) return; + + CURLV_QE *qe = curlv_qe_init(req); + + curl_easy_setopt(req->easy, CURLOPT_WRITEFUNCTION, curlv_write_callback); + curl_easy_setopt(req->easy, CURLOPT_WRITEDATA, (void *) &qe->response); + curl_easy_setopt(req->easy, CURLOPT_HTTPHEADER, req->header); + curl_easy_setopt(req->easy, CURLOPT_PRIVATE, (void *) qe); + + pthread_mutex_lock(&cv->lock); + + curl_multi_add_handle(cv->multi, req->easy); + + QUEUE_INSERT_TAIL(&cv->requests, &qe->entry); + + pthread_mutex_unlock(&cv->lock); +} + +/* `CURLV` 인터페이스에서 가장 먼저 추가된 요청을 제거한다. */ +void curlv_remove_request(CURLV *cv) { + if (cv == NULL) return; + + pthread_mutex_lock(&cv->lock); + + QUEUE(CURLV_QE) *head = QUEUE_HEAD(&cv->requests); + + QUEUE_REMOVE(head); + + CURLV_QE *qe = QUEUE_DATA(head, CURLV_QE, entry); + + curl_multi_remove_handle(cv->multi, qe->request.easy); + + curlv_qe_cleanup(qe); + + pthread_mutex_unlock(&cv->lock); +} + +/* `CURLV` 인터페이스에 추가된 모든 요청들을 처리한다. */ +void curlv_read_requests(CURLV *cv) { + if (cv == NULL) return; + + /* https://curl.se/libcurl/c/threadsafe.html */ + + pthread_mutex_lock(&cv->lock); + + int still_running; + + do { + CURLMcode result = curl_multi_perform(cv->multi, &still_running); + + if (result != CURLM_OK) break; + } while (still_running); + + struct CURLMsg *msg; + + for (;;) { + int msgs_in_queue; + + msg = curl_multi_info_read(cv->multi, &msgs_in_queue); + + if (msg == NULL) break; + + if (msg->msg == CURLMSG_DONE) { + CURLV_QE *qe; + + CURLcode result = curl_easy_getinfo( + msg->easy_handle, + CURLINFO_PRIVATE, + &qe + ); + + if (qe->request.callback != NULL) + qe->request.callback(qe->response, qe->request.user_data); + + QUEUE_REMOVE(&qe->entry); + + curl_multi_remove_handle(cv->multi, qe->request.easy); + curlv_qe_cleanup(qe); + } + } + + pthread_mutex_unlock(&cv->lock); +} + +/* `CURLV` 인터페이스의 응답 처리 시에 호출되는 함수. */ +static size_t curlv_write_callback(char *ptr, size_t size, size_t num, void *write_data) { + size_t new_size = size * num; + + CURLV_STR *response = (CURLV_STR *) write_data; + + char *new_ptr = realloc(response->str, response->len + new_size + 1); + + if (new_ptr == NULL) return 0; + + response->str = new_ptr; + + memcpy(&(response->str[response->len]), ptr, new_size); + + response->len += new_size; + response->str[response->len] = 0; + + return new_size; +} + +/* 요청 큐의 요소를 초기화한다. */ +static CURLV_QE *curlv_qe_init(const CURLV_REQ *req) { + CURLV_QE *result = calloc(1, sizeof(CURLV_QE)); + + result->request = (*req); + + return result; +} + +/* 요청 큐의 요소에 할당된 메모리를 해제한다. */ +static void curlv_qe_cleanup(CURLV_QE *qe) { + if (qe != NULL) { + curl_easy_cleanup(qe->request.easy); + curl_slist_free_all(qe->request.header); + + free(qe->response.str); + } + + free(qe); +} + +#endif // `CURLV_IMPLEMENTATION` \ No newline at end of file diff --git a/src/external/json.h b/src/external/json.h new file mode 100644 index 0000000..dccb58a --- /dev/null +++ b/src/external/json.h @@ -0,0 +1,117 @@ +/* + Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef CCAN_JSON_H +#define CCAN_JSON_H + +#include +#include + +typedef enum { + JSON_NULL, + JSON_BOOL, + JSON_STRING, + JSON_NUMBER, + JSON_ARRAY, + JSON_OBJECT, +} JsonTag; + +typedef struct JsonNode JsonNode; + +struct JsonNode +{ + /* only if parent is an object or array (NULL otherwise) */ + JsonNode *parent; + JsonNode *prev, *next; + + /* only if parent is an object (NULL otherwise) */ + char *key; /* Must be valid UTF-8. */ + + JsonTag tag; + union { + /* JSON_BOOL */ + bool bool_; + + /* JSON_STRING */ + char *string_; /* Must be valid UTF-8. */ + + /* JSON_NUMBER */ + double number_; + + /* JSON_ARRAY */ + /* JSON_OBJECT */ + struct { + JsonNode *head, *tail; + } children; + }; +}; + +/*** Encoding, decoding, and validation ***/ + +JsonNode *json_decode (const char *json); +char *json_encode (const JsonNode *node); +char *json_encode_string (const char *str); +char *json_stringify (const JsonNode *node, const char *space); +void json_delete (JsonNode *node); + +bool json_validate (const char *json); + +/*** Lookup and traversal ***/ + +JsonNode *json_find_element (JsonNode *array, int index); +JsonNode *json_find_member (JsonNode *object, const char *name); + +JsonNode *json_first_child (const JsonNode *node); + +#define json_foreach(i, object_or_array) \ + for ((i) = json_first_child(object_or_array); \ + (i) != NULL; \ + (i) = (i)->next) + +/*** Construction and manipulation ***/ + +JsonNode *json_mknull(void); +JsonNode *json_mkbool(bool b); +JsonNode *json_mkstring(const char *s); +JsonNode *json_mknumber(double n); +JsonNode *json_mkarray(void); +JsonNode *json_mkobject(void); + +void json_append_element(JsonNode *array, JsonNode *element); +void json_prepend_element(JsonNode *array, JsonNode *element); +void json_append_member(JsonNode *object, const char *key, JsonNode *value); +void json_prepend_member(JsonNode *object, const char *key, JsonNode *value); + +void json_remove_from_parent(JsonNode *node); + +/*** Debugging ***/ + +/* + * Look for structure and encoding problems in a JsonNode or its descendents. + * + * If a problem is detected, return false, writing a description of the problem + * to errmsg (unless errmsg is NULL). + */ +bool json_check(const JsonNode *node, char errmsg[256]); + +#endif diff --git a/src/external/yxml.h b/src/external/yxml.h new file mode 100644 index 0000000..a0cc5f9 --- /dev/null +++ b/src/external/yxml.h @@ -0,0 +1,162 @@ +/* Copyright (c) 2013-2014 Yoran Heling + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#ifndef YXML_H +#define YXML_H + +#include +#include + +#if defined(_MSC_VER) && !defined(__cplusplus) && !defined(inline) +#define inline __inline +#endif + +/* Full API documentation for this library can be found in the "yxml.md" file + * in the yxml git repository, or online at http://dev.yorhel.nl/yxml/man */ + +typedef enum { + YXML_EEOF = -5, /* Unexpected EOF */ + YXML_EREF = -4, /* Invalid character or entity reference (&whatever;) */ + YXML_ECLOSE = -3, /* Close tag does not match open tag ( .. ) */ + YXML_ESTACK = -2, /* Stack overflow (too deeply nested tags or too long element/attribute name) */ + YXML_ESYN = -1, /* Syntax error (unexpected byte) */ + YXML_OK = 0, /* Character consumed, no new token present */ + YXML_ELEMSTART = 1, /* Start of an element: '' or '' */ + YXML_ATTRSTART = 4, /* Attribute: 'Name=..' */ + YXML_ATTRVAL = 5, /* Attribute value */ + YXML_ATTREND = 6, /* End of attribute '.."' */ + YXML_PISTART = 7, /* Start of a processing instruction */ + YXML_PICONTENT = 8, /* Content of a PI */ + YXML_PIEND = 9 /* End of a processing instruction */ +} yxml_ret_t; + +/* When, exactly, are tokens returned? + * + * ' ELEMSTART + * '/' ELEMSTART, '>' ELEMEND + * ' ' ELEMSTART + * '>' + * '/', '>' ELEMEND + * Attr + * '=' ATTRSTART + * "X ATTRVAL + * 'Y' ATTRVAL + * 'Z' ATTRVAL + * '"' ATTREND + * '>' + * '/', '>' ELEMEND + * + * ' ELEMEND + */ + + +typedef struct { + /* PUBLIC (read-only) */ + + /* Name of the current element, zero-length if not in any element. Changed + * after YXML_ELEMSTART. The pointer will remain valid up to and including + * the next non-YXML_ATTR* token, the pointed-to buffer will remain valid + * up to and including the YXML_ELEMEND for the corresponding element. */ + char *elem; + + /* The last read character(s) of an attribute value (YXML_ATTRVAL), element + * data (YXML_CONTENT), or processing instruction (YXML_PICONTENT). Changed + * after one of the respective YXML_ values is returned, and only valid + * until the next yxml_parse() call. Usually, this string only consists of + * a single byte, but multiple bytes are returned in the following cases: + * - "": The two characters "?x" + * - "": The two characters "]x" + * - "": The three characters "]]x" + * - "&#N;" and "&#xN;", where dec(n) > 127. The referenced Unicode + * character is then encoded in multiple UTF-8 bytes. + */ + char data[8]; + + /* Name of the current attribute. Changed after YXML_ATTRSTART, valid up to + * and including the next YXML_ATTREND. */ + char *attr; + + /* Name/target of the current processing instruction, zero-length if not in + * a PI. Changed after YXML_PISTART, valid up to (but excluding) + * the next YXML_PIEND. */ + char *pi; + + /* Line number, byte offset within that line, and total bytes read. These + * values refer to the position _after_ the last byte given to + * yxml_parse(). These are useful for debugging and error reporting. */ + uint64_t byte; + uint64_t total; + uint32_t line; + + + /* PRIVATE */ + int state; + unsigned char *stack; /* Stack of element names + attribute/PI name, separated by \0. Also starts with a \0. */ + size_t stacksize, stacklen; + unsigned reflen; + unsigned quote; + int nextstate; /* Used for '@' state remembering and for the "string" consuming state */ + unsigned ignore; + unsigned char *string; +} yxml_t; + + +#ifdef __cplusplus +extern "C" { +#endif + +void yxml_init(yxml_t *, void *, size_t); + + +yxml_ret_t yxml_parse(yxml_t *, int); + + +/* May be called after the last character has been given to yxml_parse(). + * Returns YXML_OK if the XML document is valid, YXML_EEOF otherwise. Using + * this function isn't really necessary, but can be used to detect documents + * that don't end correctly. In particular, an error is returned when the XML + * document did not contain a (complete) root element, or when the document + * ended while in a comment or processing instruction. */ +yxml_ret_t yxml_eof(yxml_t *); + +#ifdef __cplusplus +} +#endif + + +/* Returns the length of the element name (x->elem), attribute name (x->attr), + * or PI name (x->pi). This function should ONLY be used directly after the + * YXML_ELEMSTART, YXML_ATTRSTART or YXML_PISTART (respectively) tokens have + * been returned by yxml_parse(), calling this at any other time may not give + * the correct results. This function should also NOT be used on strings other + * than x->elem, x->attr or x->pi. */ +static inline size_t yxml_symlen(yxml_t *x, const char *s) { + return (x->stack + x->stacklen) - (const unsigned char*)s; +} + +#endif + +/* vim: set noet sw=4 ts=4: */ diff --git a/src/info.c b/src/info.c new file mode 100644 index 0000000..ac612c4 --- /dev/null +++ b/src/info.c @@ -0,0 +1,153 @@ +/* + Copyright (c) 2022 jdeokkim + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include + +#include "saerom.h" + +/* | `info` 모듈 상수 및 변수... | */ + +/* `info` 모듈 명령어의 매개 변수. */ +static struct discord_application_command_option options[] = { }; + +/* `info` 모듈 명령어에 대한 정보. */ +static struct discord_create_global_application_command params = { + .name = "info", + .description = "Show information about this bot", + .default_permission = true, + .options = &(struct discord_application_command_options) { + .size = sizeof(options) / sizeof(*options), + .array = options + } +}; + +/* | `info` 모듈 함수... | */ + +/* `info` 모듈 명령어를 생성한다. */ +void create_info_command(struct discord *client) { + discord_create_global_application_command( + client, + APPLICATION_ID, + ¶ms, + NULL + ); +} + +/* `info` 모듈 명령어에 할당된 메모리를 해제한다. */ +void release_info_command(struct discord *client) { + /* no-op */ +} + +/* `info` 모듈 명령어를 실행한다. */ +void run_info_command( + struct discord *client, + const struct discord_interaction *event +) { + if (event->type == DISCORD_INTERACTION_MESSAGE_COMPONENT) return; + + char cpu_usage_str[MAX_STRING_SIZE]; + char ram_usage_str[MAX_STRING_SIZE]; + + char uptime_str[MAX_STRING_SIZE]; + char ping_str[MAX_STRING_SIZE]; + char flags_str[MAX_STRING_SIZE]; + + struct sr_config *config = get_sr_config(); + + time_t uptime_in_seconds = get_uptime() * 0.001f; + + snprintf(cpu_usage_str, sizeof(cpu_usage_str), "%.1f%%", get_cpu_usage()); + snprintf(ram_usage_str, sizeof(ram_usage_str), "%.1fMB", get_ram_usage()); + + strftime(uptime_str, sizeof(uptime_str), "%T", gmtime(&uptime_in_seconds)); + snprintf(ping_str, sizeof(ping_str), "%dms", discord_get_ping(client)); + snprintf(flags_str, sizeof(flags_str), "0x%02hhX", config->flags); + + struct discord_embed_field fields[] = { + { + .name = "Version", + .value = APPLICATION_VERSION, + .Inline = true + }, + { + .name = "Uptime", + .value = uptime_str, + .Inline = true + }, + { + .name = "Latency", + .value = ping_str, + .Inline = true + }, + { + .name = "CPU", + .value = cpu_usage_str, + .Inline = true + }, + { + .name = "RAM", + .value = ram_usage_str, + .Inline = true + }, + { + .name = "FLAGS", + .value = flags_str, + .Inline = true + }, + }; + + struct discord_embed embeds[] = { + { + .title = APPLICATION_NAME, + .description = APPLICATION_DESCRIPTION, + .url = APPLICATION_PROJECT_URL, + .timestamp = discord_timestamp(client), + .footer = &(struct discord_embed_footer) { + .text = "✨" + }, + .thumbnail = &(struct discord_embed_thumbnail) { + .url = (char *) get_avatar_url(discord_get_self(client)) + }, + .author = &(struct discord_embed_author) { + .name = DEVELOPER_NAME, + .icon_url = DEVELOPER_ICON_URL + }, + .fields = &(struct discord_embed_fields) { + .size = sizeof(fields) / sizeof(*fields), + .array = fields + } + } + }; + + struct discord_interaction_response params = { + .type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE, + .data = &(struct discord_interaction_callback_data) { + .embeds = &(struct discord_embeds) { + .size = sizeof(embeds) / sizeof(*embeds), + .array = embeds + } + } + }; + + discord_create_interaction_response( + client, + event->id, + event->token, + ¶ms, + NULL + ); +} \ No newline at end of file diff --git a/src/json.c b/src/json.c new file mode 100644 index 0000000..2f0452a --- /dev/null +++ b/src/json.c @@ -0,0 +1,1381 @@ +/* + Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#include "json.h" + +#include +#include +#include +#include +#include + +#define out_of_memory() do { \ + fprintf(stderr, "Out of memory.\n"); \ + exit(EXIT_FAILURE); \ + } while (0) + +/* Sadly, strdup is not portable. */ +static char *json_strdup(const char *str) +{ + char *ret = (char*) malloc(strlen(str) + 1); + if (ret == NULL) + out_of_memory(); + strcpy(ret, str); + return ret; +} + +/* String buffer */ + +typedef struct +{ + char *cur; + char *end; + char *start; +} SB; + +static void sb_init(SB *sb) +{ + sb->start = (char*) malloc(17); + if (sb->start == NULL) + out_of_memory(); + sb->cur = sb->start; + sb->end = sb->start + 16; +} + +/* sb and need may be evaluated multiple times. */ +#define sb_need(sb, need) do { \ + if ((sb)->end - (sb)->cur < (need)) \ + sb_grow(sb, need); \ + } while (0) + +static void sb_grow(SB *sb, int need) +{ + size_t length = sb->cur - sb->start; + size_t alloc = sb->end - sb->start; + + do { + alloc *= 2; + } while (alloc < length + need); + + sb->start = (char*) realloc(sb->start, alloc + 1); + if (sb->start == NULL) + out_of_memory(); + sb->cur = sb->start + length; + sb->end = sb->start + alloc; +} + +static void sb_put(SB *sb, const char *bytes, int count) +{ + sb_need(sb, count); + memcpy(sb->cur, bytes, count); + sb->cur += count; +} + +#define sb_putc(sb, c) do { \ + if ((sb)->cur >= (sb)->end) \ + sb_grow(sb, 1); \ + *(sb)->cur++ = (c); \ + } while (0) + +static void sb_puts(SB *sb, const char *str) +{ + sb_put(sb, str, strlen(str)); +} + +static char *sb_finish(SB *sb) +{ + *sb->cur = 0; + assert(sb->start <= sb->cur && strlen(sb->start) == (size_t)(sb->cur - sb->start)); + return sb->start; +} + +static void sb_free(SB *sb) +{ + free(sb->start); +} + +/* + * Unicode helper functions + * + * These are taken from the ccan/charset module and customized a bit. + * Putting them here means the compiler can (choose to) inline them, + * and it keeps ccan/json from having a dependency. + */ + +/* + * Type for Unicode codepoints. + * We need our own because wchar_t might be 16 bits. + */ +typedef uint32_t uchar_t; + +/* + * Validate a single UTF-8 character starting at @s. + * The string must be null-terminated. + * + * If it's valid, return its length (1 thru 4). + * If it's invalid or clipped, return 0. + * + * This function implements the syntax given in RFC3629, which is + * the same as that given in The Unicode Standard, Version 6.0. + * + * It has the following properties: + * + * * All codepoints U+0000..U+10FFFF may be encoded, + * except for U+D800..U+DFFF, which are reserved + * for UTF-16 surrogate pair encoding. + * * UTF-8 byte sequences longer than 4 bytes are not permitted, + * as they exceed the range of Unicode. + * * The sixty-six Unicode "non-characters" are permitted + * (namely, U+FDD0..U+FDEF, U+xxFFFE, and U+xxFFFF). + */ +static int utf8_validate_cz(const char *s) +{ + unsigned char c = *s++; + + if (c <= 0x7F) { /* 00..7F */ + return 1; + } else if (c <= 0xC1) { /* 80..C1 */ + /* Disallow overlong 2-byte sequence. */ + return 0; + } else if (c <= 0xDF) { /* C2..DF */ + /* Make sure subsequent byte is in the range 0x80..0xBF. */ + if (((unsigned char)*s++ & 0xC0) != 0x80) + return 0; + + return 2; + } else if (c <= 0xEF) { /* E0..EF */ + /* Disallow overlong 3-byte sequence. */ + if (c == 0xE0 && (unsigned char)*s < 0xA0) + return 0; + + /* Disallow U+D800..U+DFFF. */ + if (c == 0xED && (unsigned char)*s > 0x9F) + return 0; + + /* Make sure subsequent bytes are in the range 0x80..0xBF. */ + if (((unsigned char)*s++ & 0xC0) != 0x80) + return 0; + if (((unsigned char)*s++ & 0xC0) != 0x80) + return 0; + + return 3; + } else if (c <= 0xF4) { /* F0..F4 */ + /* Disallow overlong 4-byte sequence. */ + if (c == 0xF0 && (unsigned char)*s < 0x90) + return 0; + + /* Disallow codepoints beyond U+10FFFF. */ + if (c == 0xF4 && (unsigned char)*s > 0x8F) + return 0; + + /* Make sure subsequent bytes are in the range 0x80..0xBF. */ + if (((unsigned char)*s++ & 0xC0) != 0x80) + return 0; + if (((unsigned char)*s++ & 0xC0) != 0x80) + return 0; + if (((unsigned char)*s++ & 0xC0) != 0x80) + return 0; + + return 4; + } else { /* F5..FF */ + return 0; + } +} + +/* Validate a null-terminated UTF-8 string. */ +static bool utf8_validate(const char *s) +{ + int len; + + for (; *s != 0; s += len) { + len = utf8_validate_cz(s); + if (len == 0) + return false; + } + + return true; +} + +/* + * Read a single UTF-8 character starting at @s, + * returning the length, in bytes, of the character read. + * + * This function assumes input is valid UTF-8, + * and that there are enough characters in front of @s. + */ +static int utf8_read_char(const char *s, uchar_t *out) +{ + const unsigned char *c = (const unsigned char*) s; + + assert(utf8_validate_cz(s)); + + if (c[0] <= 0x7F) { + /* 00..7F */ + *out = c[0]; + return 1; + } else if (c[0] <= 0xDF) { + /* C2..DF (unless input is invalid) */ + *out = ((uchar_t)c[0] & 0x1F) << 6 | + ((uchar_t)c[1] & 0x3F); + return 2; + } else if (c[0] <= 0xEF) { + /* E0..EF */ + *out = ((uchar_t)c[0] & 0xF) << 12 | + ((uchar_t)c[1] & 0x3F) << 6 | + ((uchar_t)c[2] & 0x3F); + return 3; + } else { + /* F0..F4 (unless input is invalid) */ + *out = ((uchar_t)c[0] & 0x7) << 18 | + ((uchar_t)c[1] & 0x3F) << 12 | + ((uchar_t)c[2] & 0x3F) << 6 | + ((uchar_t)c[3] & 0x3F); + return 4; + } +} + +/* + * Write a single UTF-8 character to @s, + * returning the length, in bytes, of the character written. + * + * @unicode must be U+0000..U+10FFFF, but not U+D800..U+DFFF. + * + * This function will write up to 4 bytes to @out. + */ +static int utf8_write_char(uchar_t unicode, char *out) +{ + unsigned char *o = (unsigned char*) out; + + assert(unicode <= 0x10FFFF && !(unicode >= 0xD800 && unicode <= 0xDFFF)); + + if (unicode <= 0x7F) { + /* U+0000..U+007F */ + *o++ = unicode; + return 1; + } else if (unicode <= 0x7FF) { + /* U+0080..U+07FF */ + *o++ = 0xC0 | unicode >> 6; + *o++ = 0x80 | (unicode & 0x3F); + return 2; + } else if (unicode <= 0xFFFF) { + /* U+0800..U+FFFF */ + *o++ = 0xE0 | unicode >> 12; + *o++ = 0x80 | (unicode >> 6 & 0x3F); + *o++ = 0x80 | (unicode & 0x3F); + return 3; + } else { + /* U+10000..U+10FFFF */ + *o++ = 0xF0 | unicode >> 18; + *o++ = 0x80 | (unicode >> 12 & 0x3F); + *o++ = 0x80 | (unicode >> 6 & 0x3F); + *o++ = 0x80 | (unicode & 0x3F); + return 4; + } +} + +/* + * Compute the Unicode codepoint of a UTF-16 surrogate pair. + * + * @uc should be 0xD800..0xDBFF, and @lc should be 0xDC00..0xDFFF. + * If they aren't, this function returns false. + */ +static bool from_surrogate_pair(uint16_t uc, uint16_t lc, uchar_t *unicode) +{ + if (uc >= 0xD800 && uc <= 0xDBFF && lc >= 0xDC00 && lc <= 0xDFFF) { + *unicode = 0x10000 + ((((uchar_t)uc & 0x3FF) << 10) | (lc & 0x3FF)); + return true; + } else { + return false; + } +} + +/* + * Construct a UTF-16 surrogate pair given a Unicode codepoint. + * + * @unicode must be U+10000..U+10FFFF. + */ +static void to_surrogate_pair(uchar_t unicode, uint16_t *uc, uint16_t *lc) +{ + uchar_t n; + + assert(unicode >= 0x10000 && unicode <= 0x10FFFF); + + n = unicode - 0x10000; + *uc = ((n >> 10) & 0x3FF) | 0xD800; + *lc = (n & 0x3FF) | 0xDC00; +} + +#define is_space(c) ((c) == '\t' || (c) == '\n' || (c) == '\r' || (c) == ' ') +#define is_digit(c) ((c) >= '0' && (c) <= '9') + +static bool parse_value (const char **sp, JsonNode **out); +static bool parse_string (const char **sp, char **out); +static bool parse_number (const char **sp, double *out); +static bool parse_array (const char **sp, JsonNode **out); +static bool parse_object (const char **sp, JsonNode **out); +static bool parse_hex16 (const char **sp, uint16_t *out); + +static bool expect_literal (const char **sp, const char *str); +static void skip_space (const char **sp); + +static void emit_value (SB *out, const JsonNode *node); +static void emit_value_indented (SB *out, const JsonNode *node, const char *space, int indent_level); +static void emit_string (SB *out, const char *str); +static void emit_number (SB *out, double num); +static void emit_array (SB *out, const JsonNode *array); +static void emit_array_indented (SB *out, const JsonNode *array, const char *space, int indent_level); +static void emit_object (SB *out, const JsonNode *object); +static void emit_object_indented (SB *out, const JsonNode *object, const char *space, int indent_level); + +static int write_hex16(char *out, uint16_t val); + +static JsonNode *mknode(JsonTag tag); +static void append_node(JsonNode *parent, JsonNode *child); +static void prepend_node(JsonNode *parent, JsonNode *child); +static void append_member(JsonNode *object, char *key, JsonNode *value); + +/* Assertion-friendly validity checks */ +static bool tag_is_valid(unsigned int tag); +static bool number_is_valid(const char *num); + +JsonNode *json_decode(const char *json) +{ + const char *s = json; + JsonNode *ret; + + skip_space(&s); + if (!parse_value(&s, &ret)) + return NULL; + + skip_space(&s); + if (*s != 0) { + json_delete(ret); + return NULL; + } + + return ret; +} + +char *json_encode(const JsonNode *node) +{ + return json_stringify(node, NULL); +} + +char *json_encode_string(const char *str) +{ + SB sb; + sb_init(&sb); + + emit_string(&sb, str); + + return sb_finish(&sb); +} + +char *json_stringify(const JsonNode *node, const char *space) +{ + SB sb; + sb_init(&sb); + + if (space != NULL) + emit_value_indented(&sb, node, space, 0); + else + emit_value(&sb, node); + + return sb_finish(&sb); +} + +void json_delete(JsonNode *node) +{ + if (node != NULL) { + json_remove_from_parent(node); + + switch (node->tag) { + case JSON_STRING: + free(node->string_); + break; + case JSON_ARRAY: + case JSON_OBJECT: + { + JsonNode *child, *next; + for (child = node->children.head; child != NULL; child = next) { + next = child->next; + json_delete(child); + } + break; + } + default:; + } + + free(node); + } +} + +bool json_validate(const char *json) +{ + const char *s = json; + + skip_space(&s); + if (!parse_value(&s, NULL)) + return false; + + skip_space(&s); + if (*s != 0) + return false; + + return true; +} + +JsonNode *json_find_element(JsonNode *array, int index) +{ + JsonNode *element; + int i = 0; + + if (array == NULL || array->tag != JSON_ARRAY) + return NULL; + + json_foreach(element, array) { + if (i == index) + return element; + i++; + } + + return NULL; +} + +JsonNode *json_find_member(JsonNode *object, const char *name) +{ + JsonNode *member; + + if (object == NULL || object->tag != JSON_OBJECT) + return NULL; + + json_foreach(member, object) + if (strcmp(member->key, name) == 0) + return member; + + return NULL; +} + +JsonNode *json_first_child(const JsonNode *node) +{ + if (node != NULL && (node->tag == JSON_ARRAY || node->tag == JSON_OBJECT)) + return node->children.head; + return NULL; +} + +static JsonNode *mknode(JsonTag tag) +{ + JsonNode *ret = (JsonNode*) calloc(1, sizeof(JsonNode)); + if (ret == NULL) + out_of_memory(); + ret->tag = tag; + return ret; +} + +JsonNode *json_mknull(void) +{ + return mknode(JSON_NULL); +} + +JsonNode *json_mkbool(bool b) +{ + JsonNode *ret = mknode(JSON_BOOL); + ret->bool_ = b; + return ret; +} + +static JsonNode *mkstring(char *s) +{ + JsonNode *ret = mknode(JSON_STRING); + ret->string_ = s; + return ret; +} + +JsonNode *json_mkstring(const char *s) +{ + return mkstring(json_strdup(s)); +} + +JsonNode *json_mknumber(double n) +{ + JsonNode *node = mknode(JSON_NUMBER); + node->number_ = n; + return node; +} + +JsonNode *json_mkarray(void) +{ + return mknode(JSON_ARRAY); +} + +JsonNode *json_mkobject(void) +{ + return mknode(JSON_OBJECT); +} + +static void append_node(JsonNode *parent, JsonNode *child) +{ + child->parent = parent; + child->prev = parent->children.tail; + child->next = NULL; + + if (parent->children.tail != NULL) + parent->children.tail->next = child; + else + parent->children.head = child; + parent->children.tail = child; +} + +static void prepend_node(JsonNode *parent, JsonNode *child) +{ + child->parent = parent; + child->prev = NULL; + child->next = parent->children.head; + + if (parent->children.head != NULL) + parent->children.head->prev = child; + else + parent->children.tail = child; + parent->children.head = child; +} + +static void append_member(JsonNode *object, char *key, JsonNode *value) +{ + value->key = key; + append_node(object, value); +} + +void json_append_element(JsonNode *array, JsonNode *element) +{ + assert(array->tag == JSON_ARRAY); + assert(element->parent == NULL); + + append_node(array, element); +} + +void json_prepend_element(JsonNode *array, JsonNode *element) +{ + assert(array->tag == JSON_ARRAY); + assert(element->parent == NULL); + + prepend_node(array, element); +} + +void json_append_member(JsonNode *object, const char *key, JsonNode *value) +{ + assert(object->tag == JSON_OBJECT); + assert(value->parent == NULL); + + append_member(object, json_strdup(key), value); +} + +void json_prepend_member(JsonNode *object, const char *key, JsonNode *value) +{ + assert(object->tag == JSON_OBJECT); + assert(value->parent == NULL); + + value->key = json_strdup(key); + prepend_node(object, value); +} + +void json_remove_from_parent(JsonNode *node) +{ + JsonNode *parent = node->parent; + + if (parent != NULL) { + if (node->prev != NULL) + node->prev->next = node->next; + else + parent->children.head = node->next; + if (node->next != NULL) + node->next->prev = node->prev; + else + parent->children.tail = node->prev; + + free(node->key); + + node->parent = NULL; + node->prev = node->next = NULL; + node->key = NULL; + } +} + +static bool parse_value(const char **sp, JsonNode **out) +{ + const char *s = *sp; + + switch (*s) { + case 'n': + if (expect_literal(&s, "null")) { + if (out) + *out = json_mknull(); + *sp = s; + return true; + } + return false; + + case 'f': + if (expect_literal(&s, "false")) { + if (out) + *out = json_mkbool(false); + *sp = s; + return true; + } + return false; + + case 't': + if (expect_literal(&s, "true")) { + if (out) + *out = json_mkbool(true); + *sp = s; + return true; + } + return false; + + case '"': { + char *str; + if (parse_string(&s, out ? &str : NULL)) { + if (out) + *out = mkstring(str); + *sp = s; + return true; + } + return false; + } + + case '[': + if (parse_array(&s, out)) { + *sp = s; + return true; + } + return false; + + case '{': + if (parse_object(&s, out)) { + *sp = s; + return true; + } + return false; + + default: { + double num; + if (parse_number(&s, out ? &num : NULL)) { + if (out) + *out = json_mknumber(num); + *sp = s; + return true; + } + return false; + } + } +} + +static bool parse_array(const char **sp, JsonNode **out) +{ + const char *s = *sp; + JsonNode *ret = out ? json_mkarray() : NULL; + JsonNode *element; + + if (*s++ != '[') + goto failure; + skip_space(&s); + + if (*s == ']') { + s++; + goto success; + } + + for (;;) { + if (!parse_value(&s, out ? &element : NULL)) + goto failure; + skip_space(&s); + + if (out) + json_append_element(ret, element); + + if (*s == ']') { + s++; + goto success; + } + + if (*s++ != ',') + goto failure; + skip_space(&s); + } + +success: + *sp = s; + if (out) + *out = ret; + return true; + +failure: + json_delete(ret); + return false; +} + +static bool parse_object(const char **sp, JsonNode **out) +{ + const char *s = *sp; + JsonNode *ret = out ? json_mkobject() : NULL; + char *key; + JsonNode *value; + + if (*s++ != '{') + goto failure; + skip_space(&s); + + if (*s == '}') { + s++; + goto success; + } + + for (;;) { + if (!parse_string(&s, out ? &key : NULL)) + goto failure; + skip_space(&s); + + if (*s++ != ':') + goto failure_free_key; + skip_space(&s); + + if (!parse_value(&s, out ? &value : NULL)) + goto failure_free_key; + skip_space(&s); + + if (out) + append_member(ret, key, value); + + if (*s == '}') { + s++; + goto success; + } + + if (*s++ != ',') + goto failure; + skip_space(&s); + } + +success: + *sp = s; + if (out) + *out = ret; + return true; + +failure_free_key: + if (out) + free(key); +failure: + json_delete(ret); + return false; +} + +bool parse_string(const char **sp, char **out) +{ + const char *s = *sp; + SB sb; + char throwaway_buffer[4]; + /* enough space for a UTF-8 character */ + char *b; + + if (*s++ != '"') + return false; + + if (out) { + sb_init(&sb); + sb_need(&sb, 4); + b = sb.cur; + } else { + b = throwaway_buffer; + } + + while (*s != '"') { + unsigned char c = *s++; + + /* Parse next character, and write it to b. */ + if (c == '\\') { + c = *s++; + switch (c) { + case '"': + case '\\': + case '/': + *b++ = c; + break; + case 'b': + *b++ = '\b'; + break; + case 'f': + *b++ = '\f'; + break; + case 'n': + *b++ = '\n'; + break; + case 'r': + *b++ = '\r'; + break; + case 't': + *b++ = '\t'; + break; + case 'u': + { + uint16_t uc, lc; + uchar_t unicode; + + if (!parse_hex16(&s, &uc)) + goto failed; + + if (uc >= 0xD800 && uc <= 0xDFFF) { + /* Handle UTF-16 surrogate pair. */ + if (*s++ != '\\' || *s++ != 'u' || !parse_hex16(&s, &lc)) + goto failed; /* Incomplete surrogate pair. */ + if (!from_surrogate_pair(uc, lc, &unicode)) + goto failed; /* Invalid surrogate pair. */ + } else if (uc == 0) { + /* Disallow "\u0000". */ + goto failed; + } else { + unicode = uc; + } + + b += utf8_write_char(unicode, b); + break; + } + default: + /* Invalid escape */ + goto failed; + } + } else if (c <= 0x1F) { + /* Control characters are not allowed in string literals. */ + goto failed; + } else { + /* Validate and echo a UTF-8 character. */ + int len; + + s--; + len = utf8_validate_cz(s); + if (len == 0) + goto failed; /* Invalid UTF-8 character. */ + + while (len--) + *b++ = *s++; + } + + /* + * Update sb to know about the new bytes, + * and set up b to write another character. + */ + if (out) { + sb.cur = b; + sb_need(&sb, 4); + b = sb.cur; + } else { + b = throwaway_buffer; + } + } + s++; + + if (out) + *out = sb_finish(&sb); + *sp = s; + return true; + +failed: + if (out) + sb_free(&sb); + return false; +} + +/* + * The JSON spec says that a number shall follow this precise pattern + * (spaces and quotes added for readability): + * '-'? (0 | [1-9][0-9]*) ('.' [0-9]+)? ([Ee] [+-]? [0-9]+)? + * + * However, some JSON parsers are more liberal. For instance, PHP accepts + * '.5' and '1.'. JSON.parse accepts '+3'. + * + * This function takes the strict approach. + */ +bool parse_number(const char **sp, double *out) +{ + const char *s = *sp; + + /* '-'? */ + if (*s == '-') + s++; + + /* (0 | [1-9][0-9]*) */ + if (*s == '0') { + s++; + } else { + if (!is_digit(*s)) + return false; + do { + s++; + } while (is_digit(*s)); + } + + /* ('.' [0-9]+)? */ + if (*s == '.') { + s++; + if (!is_digit(*s)) + return false; + do { + s++; + } while (is_digit(*s)); + } + + /* ([Ee] [+-]? [0-9]+)? */ + if (*s == 'E' || *s == 'e') { + s++; + if (*s == '+' || *s == '-') + s++; + if (!is_digit(*s)) + return false; + do { + s++; + } while (is_digit(*s)); + } + + if (out) + *out = strtod(*sp, NULL); + + *sp = s; + return true; +} + +static void skip_space(const char **sp) +{ + const char *s = *sp; + while (is_space(*s)) + s++; + *sp = s; +} + +static void emit_value(SB *out, const JsonNode *node) +{ + assert(tag_is_valid(node->tag)); + switch (node->tag) { + case JSON_NULL: + sb_puts(out, "null"); + break; + case JSON_BOOL: + sb_puts(out, node->bool_ ? "true" : "false"); + break; + case JSON_STRING: + emit_string(out, node->string_); + break; + case JSON_NUMBER: + emit_number(out, node->number_); + break; + case JSON_ARRAY: + emit_array(out, node); + break; + case JSON_OBJECT: + emit_object(out, node); + break; + default: + assert(false); + } +} + +void emit_value_indented(SB *out, const JsonNode *node, const char *space, int indent_level) +{ + assert(tag_is_valid(node->tag)); + switch (node->tag) { + case JSON_NULL: + sb_puts(out, "null"); + break; + case JSON_BOOL: + sb_puts(out, node->bool_ ? "true" : "false"); + break; + case JSON_STRING: + emit_string(out, node->string_); + break; + case JSON_NUMBER: + emit_number(out, node->number_); + break; + case JSON_ARRAY: + emit_array_indented(out, node, space, indent_level); + break; + case JSON_OBJECT: + emit_object_indented(out, node, space, indent_level); + break; + default: + assert(false); + } +} + +static void emit_array(SB *out, const JsonNode *array) +{ + const JsonNode *element; + + sb_putc(out, '['); + json_foreach(element, array) { + emit_value(out, element); + if (element->next != NULL) + sb_putc(out, ','); + } + sb_putc(out, ']'); +} + +static void emit_array_indented(SB *out, const JsonNode *array, const char *space, int indent_level) +{ + const JsonNode *element = array->children.head; + int i; + + if (element == NULL) { + sb_puts(out, "[]"); + return; + } + + sb_puts(out, "[\n"); + while (element != NULL) { + for (i = 0; i < indent_level + 1; i++) + sb_puts(out, space); + emit_value_indented(out, element, space, indent_level + 1); + + element = element->next; + sb_puts(out, element != NULL ? ",\n" : "\n"); + } + for (i = 0; i < indent_level; i++) + sb_puts(out, space); + sb_putc(out, ']'); +} + +static void emit_object(SB *out, const JsonNode *object) +{ + const JsonNode *member; + + sb_putc(out, '{'); + json_foreach(member, object) { + emit_string(out, member->key); + sb_putc(out, ':'); + emit_value(out, member); + if (member->next != NULL) + sb_putc(out, ','); + } + sb_putc(out, '}'); +} + +static void emit_object_indented(SB *out, const JsonNode *object, const char *space, int indent_level) +{ + const JsonNode *member = object->children.head; + int i; + + if (member == NULL) { + sb_puts(out, "{}"); + return; + } + + sb_puts(out, "{\n"); + while (member != NULL) { + for (i = 0; i < indent_level + 1; i++) + sb_puts(out, space); + emit_string(out, member->key); + sb_puts(out, ": "); + emit_value_indented(out, member, space, indent_level + 1); + + member = member->next; + sb_puts(out, member != NULL ? ",\n" : "\n"); + } + for (i = 0; i < indent_level; i++) + sb_puts(out, space); + sb_putc(out, '}'); +} + +void emit_string(SB *out, const char *str) +{ + bool escape_unicode = false; + const char *s = str; + char *b; + + assert(utf8_validate(str)); + + /* + * 14 bytes is enough space to write up to two + * \uXXXX escapes and two quotation marks. + */ + sb_need(out, 14); + b = out->cur; + + *b++ = '"'; + while (*s != 0) { + unsigned char c = *s++; + + /* Encode the next character, and write it to b. */ + switch (c) { + case '"': + *b++ = '\\'; + *b++ = '"'; + break; + case '\\': + *b++ = '\\'; + *b++ = '\\'; + break; + case '\b': + *b++ = '\\'; + *b++ = 'b'; + break; + case '\f': + *b++ = '\\'; + *b++ = 'f'; + break; + case '\n': + *b++ = '\\'; + *b++ = 'n'; + break; + case '\r': + *b++ = '\\'; + *b++ = 'r'; + break; + case '\t': + *b++ = '\\'; + *b++ = 't'; + break; + default: { + int len; + + s--; + len = utf8_validate_cz(s); + + if (len == 0) { + /* + * Handle invalid UTF-8 character gracefully in production + * by writing a replacement character (U+FFFD) + * and skipping a single byte. + * + * This should never happen when assertions are enabled + * due to the assertion at the beginning of this function. + */ + assert(false); + if (escape_unicode) { + strcpy(b, "\\uFFFD"); + b += 6; + } else { + *b++ = 0xEF; + *b++ = 0xBF; + *b++ = 0xBD; + } + s++; + } else if (c < 0x1F || (c >= 0x80 && escape_unicode)) { + /* Encode using \u.... */ + uint32_t unicode; + + s += utf8_read_char(s, &unicode); + + if (unicode <= 0xFFFF) { + *b++ = '\\'; + *b++ = 'u'; + b += write_hex16(b, unicode); + } else { + /* Produce a surrogate pair. */ + uint16_t uc, lc; + assert(unicode <= 0x10FFFF); + to_surrogate_pair(unicode, &uc, &lc); + *b++ = '\\'; + *b++ = 'u'; + b += write_hex16(b, uc); + *b++ = '\\'; + *b++ = 'u'; + b += write_hex16(b, lc); + } + } else { + /* Write the character directly. */ + while (len--) + *b++ = *s++; + } + + break; + } + } + + /* + * Update *out to know about the new bytes, + * and set up b to write another encoded character. + */ + out->cur = b; + sb_need(out, 14); + b = out->cur; + } + *b++ = '"'; + + out->cur = b; +} + +static void emit_number(SB *out, double num) +{ + /* + * This isn't exactly how JavaScript renders numbers, + * but it should produce valid JSON for reasonable numbers + * preserve precision well enough, and avoid some oddities + * like 0.3 -> 0.299999999999999988898 . + */ + char buf[64]; + sprintf(buf, "%.16g", num); + + if (number_is_valid(buf)) + sb_puts(out, buf); + else + sb_puts(out, "null"); +} + +static bool tag_is_valid(unsigned int tag) +{ + return (/* tag >= JSON_NULL && */ tag <= JSON_OBJECT); +} + +static bool number_is_valid(const char *num) +{ + return (parse_number(&num, NULL) && *num == '\0'); +} + +static bool expect_literal(const char **sp, const char *str) +{ + const char *s = *sp; + + while (*str != '\0') + if (*s++ != *str++) + return false; + + *sp = s; + return true; +} + +/* + * Parses exactly 4 hex characters (capital or lowercase). + * Fails if any input chars are not [0-9A-Fa-f]. + */ +static bool parse_hex16(const char **sp, uint16_t *out) +{ + const char *s = *sp; + uint16_t ret = 0; + uint16_t i; + uint16_t tmp; + char c; + + for (i = 0; i < 4; i++) { + c = *s++; + if (c >= '0' && c <= '9') + tmp = c - '0'; + else if (c >= 'A' && c <= 'F') + tmp = c - 'A' + 10; + else if (c >= 'a' && c <= 'f') + tmp = c - 'a' + 10; + else + return false; + + ret <<= 4; + ret += tmp; + } + + if (out) + *out = ret; + *sp = s; + return true; +} + +/* + * Encodes a 16-bit number into hexadecimal, + * writing exactly 4 hex chars. + */ +static int write_hex16(char *out, uint16_t val) +{ + const char *hex = "0123456789ABCDEF"; + + *out++ = hex[(val >> 12) & 0xF]; + *out++ = hex[(val >> 8) & 0xF]; + *out++ = hex[(val >> 4) & 0xF]; + *out++ = hex[ val & 0xF]; + + return 4; +} + +bool json_check(const JsonNode *node, char errmsg[256]) +{ + #define problem(...) do { \ + if (errmsg != NULL) \ + snprintf(errmsg, 256, __VA_ARGS__); \ + return false; \ + } while (0) + + if (node->key != NULL && !utf8_validate(node->key)) + problem("key contains invalid UTF-8"); + + if (!tag_is_valid(node->tag)) + problem("tag is invalid (%u)", node->tag); + + if (node->tag == JSON_BOOL) { + if (node->bool_ != false && node->bool_ != true) + problem("bool_ is neither false (%d) nor true (%d)", (int)false, (int)true); + } else if (node->tag == JSON_STRING) { + if (node->string_ == NULL) + problem("string_ is NULL"); + if (!utf8_validate(node->string_)) + problem("string_ contains invalid UTF-8"); + } else if (node->tag == JSON_ARRAY || node->tag == JSON_OBJECT) { + JsonNode *head = node->children.head; + JsonNode *tail = node->children.tail; + + if (head == NULL || tail == NULL) { + if (head != NULL) + problem("tail is NULL, but head is not"); + if (tail != NULL) + problem("head is NULL, but tail is not"); + } else { + JsonNode *child; + JsonNode *last = NULL; + + if (head->prev != NULL) + problem("First child's prev pointer is not NULL"); + + for (child = head; child != NULL; last = child, child = child->next) { + if (child == node) + problem("node is its own child"); + if (child->next == child) + problem("child->next == child (cycle)"); + if (child->next == head) + problem("child->next == head (cycle)"); + + if (child->parent != node) + problem("child does not point back to parent"); + if (child->next != NULL && child->next->prev != child) + problem("child->next does not point back to child"); + + if (node->tag == JSON_ARRAY && child->key != NULL) + problem("Array element's key is not NULL"); + if (node->tag == JSON_OBJECT && child->key == NULL) + problem("Object member's key is NULL"); + + if (!json_check(child, errmsg)) + return false; + } + + if (last != tail) + problem("tail does not match pointer found by starting at head and following next links"); + } + } + + return true; + + #undef problem +} diff --git a/src/krdict.c b/src/krdict.c new file mode 100644 index 0000000..9199c07 --- /dev/null +++ b/src/krdict.c @@ -0,0 +1,589 @@ +/* + Copyright (c) 2022 jdeokkim + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include + +#include +#include + +#include "saerom.h" + +#define KRDICT_REQUEST_URL "https://krdict.korean.go.kr/api/search" + +#define KRDICT_MAX_ORDER_COUNT 8 + +/* | `krdict` 모듈 자료형 정의... | */ + +/* `krdict` 모듈 명령어 실행 정보를 나타내는 구조체. */ +struct krdict_context { + struct discord *client; + struct { + u64snowflake id; + char *token; + } event; +}; + +/* `krdict` 모듈 명령어의 개별 검색 결과를 나타내는 구조체. */ +struct krdict_item { + char word[MAX_STRING_SIZE]; + char origin[MAX_STRING_SIZE]; + char pos[MAX_STRING_SIZE]; + char link[MAX_STRING_SIZE]; + char entry[DISCORD_EMBED_DESCRIPTION_LEN]; +}; + +/* | `krdict` 모듈 상수 및 변수... | */ + +/* `krdict` 모듈 명령어의 매개 변수. */ +static struct discord_application_command_option options[] = { + { + .type = DISCORD_APPLICATION_OPTION_STRING, + .name = "query", + .description = "The text you're looking up", + .required = true + }, + { + .type = DISCORD_APPLICATION_OPTION_BOOLEAN, + .name = "translated", + .description = "Whether to translate the results to English or not" + } +}; + +/* `krdict` 모듈 명령어에 대한 정보. */ +static struct discord_create_global_application_command params = { + .name = "krd", + .description = "Search the given text in the Basic Korean Dictionary by the National Institute of Korean Language", + .default_permission = true, + .options = &(struct discord_application_command_options) { + .size = sizeof(options) / sizeof(*options), + .array = options + } +}; + +/* | `krdict` 모듈 함수... | */ + +/* 개인 메시지 전송에 성공했을 때 호출되는 함수. */ +static void on_direct_message_success( + struct discord *client, + struct discord_response *resp, + const struct discord_message *msg +); + +/* 개인 메시지 전송에 실패했을 때 호출되는 함수. */ +static void on_direct_message_failure( + struct discord *client, + struct discord_response *resp +); + +/* 컴포넌트와의 상호 작용 시에 호출되는 함수. */ +static void on_interaction( + struct discord *client, + const struct discord_interaction *event +); + +/* 컴포넌트와의 상호 작용이 끝났을 때 호출되는 함수. */ +static void on_interaction_complete( + struct discord *client, + void *data +); + +/* 요청 URL에서 응답을 받았을 때 호출되는 함수. */ +static void on_response(CURLV_STR res, void *user_data); + +/* 응답 결과로 받은 오류 메시지를 처리한다. */ +static void handle_error(struct krdict_context *context, const char *code); + +/* `krdict` 모듈 명령어를 생성한다. */ +void create_krdict_command(struct discord *client) { + discord_create_global_application_command( + client, + APPLICATION_ID, + ¶ms, + NULL + ); +} + +/* `krdict` 모듈 명령어에 할당된 메모리를 해제한다. */ +void release_krdict_command(struct discord *client) { + /* TODO: ... */ +} + +/* `krdict` 모듈 명령어를 실행한다. */ +void run_krdict_command( + struct discord *client, + const struct discord_interaction *event +) { + if (event->type == DISCORD_INTERACTION_MESSAGE_COMPONENT) { + on_interaction(client, event); + + return; + } + + char *query = "", *translated = "true"; + + for (int i = 0; i < event->data->options->size; i++) { + char *name = event->data->options->array[i].name; + char *value = event->data->options->array[i].value; + + if (string_equals(name, "query")) query = value; + else if (string_equals(name, "translated")) translated = value; + } + + CURLV_REQ request = { .callback = on_response }; + + request.easy = curl_easy_init(); + + char buffer[DISCORD_MAX_MESSAGE_LEN] = ""; + + struct sr_config *config = get_sr_config(); + + snprintf( + buffer, + DISCORD_MAX_MESSAGE_LEN, + string_equals(translated, "true") + ? "key=%s&q=%s&advanced=y&translated=y&trans_lang=1" + : "key=%s&q=%s&advanced=y", + config->krdict.api_key, + query + ); + + curl_easy_setopt(request.easy, CURLOPT_URL, KRDICT_REQUEST_URL); + curl_easy_setopt(request.easy, CURLOPT_POSTFIELDSIZE, strlen(buffer)); + curl_easy_setopt(request.easy, CURLOPT_COPYPOSTFIELDS, buffer); + curl_easy_setopt(request.easy, CURLOPT_SSL_VERIFYPEER, false); + curl_easy_setopt(request.easy, CURLOPT_POST, 1); + + struct krdict_context *context = malloc(sizeof(struct krdict_context)); + + context->client = client; + context->event.id = event->id; + context->event.token = malloc(strlen(event->token) + 1); + + strcpy(context->event.token, event->token); + + request.user_data = context; + + curlv_create_request(get_curlv(), &request); + + discord_create_interaction_response( + context->client, + context->event.id, + context->event.token, + &(struct discord_interaction_response) { + .type = DISCORD_INTERACTION_DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE + }, + NULL + ); +} + +/* 개인 메시지 전송에 성공했을 때 호출되는 함수. */ +static void on_direct_message_success( + struct discord *client, + struct discord_response *resp, + const struct discord_message *msg +) { + if (resp->data == NULL) return; + + struct discord_interaction *event = resp->data; + + struct discord_embed embeds[] = { + { + .title = "Results", + .description = "Sent you a direct message!", + .timestamp = discord_timestamp(client), + .footer = &(struct discord_embed_footer) { + .text = "🗒️" + } + } + }; + + struct discord_interaction_response params = { + .type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE, + .data = &(struct discord_interaction_callback_data) { + .flags = DISCORD_MESSAGE_EPHEMERAL, + .embeds = &(struct discord_embeds) { + .size = sizeof(embeds) / sizeof(*embeds), + .array = embeds + } + } + }; + + discord_create_interaction_response( + client, + event->id, + event->token, + ¶ms, + NULL + ); +} + +/* 개인 메시지 전송에 실패했을 때 호출되는 함수. */ +static void on_direct_message_failure( + struct discord *client, + struct discord_response *resp +) { + if (resp->data == NULL) return; + + struct discord_interaction *event = resp->data; + + struct discord_embed embeds[] = { + { + .title = "Results", + .description = "Failed to send you a direct message. " + "Please check your privacy settings!", + .timestamp = discord_timestamp(client), + .footer = &(struct discord_embed_footer) { + .text = "🗒️" + } + } + }; + + struct discord_interaction_response params = { + .type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE, + .data = &(struct discord_interaction_callback_data) { + .flags = DISCORD_MESSAGE_EPHEMERAL, + .embeds = &(struct discord_embeds) { + .size = sizeof(embeds) / sizeof(*embeds), + .array = embeds + } + } + }; + + discord_create_interaction_response( + client, + event->id, + event->token, + ¶ms, + NULL + ); +} + +/* 컴포넌트와의 상호 작용 시에 호출되는 함수. */ +static void on_interaction( + struct discord *client, + const struct discord_interaction *event +) { + struct discord_channel ret_channel = { .id = 0 }; + + const struct discord_user *user = (event->member != NULL) + ? event->member->user + : event->user; + + CCORDcode result = discord_create_dm( + client, + &(struct discord_create_dm) { + .recipient_id = user->id + }, + &(struct discord_ret_channel) { + .sync = &ret_channel + } + ); + + if (result != CCORD_OK) return; + + discord_channel_cleanup(&ret_channel); + + log_info( + "[SAEROM] Attempting to send a direct message to %s#%s", + user->username, + user->discriminator + ); + + struct discord_interaction *event_clone = malloc( + sizeof(struct discord_interaction) + ); + + event_clone->id = event->id; + event_clone->token = malloc(strlen(event->token) + 1); + + strcpy(event_clone->token, event->token); + + discord_create_message( + client, + ret_channel.id, + &(struct discord_create_message) { + .embeds = event->message->embeds + }, + &(struct discord_ret_message) { + .data = event_clone, + .cleanup = on_interaction_complete, + .done = on_direct_message_success, + .fail = on_direct_message_failure + } + ); +} + +/* 컴포넌트와의 상호 작용이 끝났을 때 호출되는 함수. */ +static void on_interaction_complete( + struct discord *client, + void *data +) { + struct discord_interaction *event = data; + + free(event->token); + free(event); +} + +/* 요청 URL에서 응답을 받았을 때 호출되는 함수. */ +static void on_response(CURLV_STR res, void *user_data) { + if (res.str == NULL || user_data == NULL) return; + + log_info("[SAEROM] Received %ld bytes from \"%s\"", res.len, KRDICT_REQUEST_URL); + + yxml_t *parser = malloc(sizeof(yxml_t) + res.len); + + yxml_init(parser, parser + 1, res.len); + + struct krdict_context *context = (struct krdict_context *) user_data; + struct krdict_item item; + + char buffer[DISCORD_EMBED_DESCRIPTION_LEN] = ""; + char content[DISCORD_MAX_MESSAGE_LEN] = ""; + + char *elem, *temp, *ptr; + + int len, num, order, total = 1; + + for (char *doc = res.str; *doc; doc++) { + yxml_ret_t result = yxml_parse(parser, *doc); + + if (result < 0) { + handle_error(context, "-1"); + + free(parser); + + return; + } + + switch (result) { + case YXML_ELEMSTART: + elem = parser->elem; + ptr = content; + + break; + + case YXML_CONTENT: + temp = parser->data; + + while (*temp != 0) { + if (*temp == '\n' || *temp == '\t') { + temp++; + + continue; + } + + *(ptr++) = *(temp++); + } + + break; + + case YXML_ELEMEND: + *ptr = 0; + + if (string_equals(parser->elem, "error")) { + handle_error(context, content); + + free(parser); + + return; + } + + if (string_equals(parser->elem, "channel")) { + if (string_equals(elem, "total")) { + total = atoi(content); + + if (total <= 0) break; + } else if (string_equals(elem, "num")) { + num = atoi(content); + + if (total > num) total = num; + } else { + continue; + } + } + + if (string_equals(elem, "word")) { + strncpy(item.word, content, sizeof(item.word)); + } else if (string_equals(elem, "origin")) { + strncpy(item.origin, content, sizeof(item.origin)); + } else if (string_equals(elem, "pos")) { + strncpy(item.pos, content, sizeof(item.pos)); + } else if (string_equals(elem, "link")) { + strncpy(item.link, content, sizeof(item.link)); + + if (strlen(item.origin) > 0) { + snprintf( + item.entry, + sizeof(item.entry), + "[**%s (%s) 「%s」**](%s)\n\n", + item.word, + item.origin, + item.pos, + item.link + ); + } else { + snprintf( + item.entry, + sizeof(item.entry), + "[**%s 「%s」**](%s)\n\n", + item.word, + item.pos, + item.link + ); + } + + strcat(buffer, item.entry); + } else if (string_equals(elem, "sense_order")) { + order = atoi(content); + + if (order >= KRDICT_MAX_ORDER_COUNT) break; + + len = strlen(buffer); + + snprintf( + buffer + len, + sizeof(buffer) - len, + "**%s. ", + content + ); + } else if (string_equals(elem, "trans_word")) { + if (order >= KRDICT_MAX_ORDER_COUNT) break; + + len = strlen(buffer); + + snprintf( + buffer + len, + sizeof(buffer) - len, + "%s**\n- ", + content + ); + } else if (string_equals(elem, "trans_dfn")) { + if (order >= KRDICT_MAX_ORDER_COUNT) break; + + len = strlen(buffer); + + snprintf( + buffer + len, + sizeof(buffer) - len, + "%s\n\n", + content + ); + } + + elem = parser->elem; + + break; + } + + if (total <= 0) break; + } + + struct discord_embed embeds[] = { + { + .title = "Results", + .description = "No results found.", + .timestamp = discord_timestamp(context->client), + .footer = &(struct discord_embed_footer) { + .text = "🗒️" + } + } + }; + + if (total > 0) embeds[0].description = buffer; + + struct discord_component buttons[] = { + { + .type = DISCORD_COMPONENT_BUTTON, + .style = DISCORD_BUTTON_SECONDARY, + .label = "🔖 Bookmark", + .custom_id = "krd_btn_uwu" + } + }; + + struct discord_component action_rows[] = { + { + .type = DISCORD_COMPONENT_ACTION_ROW, + .components = &(struct discord_components){ + .size = sizeof(buttons) / sizeof(*buttons), + .array = buttons + } + }, + }; + + discord_edit_original_interaction_response( + context->client, + APPLICATION_ID, + context->event.token, + &(struct discord_edit_original_interaction_response) { + .components = &(struct discord_components){ + .size = (total > 0) + ? sizeof(action_rows) / sizeof(*action_rows) + : 0, + .array = action_rows + }, + .embeds = &(struct discord_embeds) { + .size = sizeof(embeds) / sizeof(*embeds), + .array = embeds + } + }, + NULL + ); + + free(context->event.token); + free(context); + + free(parser); +} + +/* 응답 결과로 받은 오류 메시지를 처리한다. */ +static void handle_error(struct krdict_context *context, const char *code) { + if (context == NULL || code == NULL) return; + + log_warn( + "[SAEROM] An error (%s) has occured while processing the request", + code + ); + + struct discord_embed embeds[] = { + { + .title = "Results", + .description = "An unknown error has occured while processing " + "your request.", + .timestamp = discord_timestamp(context->client), + .footer = &(struct discord_embed_footer) { + .text = "🗒️" + } + } + }; + + discord_edit_original_interaction_response( + context->client, + APPLICATION_ID, + context->event.token, + &(struct discord_edit_original_interaction_response) { + .embeds = &(struct discord_embeds) { + .size = sizeof(embeds) / sizeof(*embeds), + .array = embeds + } + }, + NULL + ); + + free(context->event.token); + free(context); +} \ No newline at end of file diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..6652537 --- /dev/null +++ b/src/main.c @@ -0,0 +1,28 @@ +/* + Copyright (c) 2022 jdeokkim + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "saerom.h" + +int main(int argc, char *argv[]) { + init_bot(argc, argv); + + run_bot(); + + deinit_bot(); + + return 0; +} \ No newline at end of file diff --git a/src/papago.c b/src/papago.c new file mode 100644 index 0000000..b3f7792 --- /dev/null +++ b/src/papago.c @@ -0,0 +1,337 @@ +/* + Copyright (c) 2022 jdeokkim + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include + +#include +#include + +#include "saerom.h" + +#define PAPAGO_REQUEST_URL "https://openapi.naver.com/v1/papago/n2mt" + +/* | `papago` 모듈 자료형 정의... | */ + +/* `papago` 모듈 명령어 실행 정보를 나타내는 구조체. */ +struct papago_context { + struct discord *client; + struct { + u64snowflake id; + char *token; + } event; + char *text; +}; + +/* | `papago` 모듈 상수 및 변수... | */ + +/* `papago` 모듈 명령어의 원본 언어 및 목적 언어 목록.*/ +static struct discord_application_command_option_choice languages[] = { + { .name = "Chinese (Simplified)", .value = "\"zh-CN\"" }, + { .name = "Chinese (Traditional)", .value = "\"zh-TW\"" }, + { .name = "English", .value = "\"en\"" }, + { .name = "French", .value = "\"fr\"" }, + { .name = "German", .value = "\"de\"" }, + { .name = "Indonesian", .value = "\"id\"" }, + { .name = "Italian", .value = "\"it\"" }, + { .name = "Japanese", .value = "\"ja\"" }, + { .name = "Korean", .value = "\"ko\"" }, + { .name = "Russian", .value = "\"ru\"" }, + { .name = "Spanish", .value = "\"es\"" }, + { .name = "Thai", .value = "\"th\"" }, + { .name = "Vietnamese", .value = "\"vi\"" } +}; + +/* `papago` 모듈 명령어의 매개 변수. */ +static struct discord_application_command_option options[] = { + { + .type = DISCORD_APPLICATION_OPTION_STRING, + .name = "text", + .description = "The text to be translated", + .required = true + }, + { + .type = DISCORD_APPLICATION_OPTION_STRING, + .name = "source", + .description = "The language to translate the text from", + .choices = &(struct discord_application_command_option_choices) { + .size = sizeof(languages) / sizeof(*languages), + .array = languages + } + }, + { + .type = DISCORD_APPLICATION_OPTION_STRING, + .name = "target", + .description = "The language to translate the text to", + .choices = &(struct discord_application_command_option_choices) { + .size = sizeof(languages) / sizeof(*languages), + .array = languages + } + } +}; + +/* `papago` 모듈 명령어에 대한 정보. */ +static struct discord_create_global_application_command params = { + .name = "ppg", + .description = "Translate the given text between two languages using NAVER™ Papago NMT API", + .default_permission = true, + .options = &(struct discord_application_command_options) { + .size = sizeof(options) / sizeof(*options), + .array = options + } +}; + +/* | `papago` 모듈 함수... | */ + +/* 요청 URL에서 응답을 받았을 때 호출되는 함수. */ +static void on_response(CURLV_STR res, void *user_data); + +/* 응답 결과로 받은 오류 메시지를 처리한다. */ +static void handle_error(struct papago_context *context, const char *code); + +/* `papago` 모듈 명령어를 생성한다. */ +void create_papago_command(struct discord *client) { + discord_create_global_application_command( + client, + APPLICATION_ID, + ¶ms, + NULL + ); +} + +/* `papago` 모듈 명령어에 할당된 메모리를 해제한다. */ +void release_papago_command(struct discord *client) { + /* no-op */ +} + +/* `papago` 모듈 명령어를 실행한다. */ +void run_papago_command( + struct discord *client, + const struct discord_interaction *event +) { + if (event->type == DISCORD_INTERACTION_MESSAGE_COMPONENT) return; + + char *text = "", *source = "ko", *target = "en"; + + for (int i = 0; i < event->data->options->size; i++) { + char *name = event->data->options->array[i].name; + char *value = event->data->options->array[i].value; + + if (string_equals(name, "text")) text = value; + else if (string_equals(name, "source")) source = value; + else if (string_equals(name, "target")) target = value; + } + + CURLV_REQ request = { .callback = on_response }; + + request.easy = curl_easy_init(); + + char buffer[DISCORD_MAX_MESSAGE_LEN] = ""; + + snprintf( + buffer, + DISCORD_MAX_MESSAGE_LEN, + "source=%s&target=%s&text=%s", + source, + target, + text + ); + + curl_easy_setopt(request.easy, CURLOPT_URL, PAPAGO_REQUEST_URL); + curl_easy_setopt(request.easy, CURLOPT_POSTFIELDSIZE, strlen(buffer)); + curl_easy_setopt(request.easy, CURLOPT_COPYPOSTFIELDS, buffer); + curl_easy_setopt(request.easy, CURLOPT_POST, 1); + + struct sr_config *config = get_sr_config(); + + request.header = curl_slist_append( + request.header, + "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" + ); + + snprintf( + buffer, + DISCORD_MAX_MESSAGE_LEN, + "X-Naver-Client-Id: %s", + config->papago.client_id + ); + + request.header = curl_slist_append(request.header, buffer); + + snprintf( + buffer, + DISCORD_MAX_MESSAGE_LEN, + "X-Naver-Client-Secret: %s", + config->papago.client_secret + ); + + request.header = curl_slist_append(request.header, buffer); + + struct papago_context *context = malloc(sizeof(struct papago_context)); + + context->client = client; + context->event.id = event->id; + context->event.token = malloc(strlen(event->token) + 1); + context->text = malloc(strlen(text) + 1); + + strcpy(context->event.token, event->token); + strcpy(context->text, text); + + request.user_data = context; + + curlv_create_request(get_curlv(), &request); + + discord_create_interaction_response( + context->client, + context->event.id, + context->event.token, + &(struct discord_interaction_response) { + .type = DISCORD_INTERACTION_DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE + }, + NULL + ); +} + +/* 요청 URL에서 응답을 받았을 때 호출되는 함수. */ +static void on_response(CURLV_STR res, void *user_data) { + if (res.str == NULL || user_data == NULL) return; + + log_info("[SAEROM] Received %ld bytes from \"%s\"", res.len, PAPAGO_REQUEST_URL); + + JsonNode *root = json_decode(res.str); + + JsonNode *node = json_find_member(root, "errorCode"); + + struct papago_context *context = (struct papago_context *) user_data; + + if (node != NULL) { + handle_error(context, node->string_); + + json_delete(root); + + return; + } + + node = json_find_member(root, "message"); + node = json_find_member(node, "result"); + + struct discord_embed_field fields[2] = { + [0] = { .value = context->text } + }; + + char source_field_name[MAX_STRING_SIZE] = ""; + char target_field_name[MAX_STRING_SIZE] = ""; + + JsonNode *i = NULL; + + json_foreach(i, node) { + if (string_equals(i->key, "srcLangType")) { + snprintf(source_field_name, MAX_STRING_SIZE, "Source (%s)", i->string_); + + fields[0].name = source_field_name; + } else if (string_equals(i->key, "tarLangType")) { + snprintf(target_field_name, MAX_STRING_SIZE, "Target (%s)", i->string_); + + fields[1].name = target_field_name; + } else if (string_equals(i->key, "translatedText")) { + fields[1].value = i->string_; + } + } + + struct discord_embed embeds[] = { + { + .title = "Translation", + .timestamp = discord_timestamp(context->client), + .footer = &(struct discord_embed_footer) { + .text = "🌏" + }, + .fields = &(struct discord_embed_fields) { + .size = sizeof(fields) / sizeof(*fields), + .array = fields + } + } + }; + + discord_edit_original_interaction_response( + context->client, + APPLICATION_ID, + context->event.token, + &(struct discord_edit_original_interaction_response) { + .embeds = &(struct discord_embeds) { + .size = sizeof(embeds) / sizeof(*embeds), + .array = embeds + } + }, + NULL + ); + + free(context->event.token); + free(context->text); + free(context); + + json_delete(root); +} + +/* 응답 결과로 받은 오류 메시지를 처리한다. */ +static void handle_error(struct papago_context *context, const char *code) { + if (context == NULL || code == NULL) return; + + log_warn( + "[SAEROM] An error (%s) has occured while processing the request", + code + ); + + struct discord_embed embeds[] = { + { + .title = "Translation", + .timestamp = discord_timestamp(context->client), + .footer = &(struct discord_embed_footer) { + .text = "🌏" + } + } + }; + + if (string_equals(code, "024")) + embeds[0].description = "Invalid client id or client secret given, " + "please check your configuration file."; + else if (string_equals(code, "N2MT05")) + embeds[0].description = "Target language must not be the same as the " + "source language."; + else if (string_equals(code, "N2MT08")) + embeds[0].description = "The length of the `text` parameter is too long."; + else + embeds[0].description = "An unknown error has occured while processing " + "your request."; + + discord_edit_original_interaction_response( + context->client, + APPLICATION_ID, + context->event.token, + &(struct discord_edit_original_interaction_response) { + .embeds = &(struct discord_embeds) { + .size = sizeof(embeds) / sizeof(*embeds), + .array = embeds + } + }, + NULL + ); + + free(context->event.token); + free(context->text); + + free(context); +} \ No newline at end of file diff --git a/src/utils.c b/src/utils.c new file mode 100644 index 0000000..96fa982 --- /dev/null +++ b/src/utils.c @@ -0,0 +1,92 @@ +/* + Copyright (c) 2022 jdeokkim + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include + +#include + +#include "saerom.h" + +/* | `utils` 모듈 매크로 정의... | */ + +#define DISCORD_MAGIC_NUMBER 5 + +/* | `utils` 모듈 함수... | */ + +/* 주어진 사용자의 프로필 사진 URL을 반환한다. */ +const char *get_avatar_url(const struct discord_user *user) { + static char buffer[MAX_STRING_SIZE]; + + if (user->avatar != NULL) { + snprintf( + buffer, + MAX_STRING_SIZE, + "https://cdn.discordapp.com/avatars/%"SCNu64"/%s.webp", + user->id, + user->avatar + ); + } else { + snprintf( + buffer, + MAX_STRING_SIZE, + "https://cdn.discordapp.com/embed/avatars/%"SCNu64".png", + strtoul(user->discriminator, NULL, 10) % DISCORD_MAGIC_NUMBER + ); + } + + return buffer; +} + +/* 주어진 경로에 위치한 파일의 내용을 반환한다. */ +char *get_file_contents(const char *path) { + FILE *fp = fopen(path, "rb"); + + if (fp == NULL) { + log_warn("[SAEROM] Unable to open file \"%s\"", path); + + return NULL; + } + + char *buffer = malloc(MAX_FILE_SIZE); + + if (buffer == NULL) { + log_warn("[SAEROM] Unable to allocate memory for file \"%s\"", path); + + return NULL; + } + + size_t file_size = fread(buffer, sizeof(*buffer), MAX_FILE_SIZE, fp); + + char *new_buffer = realloc(buffer, file_size); + + if (new_buffer != NULL) { + new_buffer[file_size] = 0; + + buffer = new_buffer; + } + + fclose(fp); + + return buffer; +} + +/* 두 문자열의 내용이 서로 같은지 확인한다. */ +bool string_equals(const char *s1, const char *s2) { + return strncmp(s1, s2, MAX_STRING_SIZE) == 0; +} \ No newline at end of file diff --git a/src/yxml.c b/src/yxml.c new file mode 100644 index 0000000..23d04a9 --- /dev/null +++ b/src/yxml.c @@ -0,0 +1,1060 @@ +/* This file is generated by yxml-gen.pl using yxml-states and yxml.c.in as input files. + * It is preferable to edit those files instead of this one if you want to make a change. + * The source files can be found through the homepage: https://dev.yorhel.nl/yxml */ + +/* Copyright (c) 2013-2014 Yoran Heling + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include +#include + +typedef enum { + YXMLS_string, + YXMLS_attr0, + YXMLS_attr1, + YXMLS_attr2, + YXMLS_attr3, + YXMLS_attr4, + YXMLS_cd0, + YXMLS_cd1, + YXMLS_cd2, + YXMLS_comment0, + YXMLS_comment1, + YXMLS_comment2, + YXMLS_comment3, + YXMLS_comment4, + YXMLS_dt0, + YXMLS_dt1, + YXMLS_dt2, + YXMLS_dt3, + YXMLS_dt4, + YXMLS_elem0, + YXMLS_elem1, + YXMLS_elem2, + YXMLS_elem3, + YXMLS_enc0, + YXMLS_enc1, + YXMLS_enc2, + YXMLS_enc3, + YXMLS_etag0, + YXMLS_etag1, + YXMLS_etag2, + YXMLS_init, + YXMLS_le0, + YXMLS_le1, + YXMLS_le2, + YXMLS_le3, + YXMLS_lee1, + YXMLS_lee2, + YXMLS_leq0, + YXMLS_misc0, + YXMLS_misc1, + YXMLS_misc2, + YXMLS_misc2a, + YXMLS_misc3, + YXMLS_pi0, + YXMLS_pi1, + YXMLS_pi2, + YXMLS_pi3, + YXMLS_pi4, + YXMLS_std0, + YXMLS_std1, + YXMLS_std2, + YXMLS_std3, + YXMLS_ver0, + YXMLS_ver1, + YXMLS_ver2, + YXMLS_ver3, + YXMLS_xmldecl0, + YXMLS_xmldecl1, + YXMLS_xmldecl2, + YXMLS_xmldecl3, + YXMLS_xmldecl4, + YXMLS_xmldecl5, + YXMLS_xmldecl6, + YXMLS_xmldecl7, + YXMLS_xmldecl8, + YXMLS_xmldecl9 +} yxml_state_t; + + +#define yxml_isChar(c) 1 +/* 0xd should be part of SP, too, but yxml_parse() already normalizes that into 0xa */ +#define yxml_isSP(c) (c == 0x20 || c == 0x09 || c == 0x0a) +#define yxml_isAlpha(c) ((c|32)-'a' < 26) +#define yxml_isNum(c) (c-'0' < 10) +#define yxml_isHex(c) (yxml_isNum(c) || (c|32)-'a' < 6) +#define yxml_isEncName(c) (yxml_isAlpha(c) || yxml_isNum(c) || c == '.' || c == '_' || c == '-') +#define yxml_isNameStart(c) (yxml_isAlpha(c) || c == ':' || c == '_' || c >= 128) +#define yxml_isName(c) (yxml_isNameStart(c) || yxml_isNum(c) || c == '-' || c == '.') +/* XXX: The valid characters are dependent on the quote char, hence the access to x->quote */ +#define yxml_isAttValue(c) (yxml_isChar(c) && c != x->quote && c != '<' && c != '&') +/* Anything between '&' and ';', the yxml_ref* functions will do further + * validation. Strictly speaking, this is "yxml_isName(c) || c == '#'", but + * this parser doesn't understand entities with '.', ':', etc, anwyay. */ +#define yxml_isRef(c) (yxml_isNum(c) || yxml_isAlpha(c) || c == '#') + +#define INTFROM5CHARS(a, b, c, d, e) ((((uint64_t)(a))<<32) | (((uint64_t)(b))<<24) | (((uint64_t)(c))<<16) | (((uint64_t)(d))<<8) | (uint64_t)(e)) + + +/* Set the given char value to ch (0<=ch<=255). */ +static inline void yxml_setchar(char *dest, unsigned ch) { + *(unsigned char *)dest = ch; +} + + +/* Similar to yxml_setchar(), but will convert ch (any valid unicode point) to + * UTF-8 and appends a '\0'. dest must have room for at least 5 bytes. */ +static void yxml_setutf8(char *dest, unsigned ch) { + if(ch <= 0x007F) + yxml_setchar(dest++, ch); + else if(ch <= 0x07FF) { + yxml_setchar(dest++, 0xC0 | (ch>>6)); + yxml_setchar(dest++, 0x80 | (ch & 0x3F)); + } else if(ch <= 0xFFFF) { + yxml_setchar(dest++, 0xE0 | (ch>>12)); + yxml_setchar(dest++, 0x80 | ((ch>>6) & 0x3F)); + yxml_setchar(dest++, 0x80 | (ch & 0x3F)); + } else { + yxml_setchar(dest++, 0xF0 | (ch>>18)); + yxml_setchar(dest++, 0x80 | ((ch>>12) & 0x3F)); + yxml_setchar(dest++, 0x80 | ((ch>>6) & 0x3F)); + yxml_setchar(dest++, 0x80 | (ch & 0x3F)); + } + *dest = 0; +} + + +static inline yxml_ret_t yxml_datacontent(yxml_t *x, unsigned ch) { + yxml_setchar(x->data, ch); + x->data[1] = 0; + return YXML_CONTENT; +} + + +static inline yxml_ret_t yxml_datapi1(yxml_t *x, unsigned ch) { + yxml_setchar(x->data, ch); + x->data[1] = 0; + return YXML_PICONTENT; +} + + +static inline yxml_ret_t yxml_datapi2(yxml_t *x, unsigned ch) { + x->data[0] = '?'; + yxml_setchar(x->data+1, ch); + x->data[2] = 0; + return YXML_PICONTENT; +} + + +static inline yxml_ret_t yxml_datacd1(yxml_t *x, unsigned ch) { + x->data[0] = ']'; + yxml_setchar(x->data+1, ch); + x->data[2] = 0; + return YXML_CONTENT; +} + + +static inline yxml_ret_t yxml_datacd2(yxml_t *x, unsigned ch) { + x->data[0] = ']'; + x->data[1] = ']'; + yxml_setchar(x->data+2, ch); + x->data[3] = 0; + return YXML_CONTENT; +} + + +static inline yxml_ret_t yxml_dataattr(yxml_t *x, unsigned ch) { + /* Normalize attribute values according to the XML spec section 3.3.3. */ + yxml_setchar(x->data, ch == 0x9 || ch == 0xa ? 0x20 : ch); + x->data[1] = 0; + return YXML_ATTRVAL; +} + + +static yxml_ret_t yxml_pushstack(yxml_t *x, char **res, unsigned ch) { + if(x->stacklen+2 >= x->stacksize) + return YXML_ESTACK; + x->stacklen++; + *res = (char *)x->stack+x->stacklen; + x->stack[x->stacklen] = ch; + x->stacklen++; + x->stack[x->stacklen] = 0; + return YXML_OK; +} + + +static yxml_ret_t yxml_pushstackc(yxml_t *x, unsigned ch) { + if(x->stacklen+1 >= x->stacksize) + return YXML_ESTACK; + x->stack[x->stacklen] = ch; + x->stacklen++; + x->stack[x->stacklen] = 0; + return YXML_OK; +} + + +static void yxml_popstack(yxml_t *x) { + do + x->stacklen--; + while(x->stack[x->stacklen]); +} + + +static inline yxml_ret_t yxml_elemstart (yxml_t *x, unsigned ch) { return yxml_pushstack(x, &x->elem, ch); } +static inline yxml_ret_t yxml_elemname (yxml_t *x, unsigned ch) { return yxml_pushstackc(x, ch); } +static inline yxml_ret_t yxml_elemnameend(yxml_t *x, unsigned ch) { return YXML_ELEMSTART; } + + +/* Also used in yxml_elemcloseend(), since this function just removes the last + * element from the stack and returns ELEMEND. */ +static yxml_ret_t yxml_selfclose(yxml_t *x, unsigned ch) { + yxml_popstack(x); + if(x->stacklen) { + x->elem = (char *)x->stack+x->stacklen-1; + while(*(x->elem-1)) + x->elem--; + return YXML_ELEMEND; + } + x->elem = (char *)x->stack; + x->state = YXMLS_misc3; + return YXML_ELEMEND; +} + + +static inline yxml_ret_t yxml_elemclose(yxml_t *x, unsigned ch) { + if(*((unsigned char *)x->elem) != ch) + return YXML_ECLOSE; + x->elem++; + return YXML_OK; +} + + +static inline yxml_ret_t yxml_elemcloseend(yxml_t *x, unsigned ch) { + if(*x->elem) + return YXML_ECLOSE; + return yxml_selfclose(x, ch); +} + + +static inline yxml_ret_t yxml_attrstart (yxml_t *x, unsigned ch) { return yxml_pushstack(x, &x->attr, ch); } +static inline yxml_ret_t yxml_attrname (yxml_t *x, unsigned ch) { return yxml_pushstackc(x, ch); } +static inline yxml_ret_t yxml_attrnameend(yxml_t *x, unsigned ch) { return YXML_ATTRSTART; } +static inline yxml_ret_t yxml_attrvalend (yxml_t *x, unsigned ch) { yxml_popstack(x); return YXML_ATTREND; } + + +static inline yxml_ret_t yxml_pistart (yxml_t *x, unsigned ch) { return yxml_pushstack(x, &x->pi, ch); } +static inline yxml_ret_t yxml_piname (yxml_t *x, unsigned ch) { return yxml_pushstackc(x, ch); } +static inline yxml_ret_t yxml_piabort (yxml_t *x, unsigned ch) { yxml_popstack(x); return YXML_OK; } +static inline yxml_ret_t yxml_pinameend(yxml_t *x, unsigned ch) { + return (x->pi[0]|32) == 'x' && (x->pi[1]|32) == 'm' && (x->pi[2]|32) == 'l' && !x->pi[3] ? YXML_ESYN : YXML_PISTART; +} +static inline yxml_ret_t yxml_pivalend (yxml_t *x, unsigned ch) { yxml_popstack(x); x->pi = (char *)x->stack; return YXML_PIEND; } + + +static inline yxml_ret_t yxml_refstart(yxml_t *x, unsigned ch) { + memset(x->data, 0, sizeof(x->data)); + x->reflen = 0; + return YXML_OK; +} + + +static yxml_ret_t yxml_ref(yxml_t *x, unsigned ch) { + if(x->reflen >= sizeof(x->data)-1) + return YXML_EREF; + yxml_setchar(x->data+x->reflen, ch); + x->reflen++; + return YXML_OK; +} + + +static yxml_ret_t yxml_refend(yxml_t *x, yxml_ret_t ret) { + unsigned char *r = (unsigned char *)x->data; + unsigned ch = 0; + if(*r == '#') { + if(r[1] == 'x') + for(r += 2; yxml_isHex((unsigned)*r); r++) + ch = (ch<<4) + (*r <= '9' ? *r-'0' : (*r|32)-'a' + 10); + else + for(r++; yxml_isNum((unsigned)*r); r++) + ch = (ch*10) + (*r-'0'); + if(*r) + ch = 0; + } else { + uint64_t i = INTFROM5CHARS(r[0], r[1], r[2], r[3], r[4]); + ch = + i == INTFROM5CHARS('l','t', 0, 0, 0) ? '<' : + i == INTFROM5CHARS('g','t', 0, 0, 0) ? '>' : + i == INTFROM5CHARS('a','m','p', 0, 0) ? '&' : + i == INTFROM5CHARS('a','p','o','s',0) ? '\'': + i == INTFROM5CHARS('q','u','o','t',0) ? '"' : 0; + } + + /* Codepoints not allowed in the XML 1.1 definition of a Char */ + if(!ch || ch > 0x10FFFF || ch == 0xFFFE || ch == 0xFFFF || (ch-0xDFFF) < 0x7FF) + return YXML_EREF; + yxml_setutf8(x->data, ch); + return ret; +} + + +static inline yxml_ret_t yxml_refcontent(yxml_t *x, unsigned ch) { return yxml_refend(x, YXML_CONTENT); } +static inline yxml_ret_t yxml_refattrval(yxml_t *x, unsigned ch) { return yxml_refend(x, YXML_ATTRVAL); } + + +void yxml_init(yxml_t *x, void *stack, size_t stacksize) { + memset(x, 0, sizeof(*x)); + x->line = 1; + x->stack = (unsigned char*)stack; + x->stacksize = stacksize; + *x->stack = 0; + x->elem = x->pi = x->attr = (char *)x->stack; + x->state = YXMLS_init; +} + + +yxml_ret_t yxml_parse(yxml_t *x, int _ch) { + /* Ensure that characters are in the range of 0..255 rather than -126..125. + * All character comparisons are done with positive integers. */ + unsigned ch = (unsigned)(_ch+256) & 0xff; + if(!ch) + return YXML_ESYN; + x->total++; + + /* End-of-Line normalization, "\rX", "\r\n" and "\n" are recognized and + * normalized to a single '\n' as per XML 1.0 section 2.11. XML 1.1 adds + * some non-ASCII character sequences to this list, but we can only handle + * ASCII here without making assumptions about the input encoding. */ + if(x->ignore == ch) { + x->ignore = 0; + return YXML_OK; + } + x->ignore = (ch == 0xd) * 0xa; + if(ch == 0xa || ch == 0xd) { + ch = 0xa; + x->line++; + x->byte = 0; + } + x->byte++; + + switch((yxml_state_t)x->state) { + case YXMLS_string: + if(ch == *x->string) { + x->string++; + if(!*x->string) + x->state = x->nextstate; + return YXML_OK; + } + break; + case YXMLS_attr0: + if(yxml_isName(ch)) + return yxml_attrname(x, ch); + if(yxml_isSP(ch)) { + x->state = YXMLS_attr1; + return yxml_attrnameend(x, ch); + } + if(ch == (unsigned char)'=') { + x->state = YXMLS_attr2; + return yxml_attrnameend(x, ch); + } + break; + case YXMLS_attr1: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'=') { + x->state = YXMLS_attr2; + return YXML_OK; + } + break; + case YXMLS_attr2: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'\'' || ch == (unsigned char)'"') { + x->state = YXMLS_attr3; + x->quote = ch; + return YXML_OK; + } + break; + case YXMLS_attr3: + if(yxml_isAttValue(ch)) + return yxml_dataattr(x, ch); + if(ch == (unsigned char)'&') { + x->state = YXMLS_attr4; + return yxml_refstart(x, ch); + } + if(x->quote == ch) { + x->state = YXMLS_elem2; + return yxml_attrvalend(x, ch); + } + break; + case YXMLS_attr4: + if(yxml_isRef(ch)) + return yxml_ref(x, ch); + if(ch == (unsigned char)'\x3b') { + x->state = YXMLS_attr3; + return yxml_refattrval(x, ch); + } + break; + case YXMLS_cd0: + if(ch == (unsigned char)']') { + x->state = YXMLS_cd1; + return YXML_OK; + } + if(yxml_isChar(ch)) + return yxml_datacontent(x, ch); + break; + case YXMLS_cd1: + if(ch == (unsigned char)']') { + x->state = YXMLS_cd2; + return YXML_OK; + } + if(yxml_isChar(ch)) { + x->state = YXMLS_cd0; + return yxml_datacd1(x, ch); + } + break; + case YXMLS_cd2: + if(ch == (unsigned char)']') + return yxml_datacontent(x, ch); + if(ch == (unsigned char)'>') { + x->state = YXMLS_misc2; + return YXML_OK; + } + if(yxml_isChar(ch)) { + x->state = YXMLS_cd0; + return yxml_datacd2(x, ch); + } + break; + case YXMLS_comment0: + if(ch == (unsigned char)'-') { + x->state = YXMLS_comment1; + return YXML_OK; + } + break; + case YXMLS_comment1: + if(ch == (unsigned char)'-') { + x->state = YXMLS_comment2; + return YXML_OK; + } + break; + case YXMLS_comment2: + if(ch == (unsigned char)'-') { + x->state = YXMLS_comment3; + return YXML_OK; + } + if(yxml_isChar(ch)) + return YXML_OK; + break; + case YXMLS_comment3: + if(ch == (unsigned char)'-') { + x->state = YXMLS_comment4; + return YXML_OK; + } + if(yxml_isChar(ch)) { + x->state = YXMLS_comment2; + return YXML_OK; + } + break; + case YXMLS_comment4: + if(ch == (unsigned char)'>') { + x->state = x->nextstate; + return YXML_OK; + } + break; + case YXMLS_dt0: + if(ch == (unsigned char)'>') { + x->state = YXMLS_misc1; + return YXML_OK; + } + if(ch == (unsigned char)'\'' || ch == (unsigned char)'"') { + x->state = YXMLS_dt1; + x->quote = ch; + x->nextstate = YXMLS_dt0; + return YXML_OK; + } + if(ch == (unsigned char)'<') { + x->state = YXMLS_dt2; + return YXML_OK; + } + if(yxml_isChar(ch)) + return YXML_OK; + break; + case YXMLS_dt1: + if(x->quote == ch) { + x->state = x->nextstate; + return YXML_OK; + } + if(yxml_isChar(ch)) + return YXML_OK; + break; + case YXMLS_dt2: + if(ch == (unsigned char)'?') { + x->state = YXMLS_pi0; + x->nextstate = YXMLS_dt0; + return YXML_OK; + } + if(ch == (unsigned char)'!') { + x->state = YXMLS_dt3; + return YXML_OK; + } + break; + case YXMLS_dt3: + if(ch == (unsigned char)'-') { + x->state = YXMLS_comment1; + x->nextstate = YXMLS_dt0; + return YXML_OK; + } + if(yxml_isChar(ch)) { + x->state = YXMLS_dt4; + return YXML_OK; + } + break; + case YXMLS_dt4: + if(ch == (unsigned char)'\'' || ch == (unsigned char)'"') { + x->state = YXMLS_dt1; + x->quote = ch; + x->nextstate = YXMLS_dt4; + return YXML_OK; + } + if(ch == (unsigned char)'>') { + x->state = YXMLS_dt0; + return YXML_OK; + } + if(yxml_isChar(ch)) + return YXML_OK; + break; + case YXMLS_elem0: + if(yxml_isName(ch)) + return yxml_elemname(x, ch); + if(yxml_isSP(ch)) { + x->state = YXMLS_elem1; + return yxml_elemnameend(x, ch); + } + if(ch == (unsigned char)'/') { + x->state = YXMLS_elem3; + return yxml_elemnameend(x, ch); + } + if(ch == (unsigned char)'>') { + x->state = YXMLS_misc2; + return yxml_elemnameend(x, ch); + } + break; + case YXMLS_elem1: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'/') { + x->state = YXMLS_elem3; + return YXML_OK; + } + if(ch == (unsigned char)'>') { + x->state = YXMLS_misc2; + return YXML_OK; + } + if(yxml_isNameStart(ch)) { + x->state = YXMLS_attr0; + return yxml_attrstart(x, ch); + } + break; + case YXMLS_elem2: + if(yxml_isSP(ch)) { + x->state = YXMLS_elem1; + return YXML_OK; + } + if(ch == (unsigned char)'/') { + x->state = YXMLS_elem3; + return YXML_OK; + } + if(ch == (unsigned char)'>') { + x->state = YXMLS_misc2; + return YXML_OK; + } + break; + case YXMLS_elem3: + if(ch == (unsigned char)'>') { + x->state = YXMLS_misc2; + return yxml_selfclose(x, ch); + } + break; + case YXMLS_enc0: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'=') { + x->state = YXMLS_enc1; + return YXML_OK; + } + break; + case YXMLS_enc1: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'\'' || ch == (unsigned char)'"') { + x->state = YXMLS_enc2; + x->quote = ch; + return YXML_OK; + } + break; + case YXMLS_enc2: + if(yxml_isAlpha(ch)) { + x->state = YXMLS_enc3; + return YXML_OK; + } + break; + case YXMLS_enc3: + if(yxml_isEncName(ch)) + return YXML_OK; + if(x->quote == ch) { + x->state = YXMLS_xmldecl6; + return YXML_OK; + } + break; + case YXMLS_etag0: + if(yxml_isNameStart(ch)) { + x->state = YXMLS_etag1; + return yxml_elemclose(x, ch); + } + break; + case YXMLS_etag1: + if(yxml_isName(ch)) + return yxml_elemclose(x, ch); + if(yxml_isSP(ch)) { + x->state = YXMLS_etag2; + return yxml_elemcloseend(x, ch); + } + if(ch == (unsigned char)'>') { + x->state = YXMLS_misc2; + return yxml_elemcloseend(x, ch); + } + break; + case YXMLS_etag2: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'>') { + x->state = YXMLS_misc2; + return YXML_OK; + } + break; + case YXMLS_init: + if(ch == (unsigned char)'\xef') { + x->state = YXMLS_string; + x->nextstate = YXMLS_misc0; + x->string = (unsigned char *)"\xbb\xbf"; + return YXML_OK; + } + if(yxml_isSP(ch)) { + x->state = YXMLS_misc0; + return YXML_OK; + } + if(ch == (unsigned char)'<') { + x->state = YXMLS_le0; + return YXML_OK; + } + break; + case YXMLS_le0: + if(ch == (unsigned char)'!') { + x->state = YXMLS_lee1; + return YXML_OK; + } + if(ch == (unsigned char)'?') { + x->state = YXMLS_leq0; + return YXML_OK; + } + if(yxml_isNameStart(ch)) { + x->state = YXMLS_elem0; + return yxml_elemstart(x, ch); + } + break; + case YXMLS_le1: + if(ch == (unsigned char)'!') { + x->state = YXMLS_lee1; + return YXML_OK; + } + if(ch == (unsigned char)'?') { + x->state = YXMLS_pi0; + x->nextstate = YXMLS_misc1; + return YXML_OK; + } + if(yxml_isNameStart(ch)) { + x->state = YXMLS_elem0; + return yxml_elemstart(x, ch); + } + break; + case YXMLS_le2: + if(ch == (unsigned char)'!') { + x->state = YXMLS_lee2; + return YXML_OK; + } + if(ch == (unsigned char)'?') { + x->state = YXMLS_pi0; + x->nextstate = YXMLS_misc2; + return YXML_OK; + } + if(ch == (unsigned char)'/') { + x->state = YXMLS_etag0; + return YXML_OK; + } + if(yxml_isNameStart(ch)) { + x->state = YXMLS_elem0; + return yxml_elemstart(x, ch); + } + break; + case YXMLS_le3: + if(ch == (unsigned char)'!') { + x->state = YXMLS_comment0; + x->nextstate = YXMLS_misc3; + return YXML_OK; + } + if(ch == (unsigned char)'?') { + x->state = YXMLS_pi0; + x->nextstate = YXMLS_misc3; + return YXML_OK; + } + break; + case YXMLS_lee1: + if(ch == (unsigned char)'-') { + x->state = YXMLS_comment1; + x->nextstate = YXMLS_misc1; + return YXML_OK; + } + if(ch == (unsigned char)'D') { + x->state = YXMLS_string; + x->nextstate = YXMLS_dt0; + x->string = (unsigned char *)"OCTYPE"; + return YXML_OK; + } + break; + case YXMLS_lee2: + if(ch == (unsigned char)'-') { + x->state = YXMLS_comment1; + x->nextstate = YXMLS_misc2; + return YXML_OK; + } + if(ch == (unsigned char)'[') { + x->state = YXMLS_string; + x->nextstate = YXMLS_cd0; + x->string = (unsigned char *)"CDATA["; + return YXML_OK; + } + break; + case YXMLS_leq0: + if(ch == (unsigned char)'x') { + x->state = YXMLS_xmldecl0; + x->nextstate = YXMLS_misc1; + return yxml_pistart(x, ch); + } + if(yxml_isNameStart(ch)) { + x->state = YXMLS_pi1; + x->nextstate = YXMLS_misc1; + return yxml_pistart(x, ch); + } + break; + case YXMLS_misc0: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'<') { + x->state = YXMLS_le0; + return YXML_OK; + } + break; + case YXMLS_misc1: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'<') { + x->state = YXMLS_le1; + return YXML_OK; + } + break; + case YXMLS_misc2: + if(ch == (unsigned char)'<') { + x->state = YXMLS_le2; + return YXML_OK; + } + if(ch == (unsigned char)'&') { + x->state = YXMLS_misc2a; + return yxml_refstart(x, ch); + } + if(yxml_isChar(ch)) + return yxml_datacontent(x, ch); + break; + case YXMLS_misc2a: + if(yxml_isRef(ch)) + return yxml_ref(x, ch); + if(ch == (unsigned char)'\x3b') { + x->state = YXMLS_misc2; + return yxml_refcontent(x, ch); + } + break; + case YXMLS_misc3: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'<') { + x->state = YXMLS_le3; + return YXML_OK; + } + break; + case YXMLS_pi0: + if(yxml_isNameStart(ch)) { + x->state = YXMLS_pi1; + return yxml_pistart(x, ch); + } + break; + case YXMLS_pi1: + if(yxml_isName(ch)) + return yxml_piname(x, ch); + if(ch == (unsigned char)'?') { + x->state = YXMLS_pi4; + return yxml_pinameend(x, ch); + } + if(yxml_isSP(ch)) { + x->state = YXMLS_pi2; + return yxml_pinameend(x, ch); + } + break; + case YXMLS_pi2: + if(ch == (unsigned char)'?') { + x->state = YXMLS_pi3; + return YXML_OK; + } + if(yxml_isChar(ch)) + return yxml_datapi1(x, ch); + break; + case YXMLS_pi3: + if(ch == (unsigned char)'>') { + x->state = x->nextstate; + return yxml_pivalend(x, ch); + } + if(yxml_isChar(ch)) { + x->state = YXMLS_pi2; + return yxml_datapi2(x, ch); + } + break; + case YXMLS_pi4: + if(ch == (unsigned char)'>') { + x->state = x->nextstate; + return yxml_pivalend(x, ch); + } + break; + case YXMLS_std0: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'=') { + x->state = YXMLS_std1; + return YXML_OK; + } + break; + case YXMLS_std1: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'\'' || ch == (unsigned char)'"') { + x->state = YXMLS_std2; + x->quote = ch; + return YXML_OK; + } + break; + case YXMLS_std2: + if(ch == (unsigned char)'y') { + x->state = YXMLS_string; + x->nextstate = YXMLS_std3; + x->string = (unsigned char *)"es"; + return YXML_OK; + } + if(ch == (unsigned char)'n') { + x->state = YXMLS_string; + x->nextstate = YXMLS_std3; + x->string = (unsigned char *)"o"; + return YXML_OK; + } + break; + case YXMLS_std3: + if(x->quote == ch) { + x->state = YXMLS_xmldecl8; + return YXML_OK; + } + break; + case YXMLS_ver0: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'=') { + x->state = YXMLS_ver1; + return YXML_OK; + } + break; + case YXMLS_ver1: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'\'' || ch == (unsigned char)'"') { + x->state = YXMLS_string; + x->quote = ch; + x->nextstate = YXMLS_ver2; + x->string = (unsigned char *)"1."; + return YXML_OK; + } + break; + case YXMLS_ver2: + if(yxml_isNum(ch)) { + x->state = YXMLS_ver3; + return YXML_OK; + } + break; + case YXMLS_ver3: + if(yxml_isNum(ch)) + return YXML_OK; + if(x->quote == ch) { + x->state = YXMLS_xmldecl4; + return YXML_OK; + } + break; + case YXMLS_xmldecl0: + if(ch == (unsigned char)'m') { + x->state = YXMLS_xmldecl1; + return yxml_piname(x, ch); + } + if(yxml_isName(ch)) { + x->state = YXMLS_pi1; + return yxml_piname(x, ch); + } + if(ch == (unsigned char)'?') { + x->state = YXMLS_pi4; + return yxml_pinameend(x, ch); + } + if(yxml_isSP(ch)) { + x->state = YXMLS_pi2; + return yxml_pinameend(x, ch); + } + break; + case YXMLS_xmldecl1: + if(ch == (unsigned char)'l') { + x->state = YXMLS_xmldecl2; + return yxml_piname(x, ch); + } + if(yxml_isName(ch)) { + x->state = YXMLS_pi1; + return yxml_piname(x, ch); + } + if(ch == (unsigned char)'?') { + x->state = YXMLS_pi4; + return yxml_pinameend(x, ch); + } + if(yxml_isSP(ch)) { + x->state = YXMLS_pi2; + return yxml_pinameend(x, ch); + } + break; + case YXMLS_xmldecl2: + if(yxml_isSP(ch)) { + x->state = YXMLS_xmldecl3; + return yxml_piabort(x, ch); + } + if(yxml_isName(ch)) { + x->state = YXMLS_pi1; + return yxml_piname(x, ch); + } + break; + case YXMLS_xmldecl3: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'v') { + x->state = YXMLS_string; + x->nextstate = YXMLS_ver0; + x->string = (unsigned char *)"ersion"; + return YXML_OK; + } + break; + case YXMLS_xmldecl4: + if(yxml_isSP(ch)) { + x->state = YXMLS_xmldecl5; + return YXML_OK; + } + if(ch == (unsigned char)'?') { + x->state = YXMLS_xmldecl9; + return YXML_OK; + } + break; + case YXMLS_xmldecl5: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'?') { + x->state = YXMLS_xmldecl9; + return YXML_OK; + } + if(ch == (unsigned char)'e') { + x->state = YXMLS_string; + x->nextstate = YXMLS_enc0; + x->string = (unsigned char *)"ncoding"; + return YXML_OK; + } + if(ch == (unsigned char)'s') { + x->state = YXMLS_string; + x->nextstate = YXMLS_std0; + x->string = (unsigned char *)"tandalone"; + return YXML_OK; + } + break; + case YXMLS_xmldecl6: + if(yxml_isSP(ch)) { + x->state = YXMLS_xmldecl7; + return YXML_OK; + } + if(ch == (unsigned char)'?') { + x->state = YXMLS_xmldecl9; + return YXML_OK; + } + break; + case YXMLS_xmldecl7: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'?') { + x->state = YXMLS_xmldecl9; + return YXML_OK; + } + if(ch == (unsigned char)'s') { + x->state = YXMLS_string; + x->nextstate = YXMLS_std0; + x->string = (unsigned char *)"tandalone"; + return YXML_OK; + } + break; + case YXMLS_xmldecl8: + if(yxml_isSP(ch)) + return YXML_OK; + if(ch == (unsigned char)'?') { + x->state = YXMLS_xmldecl9; + return YXML_OK; + } + break; + case YXMLS_xmldecl9: + if(ch == (unsigned char)'>') { + x->state = YXMLS_misc1; + return YXML_OK; + } + break; + } + return YXML_ESYN; +} + + +yxml_ret_t yxml_eof(yxml_t *x) { + if(x->state != YXMLS_misc3) + return YXML_EEOF; + return YXML_OK; +} + + +/* vim: set noet sw=4 ts=4: */