├── .github
├── afdian.jpg
└── workflows
│ └── build.yaml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle.kts
├── example
├── 5000choyen.png
├── dear.gif
├── monster.png
├── petpet.gif
├── pornhub.png
├── school.png
├── spell.png
├── tank.png
├── trap.png
└── zzkia.png
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
├── main
├── kotlin
│ ├── Context.kt
│ ├── EmojiKitchen.kt
│ ├── EmojiKitchenCombination.kt
│ ├── EmojiKitchenItem.kt
│ ├── EmojiKitchenMetadata.kt
│ ├── MemeHelper.kt
│ ├── MemeHelperPlugin.kt
│ ├── ProjectSekaiStickers.kt
│ ├── YgoCard.kt
│ ├── face
│ │ ├── AdditionInfo.kt
│ │ ├── AppData.kt
│ │ ├── AuthorDetail.kt
│ │ ├── DiversionConfig.kt
│ │ ├── ItemData.kt
│ │ ├── ItemInfo.kt
│ │ ├── ItemSource.kt
│ │ ├── KeywordsSerializer.kt
│ │ ├── MD5Info.kt
│ │ ├── MarketFaceAndroid.kt
│ │ ├── MarketFaceBaseInfo.kt
│ │ ├── MarketFaceData.kt
│ │ ├── MarketFaceDetail.kt
│ │ ├── MarketFaceHelper.kt
│ │ ├── MarketFaceItem.kt
│ │ ├── NumberToBooleanSerializer.kt
│ │ ├── OperationInfo.kt
│ │ ├── RelationIdInfo.kt
│ │ ├── Restful.kt
│ │ └── SupplierInfo.kt
│ ├── impl
│ │ ├── MemeBiliBiliEmote.kt
│ │ ├── MemeChoYen.kt
│ │ ├── MemeDear.kt
│ │ ├── MemeEmojiKitchen.kt
│ │ ├── MemePetPet.kt
│ │ ├── MemePornHub.kt
│ │ ├── MemeProjectSekaiStickers.kt
│ │ ├── MemeRecord.kt
│ │ ├── MemeSchool.kt
│ │ ├── MemeTank.kt
│ │ ├── MemeWeiboEmoticon.kt
│ │ ├── MemeYgo.kt
│ │ └── MemeZZKIA.kt
│ ├── package.kt
│ └── service
│ │ └── MemeService.kt
└── resources
│ ├── META-INF
│ └── services
│ │ ├── net.mamoe.mirai.console.plugin.jvm.JvmPlugin
│ │ └── xyz.cssxsh.mirai.meme.service.MemeService
│ └── xyz
│ └── cssxsh
│ └── mirai
│ └── meme
│ └── impl
│ └── record.png
└── test
└── resources
└── META-INF
└── services
└── org.slf4j.spi.SLF4JServiceProvider
/.github/afdian.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/.github/afdian.jpg
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: "build"
2 | on:
3 | push:
4 | paths-ignore:
5 | - '**/*.md'
6 | pull_request:
7 | paths-ignore:
8 | - '**/*.md'
9 |
10 | jobs:
11 | release:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v3
16 |
17 | - name: Setup JDK 11
18 | uses: actions/setup-java@v3
19 | with:
20 | distribution: 'adopt'
21 | java-version: '11'
22 |
23 | - name: chmod -R 777 *
24 | run: chmod -R 777 *
25 |
26 | - name: Build Plugin
27 | run: ./gradlew buildPlugin
28 |
29 | - name: Upload
30 | uses: actions/upload-artifact@v3
31 | with:
32 | name: build-${{ github.run_id }}
33 | path: build/mirai/*
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # User-specific stuff
2 | .idea/
3 |
4 | *.iml
5 | *.ipr
6 | *.iws
7 |
8 | # IntelliJ
9 | out/
10 | # mpeltonen/sbt-idea plugin
11 | .idea_modules/
12 |
13 | # JIRA plugin
14 | atlassian-ide-plugin.xml
15 |
16 | # Compiled class file
17 | *.class
18 |
19 | # Log file
20 | *.log
21 |
22 | # BlueJ files
23 | *.ctxt
24 |
25 | # Package Files #
26 | *.jar
27 | *.war
28 | *.nar
29 | *.ear
30 | *.zip
31 | *.tar.gz
32 | *.rar
33 |
34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
35 | hs_err_pid*
36 |
37 | *~
38 |
39 | # temporary files which can be created if a process still has a handle open of a deleted file
40 | .fuse_hidden*
41 |
42 | # KDE directory preferences
43 | .directory
44 |
45 | # Linux trash folder which might appear on any partition or disk
46 | .Trash-*
47 |
48 | # .nfs files are created when an open file is removed but is still being accessed
49 | .nfs*
50 |
51 | # General
52 | .DS_Store
53 | .AppleDouble
54 | .LSOverride
55 |
56 | # Icon must end with two \r
57 | Icon
58 |
59 | # Thumbnails
60 | ._*
61 |
62 | # Files that might appear in the root of a volume
63 | .DocumentRevisions-V100
64 | .fseventsd
65 | .Spotlight-V100
66 | .TemporaryItems
67 | .Trashes
68 | .VolumeIcon.icns
69 | .com.apple.timemachine.donotpresent
70 |
71 | # Directories potentially created on remote AFP share
72 | .AppleDB
73 | .AppleDesktop
74 | Network Trash Folder
75 | Temporary Items
76 | .apdisk
77 |
78 | # Windows thumbnail cache files
79 | Thumbs.db
80 | Thumbs.db:encryptable
81 | ehthumbs.db
82 | ehthumbs_vista.db
83 |
84 | # Dump file
85 | *.stackdump
86 |
87 | # Folder config file
88 | [Dd]esktop.ini
89 |
90 | # Recycle Bin used on file shares
91 | $RECYCLE.BIN/
92 |
93 | # Windows Installer files
94 | *.cab
95 | *.msi
96 | *.msix
97 | *.msm
98 | *.msp
99 |
100 | # Windows shortcuts
101 | *.lnk
102 |
103 | .gradle
104 | build/
105 |
106 | # Ignore Gradle GUI config
107 | gradle-app.setting
108 |
109 | # Cache of project
110 | .gradletasknamecache
111 |
112 | **/build/
113 |
114 | # Common working directory
115 | run/
116 |
117 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
118 | !gradle-wrapper.jar
119 |
120 |
121 | # Local Test Launch point
122 | src/test/kotlin/RunTerminal.kt
123 |
124 | # Mirai console files with direct bootstrap
125 | /config
126 | /data
127 | /plugins
128 | /bots
129 |
130 | # Local Test Launch Point working directory
131 | /debug-sandbox
132 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to mirai it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) mirai the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to mirai the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, mirai and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | mirai a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to mirai, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise mirai, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [Meme Helper](https://github.com/cssxsh/meme-helper)
2 |
3 | > 基于 [Mirai Console](https://github.com/mamoe/mirai-console) 的 MEME 生成插件
4 |
5 | [](https://github.com/cssxsh/meme-helper/releases)
6 | [](https://repo1.maven.org/maven2/xyz/cssxsh/meme-helper/)
7 | [](https://mirai.mamoe.net/topic/1271)
8 |
9 | **使用前应该查阅的相关文档或项目**
10 |
11 | * [User Manual](https://github.com/mamoe/mirai/blob/dev/docs/UserManual.md)
12 | * [Permission Command](https://github.com/mamoe/mirai/blob/dev/mirai-console/docs/BuiltInCommands.md#permissioncommand)
13 |
14 | 插件权限ID `xyz.cssxsh.mirai.plugin.meme-helper:*`,使用前授予权限
15 |
16 | 插件依赖于 [Mirai Skia Plugin](https://github.com/cssxsh/mirai-skia-plugin) (必要)
17 | 插件依赖于 [Mirai Hibernate Plugin](https://github.com/cssxsh/mirai-hibernate-plugin) (不必要)
18 |
19 | **目前插件开发处于实验性阶段**
20 |
21 | ## 安装
22 |
23 | ### MCL 指令安装
24 |
25 | `./mcl --update-package xyz.cssxsh:meme-helper --channel maven-stable --type plugin`
26 |
27 | ### 手动安装
28 |
29 | 1. 从 [Releases](https://github.com/cssxsh/meme-helper/releases) 或者 [Maven](https://repo1.maven.org/maven2/xyz/cssxsh/meme-helper/) 下载 `mirai2.jar`
30 | 2. 将其放入 `plugins` 文件夹中
31 |
32 | ## 基本功能
33 |
34 | ### PornHub
35 |
36 | PornHub Login 生成
37 |
38 | 示例: `#ph Git Hub`
39 | 
40 |
41 | ### PetPet
42 |
43 | 摸摸头生成
44 |
45 | 可以发送 `图片` 或者 `@某人` 或者 `QQ号` 确定卡片图片
46 | 示例: `#pet @群主`, `#pet 123456`
47 | 
48 |
49 | ### Dear
50 |
51 | 狂亲表情包生成
52 |
53 | 可以发送 `图片` 或者 `@某人` 或者 `QQ号` 确定卡片图片
54 | 示例: `#dear @群主`, `#dear 123456`
55 | 
56 |
57 | ### 5000choyen
58 |
59 | 5000choyen 表情包生成
60 |
61 | 示例: `#choyen 我想吃 肯德基疯狂星期四套餐`
62 | 
63 |
64 | ### zzkia
65 |
66 | 诺基亚短信 表情包生成
67 |
68 | 示例: `#pinyin 无内鬼,来点色图`
69 | 
70 |
71 | ### 游戏王
72 |
73 | 游戏王 表情包生成
74 | 可以发送 `图片` 或者 `@某人` 或者 `QQ号` 确定卡片图片
75 | 可以使用 `xxx=yyy`, 指定属性,可选属性有:
76 | * 卡片名称: `name`
77 | * 怪兽属性: `attr` = `dark, divine, earth, fire, light, spell, trap, water, wind`
78 | * 怪兽等级: `level`
79 | * 怪兽种族: `race`
80 | * 怪兽攻击: `atk`
81 | * 怪兽防御: `def`
82 | * 卡片版权:`copyright`
83 |
84 | 示例:
85 | ```
86 | #spell @Him188
87 | name=Welcome PR
88 | 你可以PR
89 | ```
90 | 
91 |
92 | ```
93 | #trap @Him188
94 | name=Useless PR
95 | 这个还是由我们自己实现
96 | ```
97 | 
98 |
99 | ```
100 | #monster @LaoLittle
101 | name=援交JD
102 | 五元一次
103 | ```
104 | 
105 |
106 | ### 随机表情包
107 |
108 | 随机表情包一张(通过 Mirai Hibernate Plugin 插件 从消息中收集得到)
109 |
110 | 示例: `#群友表情`
111 |
112 | ### Emoji合成
113 |
114 | Google Emoji Kitchen,两个 emoji 合成 一个
115 |
116 | 示例: `😍+🥵`
117 | 
118 |
119 |
120 | ### 幻影坦克
121 |
122 | 需要 `mirai-skia-plugin` 版本 `>= 1.1.9`
123 |
124 | ```
125 | #tank
126 | [图片]
127 | [图片]
128 | ```
129 | 
130 |
131 | ### 学历
132 |
133 | 需要 `mirai-skia-plugin` 版本 `>= 1.2.0`
134 |
135 | ```
136 | #学历
137 | 专科
138 | 知乎
139 | 知乎大学
140 | 肆业
141 | 2022年6月
142 | ```
143 | 
144 |
145 | ### sekai-stickers
146 |
147 | ```
148 | #Airi 01
149 | keep up
150 | ```
151 |
152 | https://st.ayaka.one/
153 |
154 | ## [爱发电](https://afdian.net/@cssxsh)
155 |
156 | 
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm") version "1.8.22"
3 | kotlin("plugin.serialization") version "1.8.22"
4 |
5 | id("net.mamoe.mirai-console") version "2.16.0"
6 | id("me.him188.maven-central-publish") version "1.0.0"
7 | }
8 |
9 | group = "xyz.cssxsh"
10 | version = "1.4.0"
11 |
12 | repositories {
13 | mavenLocal()
14 | mavenCentral()
15 | }
16 |
17 | mavenCentralPublish {
18 | useCentralS01()
19 | singleDevGithubProject("cssxsh", "meme-helper")
20 | licenseFromGitHubProject("AGPL-3.0")
21 | workingDir = System.getenv("PUBLICATION_TEMP")?.let { file(it).resolve(projectName) }
22 | ?: buildDir.resolve("publishing-tmp")
23 | publication {
24 | artifact(tasks["buildPlugin"])
25 | }
26 | }
27 |
28 | dependencies {
29 | compileOnly("xyz.cssxsh:bilibili-helper:1.7.2")
30 | compileOnly("xyz.cssxsh:weibo-helper:1.6.2")
31 | compileOnly("xyz.cssxsh.mirai:mirai-hibernate-plugin:2.9.0")
32 | compileOnly("xyz.cssxsh.mirai:mirai-skia-plugin:1.3.1")
33 | implementation("com.itextpdf:io:8.0.5")
34 | testImplementation(kotlin("test"))
35 | //
36 | implementation(platform("net.mamoe:mirai-bom:2.16.0"))
37 | compileOnly("net.mamoe:mirai-core")
38 | compileOnly("net.mamoe:mirai-core-utils")
39 | compileOnly("net.mamoe:mirai-console-compiler-common")
40 | testImplementation("net.mamoe:mirai-core-utils")
41 | //
42 | implementation(platform("io.ktor:ktor-bom:2.3.9"))
43 | implementation("io.ktor:ktor-client-okhttp")
44 | implementation("io.ktor:ktor-client-encoding")
45 | //
46 | implementation(platform("org.slf4j:slf4j-parent:2.0.16"))
47 | testImplementation("org.slf4j:slf4j-simple")
48 | }
49 |
50 | mirai {
51 | jvmTarget = JavaVersion.VERSION_11
52 | }
53 |
54 | kotlin {
55 | explicitApi()
56 | }
57 |
58 | tasks {
59 | test {
60 | useJUnitPlatform()
61 | }
62 | }
--------------------------------------------------------------------------------
/example/5000choyen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/example/5000choyen.png
--------------------------------------------------------------------------------
/example/dear.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/example/dear.gif
--------------------------------------------------------------------------------
/example/monster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/example/monster.png
--------------------------------------------------------------------------------
/example/petpet.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/example/petpet.gif
--------------------------------------------------------------------------------
/example/pornhub.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/example/pornhub.png
--------------------------------------------------------------------------------
/example/school.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/example/school.png
--------------------------------------------------------------------------------
/example/spell.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/example/spell.png
--------------------------------------------------------------------------------
/example/tank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/example/tank.png
--------------------------------------------------------------------------------
/example/trap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/example/trap.png
--------------------------------------------------------------------------------
/example/zzkia.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/example/zzkia.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MSYS* | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "meme-helper"
--------------------------------------------------------------------------------
/src/main/kotlin/Context.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.engine.okhttp.*
5 | import io.ktor.client.plugins.*
6 | import io.ktor.client.plugins.compression.*
7 | import io.ktor.client.request.*
8 | import io.ktor.client.statement.*
9 | import io.ktor.http.*
10 | import io.ktor.utils.io.jvm.javaio.*
11 | import kotlinx.coroutines.*
12 | import net.mamoe.mirai.console.permission.*
13 | import net.mamoe.mirai.console.plugin.jvm.*
14 | import net.mamoe.mirai.message.data.*
15 | import net.mamoe.mirai.message.data.Image.Key.queryUrl
16 | import net.mamoe.mirai.utils.*
17 | import org.jetbrains.skia.Image as SkiaImage
18 | import xyz.cssxsh.mirai.meme.service.*
19 | import java.io.File
20 | import java.time.*
21 |
22 | internal val logger by lazy {
23 | try {
24 | MemeHelperPlugin.logger
25 | } catch (_: ExceptionInInitializerError) {
26 | MiraiLogger.Factory.create(MemeHelper::class)
27 | }
28 | }
29 |
30 | internal lateinit var avatarFolder: File
31 |
32 | internal lateinit var imageFolder: File
33 |
34 | internal val http = HttpClient(OkHttp) {
35 | CurlUserAgent()
36 | ContentEncoding()
37 | expectSuccess = true
38 | install(HttpTimeout) {
39 | connectTimeoutMillis = 30_000
40 | socketTimeoutMillis = 30_000
41 | }
42 | }
43 |
44 | /**
45 | * 下载用户头像
46 | * @param id 用户的ID
47 | * @param size 图片尺寸
48 | */
49 | public suspend fun avatar(id: Long, size: Int = 140): SkiaImage {
50 | val cache = http.prepareGet("https://q.qlogo.cn/g?b=qq&nk=${id}&s=${size}").execute { response ->
51 | val file = avatarFolder.resolve("${id}.${size}.${response.contentType()?.contentSubtype}")
52 | if (file.exists().not() || file.lastModified() < (response.lastModified()?.time ?: 0)) {
53 | file.outputStream().use { output ->
54 | val channel = response.bodyAsChannel()
55 |
56 | while (!channel.isClosedForRead) channel.copyTo(output)
57 | }
58 | }
59 | file
60 | }
61 |
62 | return SkiaImage.makeFromEncoded(bytes = cache.readBytes())
63 | }
64 |
65 | /**
66 | * 下载聊天图片
67 | * @param image 聊天图片
68 | */
69 | public suspend fun cache(image: Image): SkiaImage {
70 | val md5 = image.md5.toUHexString(separator = "")
71 | val cache = imageFolder.listFiles { file -> file.name.startsWith(prefix = md5) }?.firstOrNull()
72 | ?: http.prepareGet(image.queryUrl()).execute { response ->
73 | val file = imageFolder.resolve("${md5}.${response.contentType()?.contentSubtype}")
74 | file.outputStream().use { output ->
75 | val channel = response.bodyAsChannel()
76 |
77 | while (!channel.isClosedForRead) channel.copyTo(output)
78 | }
79 | file
80 | }
81 |
82 | return SkiaImage.makeFromEncoded(bytes = cache.readBytes())
83 | }
84 |
85 | internal suspend fun download(urlString: String, folder: File): File = supervisorScope {
86 | http.prepareGet(urlString).execute { response ->
87 | val relative = response.headers[HttpHeaders.ContentDisposition]
88 | ?.let { ContentDisposition.parse(it).parameter(ContentDisposition.Parameters.FileName) }
89 | ?: response.request.url.encodedPath.substringAfterLast('/').decodeURLPart()
90 |
91 | val file = folder.resolve(relative)
92 |
93 | if (file.exists()) {
94 | logger.info { "文件 ${file.name} 已存在,跳过下载" }
95 | } else {
96 | logger.info { "文件 ${file.name} 开始下载" }
97 | file.outputStream().use { output ->
98 | val channel = response.bodyAsChannel()
99 |
100 | while (!channel.isClosedForRead) channel.copyTo(output)
101 | }
102 | }
103 |
104 | file
105 | }
106 | }
107 |
108 | internal fun JvmPlugin.loadMemeService() {
109 | MemeService.coroutineContext = coroutineContext + CoroutineName("MemeServiceLoader") + Dispatchers.IO
110 | @OptIn(MiraiInternalApi::class)
111 | @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
112 | val services: Sequence = sequence {
113 | for (classLoader in MemeHelperPlugin.loader.classLoaders) {
114 | try {
115 | with(net.mamoe.mirai.console.internal.util.PluginServiceHelper) {
116 | val services = classLoader.findServices().loadAllServices()
117 | for (service in services) {
118 | if (MemeService[service.id] != null) {
119 | logger.warning { "${service.id} 加载重复" }
120 | continue
121 | }
122 |
123 | yield(service)
124 | }
125 | }
126 | } catch (cause: Exception) {
127 | logger.warning({ "MemeService 加载失败" }, cause)
128 | }
129 | }
130 | }
131 | for (service in services) {
132 | try {
133 | val properties = resolveConfigFile("${service.id}.properties")
134 | if (properties.exists()) {
135 | properties.inputStream().use { input ->
136 | service.properties.load(input)
137 | }
138 | } else {
139 | properties.outputStream().use { output ->
140 | service.properties.store(output, "${OffsetDateTime.now()}")
141 | }
142 | }
143 |
144 | val folder = resolveDataFile(service.id).apply { mkdirs() }
145 |
146 | service.load(folder = folder)
147 | } catch (cause: Throwable) {
148 | logger.info({ "${service.name} 加载失败" }, cause)
149 | continue
150 | }
151 |
152 | MemeService.instances.add(service)
153 | logger.info { "${service.name} 加载成功" }
154 | }
155 | }
156 |
157 | internal fun JvmPlugin.enableMemeService() {
158 | for (service in MemeService) {
159 | if (!service.loaded) continue
160 | try {
161 | val permission = PermissionService.INSTANCE.register(
162 | id = permissionId(service.id),
163 | description = service.description,
164 | parent = parentPermission
165 | )
166 |
167 | service.enable(permission)
168 | logger.info { "enable success, ${service.name} - ${service.permission}" }
169 | } catch (cause: Throwable) {
170 | logger.warning({ "enable failure: ${service.name} - ${cause.message}" }, cause)
171 | }
172 | }
173 | }
174 |
175 | internal fun JvmPlugin.disableMemeService() {
176 | for (service in MemeService) {
177 | if (!service.loaded) continue
178 | service.disable()
179 | dataFolder.resolve("${service.id}.properties").outputStream().use { output ->
180 | service.properties.store(output, "${OffsetDateTime.now()}")
181 | }
182 | }
183 | }
184 |
185 |
--------------------------------------------------------------------------------
/src/main/kotlin/EmojiKitchen.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme
2 |
3 | import net.mamoe.mirai.utils.*
4 |
5 | public data class EmojiKitchen internal constructor(val items: Map) {
6 |
7 | public fun cook(left: String, right: String): Pair? {
8 | val l = left.unicode()
9 | val r = right.unicode()
10 | // val list = items[l] ?: items[r] ?: return null
11 | // val item = list.combinations.find { it.leftEmoji == left && it.rightEmoji == right } ?: return null
12 |
13 | val combinationsLists = items[l]?.combinations?.values?.flatten()
14 | ?: items[r]?.combinations?.values?.flatten()
15 | ?: return null
16 |
17 | val item = combinationsLists.find { it.leftEmoji == left && it.rightEmoji == right }
18 | ?: return null
19 |
20 | return item.filename to item.gStaticUrl
21 | }
22 |
23 | public companion object {
24 | @PublishedApi
25 | internal fun String.unicode(): String {
26 | val bytes = toByteArray(charset = Charsets.UTF_32)
27 | return (bytes.indices step 4).joinToString(separator = "-") {
28 | bytes.toInt(offset = it).toString(radix = 16)
29 | }
30 | }
31 |
32 | @PublishedApi
33 | internal const val LAST_UPDATE: String = "2023-10-02T11:19:09.000-07:00"
34 |
35 | // language=RegExp
36 | @PublishedApi
37 | internal const val EMOJI_REGEX: String = """\u2764\ufe0f\u200d\ud83e\ude79|\ud83d\ude2e\u200d\ud83d\udca8|\ud83d\ude36\u200d\ud83c\udf2b\ufe0f|[\u2601-\u2b50]\ufe0f?|[\ud83c\udc04-\ud83c\udff9]\ufe0f?|[\ud83d\udc0c-\ud83d\udefc]\ufe0f?|[\ud83e\udd0d-\ud83e\udee7]"""
38 | }
39 | }
--------------------------------------------------------------------------------
/src/main/kotlin/EmojiKitchenCombination.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class EmojiKitchenCombination(
7 | @SerialName("gStaticUrl")
8 | public val gStaticUrl: String,
9 | @SerialName("alt")
10 | public val alt: String,
11 | @SerialName("leftEmoji")
12 | public val leftEmoji: String,
13 | @SerialName("leftEmojiCodepoint")
14 | public val leftEmojiCodepoint: String,
15 | @SerialName("rightEmoji")
16 | public val rightEmoji: String,
17 | @SerialName("rightEmojiCodepoint")
18 | public val rightEmojiCodepoint: String,
19 | @SerialName("date")
20 | public val date: String,
21 | @SerialName("isLatest")
22 | public val isLatest: Boolean,
23 | @SerialName("gBoardOrder")
24 | public val gBoardOrder: Int,
25 | ) {
26 | public val filename: String
27 | get() = gStaticUrl.substringAfterLast('/')
28 |
29 | }
--------------------------------------------------------------------------------
/src/main/kotlin/EmojiKitchenItem.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class EmojiKitchenItem(
7 | @SerialName("alt")
8 | public val alt: String,
9 | @SerialName("emoji")
10 | public val emoji: String,
11 | @SerialName("emojiCodepoint")
12 | public val emojiCodepoint: String,
13 | @SerialName("gBoardOrder")
14 | public val gBoardOrder: Int,
15 | @SerialName("keywords")
16 | public val keywords: List,
17 | @SerialName("category")
18 | public val category: String,
19 | @SerialName("subcategory")
20 | public val subcategory: String,
21 | //由于json结构发生变化,序列化需要调整
22 | @SerialName("combinations")
23 | public val combinations: Map>,
24 | )
--------------------------------------------------------------------------------
/src/main/kotlin/EmojiKitchenMetadata.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class EmojiKitchenMetadata(
7 | @SerialName("knownSupportedEmoji")
8 | public val knownSupportedEmoji: List,
9 | @SerialName("data")
10 | public val data: Map
11 | )
--------------------------------------------------------------------------------
/src/main/kotlin/MemeHelper.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme
2 |
3 | import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender
4 | import net.mamoe.mirai.console.permission.PermissionService.Companion.hasPermission
5 | import net.mamoe.mirai.event.*
6 | import net.mamoe.mirai.event.events.*
7 | import net.mamoe.mirai.message.data.*
8 | import net.mamoe.mirai.utils.*
9 | import xyz.cssxsh.mirai.meme.service.*
10 | import kotlin.coroutines.*
11 | import kotlin.coroutines.cancellation.*
12 |
13 | public object MemeHelper : SimpleListenerHost() {
14 |
15 | @EventHandler
16 | public suspend fun MessageEvent.handle() {
17 | if (this is MessageSyncEvent) return
18 | for (service in MemeService) {
19 | if (!service.loaded) continue
20 | try {
21 | if (!toCommandSender().hasPermission(service.permission)) continue
22 | } catch (_: IllegalArgumentException) {
23 | continue
24 | }
25 |
26 | val match = service.regex.find(input = message.content) ?: continue
27 |
28 | val message = with(service) { replier(match) } ?: continue
29 |
30 | subject.sendMessage(message = message)
31 | }
32 | }
33 |
34 | override fun handleException(context: CoroutineContext, exception: Throwable) {
35 | when (exception) {
36 | is ExceptionInEventHandlerException -> logger.warning({ "MemeHelper Handle Exception" }, exception.cause)
37 | is CancellationException -> Unit
38 | else -> logger.warning({ "MemeHelper Exception" }, exception)
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/main/kotlin/MemeHelperPlugin.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme
2 |
3 | import kotlinx.coroutines.*
4 | import net.mamoe.mirai.console.extension.*
5 | import net.mamoe.mirai.console.plugin.jvm.*
6 | import net.mamoe.mirai.event.*
7 |
8 | @PublishedApi
9 | internal object MemeHelperPlugin : KotlinPlugin(
10 | JvmPluginDescription(
11 | id = "xyz.cssxsh.mirai.plugin.meme-helper",
12 | name = "meme-helper",
13 | version = "1.4.0"
14 | ) {
15 | author("cssxsh")
16 | dependsOn("xyz.cssxsh.mirai.plugin.mirai-skia-plugin", ">= 1.1.0", false)
17 | dependsOn("xyz.cssxsh.mirai.plugin.mirai-hibernate-plugin", ">= 2.4.0", true)
18 | dependsOn("xyz.cssxsh.mirai.plugin.weibo-helper", true)
19 | dependsOn("xyz.cssxsh.mirai.plugin.bilibili-helper", true)
20 | }
21 | ) {
22 |
23 | override fun PluginComponentStorage.onLoad() {
24 | loadMemeService()
25 | }
26 |
27 | override fun onEnable() {
28 | avatarFolder = resolveDataFile("avatar").apply { mkdirs() }
29 | imageFolder = resolveDataFile("image").apply { mkdirs() }
30 | enableMemeService()
31 | MemeHelper.registerTo(globalEventChannel())
32 | }
33 |
34 | override fun onDisable() {
35 | MemeHelper.cancel()
36 | disableMemeService()
37 | }
38 | }
--------------------------------------------------------------------------------
/src/main/kotlin/ProjectSekaiStickers.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme
2 |
3 | import com.itextpdf.io.font.woff2.*
4 | import kotlinx.serialization.*
5 | import kotlinx.serialization.json.*
6 | import org.jetbrains.skia.*
7 | import org.jetbrains.skia.paragraph.*
8 | import xyz.cssxsh.skia.FontUtils
9 | import java.io.Closeable
10 | import java.io.File
11 | import java.io.FileNotFoundException
12 | import java.io.InputStream
13 | import java.util.zip.ZipFile
14 | import kotlin.math.*
15 |
16 | /**
17 | * [The Original Ayaka](https://github.com/TheOriginalAyaka/sekai-stickers)
18 | */
19 | public class ProjectSekaiStickers private constructor(private val source: ZipFile) : Closeable {
20 | public constructor(path: String) : this(source = ZipFile(path))
21 | public constructor(file: File) : this(source = ZipFile(file))
22 |
23 | private fun ZipFile.getInputStream(name: String): InputStream {
24 | val entry = getEntry(name) ?: throw FileNotFoundException(name)
25 | return getInputStream(entry)
26 | }
27 |
28 | internal val characters: List by lazy {
29 | source.getInputStream("sekai-stickers-main/src/characters.json").use { input ->
30 | @OptIn(ExperimentalSerializationApi::class)
31 | Json.decodeFromStream>(input)
32 | }
33 | }
34 |
35 | private val fonts: FontCollection by lazy {
36 | source.getInputStream("sekai-stickers-main/src/fonts/YurukaStd.woff2").use { input ->
37 | FontUtils.loadTypeface(Woff2Converter.convert(input.readAllBytes()))
38 | }
39 | source.getInputStream("sekai-stickers-main/src/fonts/ShangShouFangTangTi.woff2").use { input ->
40 | FontUtils.loadTypeface(Woff2Converter.convert(input.readAllBytes()))
41 | }
42 | FontCollection()
43 | .setDynamicFontManager(FontUtils.provider)
44 | .setDefaultFontManager(FontMgr.default)
45 | }
46 |
47 | internal fun image(character: Character): Image {
48 | val bytes = source.getInputStream("sekai-stickers-main/public/img/${character.image}")
49 | .use { it.readAllBytes() }
50 | return Image.makeFromEncoded(bytes)
51 | }
52 |
53 | public fun create(name: String, block: Content.() -> Unit): Surface {
54 | val character = characters.find { it.name == name } ?: throw NoSuchElementException(name)
55 |
56 | val surface = Surface.makeRasterN32Premul(296, 256)
57 | val canvas = surface.canvas
58 | val image = image(character = character)
59 | val content = character.default.apply(block)
60 |
61 | val hRatio = surface.width.toFloat() / image.width
62 | val vRatio = surface.height.toFloat() / image.height
63 | val ratio = minOf(hRatio, vRatio)
64 | canvas.drawImageRect(
65 | image = image,
66 | dst = Rect.makeXYWH(
67 | l = (surface.width - image.width * ratio) / 2,
68 | t = (surface.height - image.height * ratio) / 2,
69 | w = image.width * ratio,
70 | h = image.height * ratio
71 | )
72 | )
73 |
74 | val rad = content.rotate / 10
75 | val sin = sin(rad)
76 | val cos = cos(rad)
77 | val m = Matrix33(
78 | cos, -sin, content.x - content.x * cos + content.y * sin,
79 | sin, cos, content.y - content.y * cos - content.y * sin,
80 | 0f, 0f, 1f
81 | )
82 | canvas.concat(m)
83 |
84 | val style = ParagraphStyle().apply {
85 | maxLinesCount = 3
86 | alignment = Alignment.CENTER
87 | }
88 |
89 | ParagraphBuilder(style, fonts)
90 | .pushStyle(
91 | TextStyle()
92 | .setFontSize(content.size)
93 | .setForeground(Paint().apply {
94 | strokeCap = PaintStrokeCap.ROUND
95 | strokeJoin = PaintStrokeJoin.ROUND
96 | strokeWidth = 10F
97 | color = Color.WHITE
98 | mode = PaintMode.STROKE
99 | })
100 | .setFontFamilies(arrayOf("FOT-Yuruka Std UB", "YurukaStd", "SSFangTangTi"))
101 | )
102 | .addText(content.text)
103 | .build()
104 | .layout(surface.width.toFloat())
105 | .paint(canvas, -6F, 6F)
106 |
107 |
108 | ParagraphBuilder(style, fonts)
109 | .pushStyle(
110 | TextStyle()
111 | .setFontSize(content.size)
112 | .setForeground(Paint().apply {
113 | color = character.color.replace("#", "FF").toLong(16).toInt()
114 | mode = PaintMode.FILL
115 | })
116 | .setFontFamilies(arrayOf("FOT-Yuruka Std UB", "YurukaStd", "SSFangTangTi"))
117 | )
118 | .addText(content.text)
119 | .build()
120 | .layout(surface.width.toFloat())
121 | .paint(canvas, -6F, 6F)
122 |
123 | return surface
124 | }
125 |
126 | override fun close(): Unit = source.close()
127 |
128 | @Serializable
129 | internal data class Character(
130 | @SerialName("character")
131 | val category: String,
132 | @SerialName("color")
133 | val color: String,
134 | @SerialName("defaultText")
135 | val default: Content,
136 | @SerialName("id")
137 | val id: String,
138 | @SerialName("img")
139 | val image: String,
140 | @SerialName("name")
141 | val name: String
142 | )
143 |
144 | @Serializable
145 | public data class Content(
146 | @SerialName("r")
147 | var rotate: Float = 0F,
148 | @SerialName("s")
149 | var size: Float = 0F,
150 | @SerialName("text")
151 | var text: String = "",
152 | @SerialName("x")
153 | var x: Float = 0F,
154 | @SerialName("y")
155 | var y: Float = 0F
156 | )
157 | }
--------------------------------------------------------------------------------
/src/main/kotlin/YgoCard.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme
2 |
3 | import org.jetbrains.skia.*
4 | import org.jetbrains.skia.paragraph.*
5 | import xyz.cssxsh.skia.*
6 | import java.io.*
7 | import java.util.*
8 |
9 | /**
10 | * [游戏王制卡器](https://ymssx.github.io/ygo/#/)
11 | */
12 | public sealed class YgoCard {
13 |
14 | public abstract val name: String
15 |
16 | public abstract val color: Int
17 |
18 | public abstract val attribute: Attribute
19 |
20 | public abstract val type: String
21 |
22 | public abstract val face: Image
23 |
24 | public abstract val race: List
25 |
26 | public abstract val description: String
27 |
28 | public abstract val locale: Locale
29 |
30 | public abstract val copyright: String?
31 |
32 | protected abstract fun frame(folder: File): File
33 |
34 | @Suppress("EnumEntryName")
35 | public enum class Attribute {
36 | dark, divine, earth, fire, light, spell, trap, water, wind;
37 |
38 | public companion object {
39 | public val monster: List by lazy { values().toList() - spell - trap }
40 | }
41 | }
42 |
43 | /**
44 | * 怪兽卡
45 | */
46 | public data class Monster(
47 | override var name: String,
48 | override var face: Image,
49 | override var description: String,
50 | val level: Int = 1,
51 | val attack: String = "0",
52 | val defend: String = "0",
53 | override val race: List,
54 | override var attribute: Attribute,
55 | override var color: Int = Color.BLACK,
56 | override var locale: Locale = Locale.getDefault(),
57 | override var copyright: String? = null
58 | ) : YgoCard() {
59 | override val type: String get() = ""
60 | override fun frame(folder: File): File = folder.resolve("source/mold/frame/monster_xg.jpg")
61 | }
62 |
63 | /**
64 | * 魔法卡
65 | */
66 | public data class Spell(
67 | override var name: String,
68 | override var face: Image,
69 | override var description: String,
70 | override var color: Int = Color.BLACK,
71 | override var locale: Locale = Locale.getDefault(),
72 | override var copyright: String? = null
73 | ) : YgoCard() {
74 | override val race: List get() = emptyList()
75 | override val attribute: Attribute get() = Attribute.spell
76 | override val type: String get() = "【" + when (locale) {
77 | Locale.JAPAN -> "魔法卡"
78 | Locale.ENGLISH -> "魔法卡"
79 | Locale.SIMPLIFIED_CHINESE -> "魔法卡"
80 | Locale.TRADITIONAL_CHINESE -> "魔法卡"
81 | else -> "魔法卡"
82 | } + "】"
83 | override fun frame(folder: File): File = folder.resolve("source/mold/frame/spell.jpg")
84 | }
85 |
86 | /**
87 | * 陷阱卡
88 | */
89 | public data class Trap(
90 | override var name: String,
91 | override var face: Image,
92 | override var description: String,
93 | override var color: Int = Color.BLACK,
94 | override var locale: Locale = Locale.getDefault(),
95 | override var copyright: String? = null
96 | ) : YgoCard() {
97 | override val race: List get() = emptyList()
98 | override val attribute: Attribute get() = Attribute.trap
99 | override val type: String get() = "【" + when (locale) {
100 | Locale.JAPAN -> "陷阱卡"
101 | Locale.ENGLISH -> "陷阱卡"
102 | Locale.SIMPLIFIED_CHINESE -> "陷阱卡"
103 | Locale.TRADITIONAL_CHINESE -> "陷阱卡"
104 | else -> "陷阱卡"
105 | } + "】"
106 | override fun frame(folder: File): File = folder.resolve("source/mold/frame/trap.jpg")
107 |
108 | }
109 |
110 | private val fonts = FontCollection()
111 | .setDynamicFontManager(FontUtils.provider)
112 | .setDefaultFontManager(FontMgr.default)
113 |
114 | private fun name(): Paragraph {
115 | val style = ParagraphStyle().apply {
116 | maxLinesCount = 1
117 | textStyle = TextStyle().setFontSize(60F).setColor(this@YgoCard.color)
118 | .setFontFamilies(arrayOf("YGO-DIY-GB", "YGO-DIY-2-BIG5", "YGODIY-JP", "YGODIY-MatrixBoldSmallCaps"))
119 | }
120 |
121 | return ParagraphBuilder(style, fonts).addText(name).build()
122 | }
123 |
124 | private fun type(): Paragraph {
125 | val style = ParagraphStyle().apply {
126 | maxLinesCount = 1
127 | alignment = Alignment.RIGHT
128 | textStyle = TextStyle().setFontSize(48F).setColor(Color.BLACK)
129 | .setFontFamilies(arrayOf("YGO-DIY-GB", "YGO-DIY-2-BIG5", "YGODIY-JP", "YGODIY-MatrixBoldSmallCaps"))
130 | }
131 | return ParagraphBuilder(style, fonts).addText(type).build()
132 | }
133 |
134 | private fun race(): Paragraph {
135 | val style = ParagraphStyle().apply {
136 | maxLinesCount = 1
137 | textStyle = TextStyle().setFontSize(26F).setColor(Color.BLACK)
138 | .setFontFamilies(arrayOf("YGO-DIY-GB", "YGO-DIY-2-BIG5", "YGODIY-JP", "YGODIY-MatrixBoldSmallCaps"))
139 | }
140 | val text = race.joinToString(separator = "/", prefix = "【", postfix = "】")
141 |
142 | return ParagraphBuilder(style, fonts).addText(text).build()
143 | }
144 |
145 | private fun description(): Paragraph {
146 | val style = ParagraphStyle().apply {
147 | maxLinesCount = 9
148 | textStyle = TextStyle().setFontSize(24F).setColor(Color.BLACK)
149 | .setFontFamilies(arrayOf("YGO-DIY-GB", "YGO-DIY-2-BIG5", "YGODIY-JP", "YGODIY-MatrixBoldSmallCaps"))
150 | }
151 |
152 | return ParagraphBuilder(style, fonts).addText(description).build()
153 | }
154 |
155 | private fun value(text: String): Paragraph {
156 | val style = ParagraphStyle().apply {
157 | maxLinesCount = 1
158 | alignment = Alignment.RIGHT
159 | textStyle = TextStyle().setFontSize(36F).setColor(Color.BLACK)
160 | .setFontFamilies(arrayOf("YGODIY-MatrixBoldSmallCaps"))
161 | }
162 |
163 | return ParagraphBuilder(style, fonts).addText(text).build()
164 | }
165 |
166 | private fun copyright(): Paragraph {
167 | val style = ParagraphStyle().apply {
168 | maxLinesCount = 1
169 | alignment = Alignment.RIGHT
170 | textStyle = TextStyle().setFontSize(18F).setColor(Color.BLACK)
171 | .setFontFamilies(arrayOf("FOT-Rodin Pro"))
172 | }
173 | val text = this.copyright ?: when (locale) {
174 | Locale.JAPAN -> "ⓒスタジオ·ダイス /集英社·テレビ東京·KONAMI"
175 | Locale.ENGLISH -> "ⓒ1996 KAZUKI TAKAHASHI"
176 | Locale.CHINA -> "ⓒ2020 Studio Dice/SHUEISHA, TV TOKYO,KONAMI"
177 | else -> "ⓒ高橋和希 スタジオ・ダイス/集英社 "
178 | }
179 |
180 | return ParagraphBuilder(style, fonts).addText(text).build()
181 | }
182 |
183 | /**
184 | * @param project [https://github.com/ymssx/ygo-card/archive/refs/heads/master.zip]
185 | */
186 | public fun render(project: File = File(System.getProperty(SOURCE_KEY, "."))): Surface {
187 | project.resolve("source/mold/font").walk().forEach { file ->
188 | try {
189 | FontUtils.loadTypeface(file.path)
190 | } catch (_: Throwable) {
191 | //
192 | }
193 | }
194 | val surface = Surface.makeRasterN32Premul(813, 1185)
195 | val canvas = surface.canvas
196 |
197 | // draw frame
198 | val frame = frame(folder = project)
199 | canvas.drawImage(Image.makeFromEncoded(frame.readBytes()), 0F, 0F)
200 |
201 | // draw name
202 | name().layout(615F).paint(canvas, 65F, 63F)
203 |
204 | // draw attribute
205 | val attribute = project.resolve("source/mold/attribute/${locale.language}/${attribute}.png")
206 | canvas.drawImageRect(Image.makeFromEncoded(attribute.readBytes()), Rect.makeXYWH(680F, 57F, 75F, 75F))
207 |
208 | // draw Type
209 | type().layout(616F).paint(canvas, 132F, 148F)
210 |
211 | // draw Star
212 | if (this is Monster) {
213 | val star = Image.makeFromEncoded(project.resolve("source/mold/star/level.png").readBytes())
214 | repeat(level) { index ->
215 | canvas.drawImageRect(star, Rect.makeXYWH(686F - index * 55, 145F, 50F, 50F))
216 | }
217 | }
218 |
219 | // draw face
220 | canvas.drawImageRect(face, Rect.makeXYWH(100F, 219F, 614F, 616F))
221 |
222 | // draw description
223 |
224 | if (this is Monster) {
225 | race().layout(610F).paint(canvas, 53F, 896F)
226 | description().layout(681F).paint(canvas, 66F, 923F)
227 | } else {
228 | description().layout(681F).paint(canvas, 66F, 896F)
229 | }
230 |
231 | // draw ATK/DEF
232 | if (this is Monster) {
233 | canvas.drawLine(64F, 1079F, 64F + 683, 1079F, Paint().apply {
234 | color = Color.BLACK
235 | strokeWidth = 2F
236 | })
237 | value("ATK/").layout(100F).paint(canvas, 413F, 1080F)
238 | value(attack).layout(100F).paint(canvas, 485F, 1080F)
239 | value("DEF/").layout(100F).paint(canvas, 578F, 1080F)
240 | value(defend).layout(100F).paint(canvas, 650F, 1080F)
241 | }
242 |
243 | // draw copyright
244 | copyright().layout(647F).paint(canvas, 83F, 1122F)
245 |
246 | return surface
247 | }
248 |
249 | public companion object {
250 | @PublishedApi
251 | internal const val SOURCE_KEY: String = "xyz.cssxsh.skia.ygo"
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/src/main/kotlin/face/AdditionInfo.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class AdditionInfo(
7 | @SerialName("emojiId")
8 | val emojiId: String?,
9 | @SerialName("keyword")
10 | val keyword: String?,
11 | @SerialName("order")
12 | val order: Int,
13 | @SerialName("wordingText")
14 | internal val words: List = emptyList(),
15 | @SerialName("fontcolor")
16 | internal val fontcolor: String = "",
17 | @SerialName("backcolor")
18 | internal val backcolor: String = "",
19 | @SerialName("heightItems")
20 | internal val heightItems: String = "",
21 | @SerialName("emojilabel")
22 | internal val emojiLabel: String = "",
23 | @SerialName("__v")
24 | internal val __v: Int = 0,
25 | @SerialName("cfgID")
26 | internal val cfgID: String = "",
27 | @SerialName("_id")
28 | internal val _id: String = "",
29 | @SerialName("id")
30 | internal val id: String = ""
31 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/AppData.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class AppData(
7 | @SerialName("name")
8 | val name: String
9 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/AuthorDetail.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class AuthorDetail(
7 | @SerialName("authorName")
8 | val name: String,
9 | @SerialName("backgroundImg")
10 | val background: String,
11 | @SerialName("desc")
12 | val description: String,
13 | @SerialName("fansNum")
14 | val fans: Int,
15 | @SerialName("headImg")
16 | val head: String,
17 | @SerialName("isFollowed")
18 | @Serializable(with = NumberToBooleanSerializer::class)
19 | val isFollowed: Boolean,
20 | @SerialName("itemList")
21 | val items: List,
22 | @SerialName("itemNum")
23 | val total: Int,
24 | @SerialName("rewardDesc")
25 | val thank: String,
26 | @SerialName("rewardImg")
27 | val reward: String
28 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/DiversionConfig.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class DiversionConfig(
7 | @SerialName("beginTime")
8 | val begin: Long = 0,
9 | @SerialName("copywriting")
10 | val copy: String = "",
11 | @SerialName("displayImg")
12 | val display: String = "",
13 | @SerialName("diversionName")
14 | val name: String = "",
15 | @SerialName("diversionType")
16 | val type: String = "",
17 | @SerialName("endTime")
18 | val end: Long = 0,
19 | @SerialName("from")
20 | val from: String = "",
21 | @SerialName("jumpUrl")
22 | val jump: String = "",
23 | @SerialName("wording")
24 | val wording: String = "",
25 | @SerialName("__v")
26 | internal val __v: Int = 0,
27 | @SerialName("cfgID")
28 | internal val cfgID: String = "",
29 | @SerialName("_id")
30 | internal val _id: String = "",
31 | @SerialName("id")
32 | internal val id: String = ""
33 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/ItemData.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 | import kotlinx.serialization.json.*
5 | import net.mamoe.mirai.utils.*
6 |
7 | @Serializable
8 | public data class ItemData(
9 | @SerialName("authorId")
10 | val authorId: Int = 0,
11 | @SerialName("bid")
12 | val bid: String = "",
13 | @SerialName("cfgID")
14 | internal val cfgID: String = "",
15 | @SerialName("childMagicEmojiId")
16 | internal val childMagicEmojiId: String = "",
17 | @SerialName("desc")
18 | val description: String = "",
19 | @SerialName("favourite")
20 | val favourite: Int = 0,
21 | @SerialName("feeType")
22 | val feeType: Int = 0,
23 | @SerialName("icon")
24 | val icon: Int = 0,
25 | @SerialName("_id")
26 | internal val _id: String = "",
27 | @SerialName("id")
28 | internal val id: String = "",
29 | @SerialName("isApng")
30 | @Serializable(with = NumberToBooleanSerializer::class)
31 | val isApng: Boolean = false,
32 | @SerialName("isFree")
33 | @Serializable(with = NumberToBooleanSerializer::class)
34 | val isFree: Boolean = false,
35 | @SerialName("isOriginal")
36 | @Serializable(with = NumberToBooleanSerializer::class)
37 | val isOriginal: Boolean = false,
38 | @SerialName("QQgif")
39 | @Serializable(with = NumberToBooleanSerializer::class)
40 | val isQQgif: Boolean = false,
41 | @SerialName("isShow")
42 | @Serializable(with = NumberToBooleanSerializer::class)
43 | val isShow: Boolean = false,
44 | @SerialName("itemBgcolor")
45 | val itemBgColor: String = "",
46 | @SerialName("itemId")
47 | val itemId: String = "",
48 | @SerialName("itemImg")
49 | val itemImage: String = "",
50 | @SerialName("itemTitle")
51 | val itemTitle: String = "",
52 | @SerialName("itemType")
53 | val itemType: String = "",
54 | @SerialName("label")
55 | val label: List = emptyList(),
56 | @SerialName("limitBeginTime")
57 | val limitBeginTime: String = "",
58 | @SerialName("limitEndTime")
59 | val limitEndTime: String = "",
60 | @SerialName("limitFreeBeginTime")
61 | val limitFreeBeginTime: String = "",
62 | @SerialName("limitFreeEndTime")
63 | val limitFreeEndTime: String = "",
64 | @SerialName("limitType")
65 | val limitType: Int = 0,
66 | @SerialName("maxVersion")
67 | val maxVersion: String = "",
68 | @SerialName("minVersion")
69 | val minVersion: String = "",
70 | @SerialName("name")
71 | val name: String = "",
72 | @SerialName("platform")
73 | val platform: Int = 0,
74 | @SerialName("price")
75 | val price: Double = 0.0,
76 | @SerialName("productId")
77 | val productId: String = "",
78 | @SerialName("providerId")
79 | val providerId: Int = 0,
80 | @SerialName("qzone")
81 | val qzone: Int = 0,
82 | @SerialName("realSize")
83 | val realSize: String = "",
84 | @SerialName("ringType")
85 | val ringType: Int = 0,
86 | @SerialName("sex")
87 | val sex: Int = 0,
88 | @SerialName("sougou")
89 | val sougou: Int = 0,
90 | @SerialName("tag")
91 | val tag: JsonElement = JsonNull,
92 | @SerialName("tagType")
93 | val tagType: Int = 0,
94 | @SerialName("type")
95 | val type: Int = 0,
96 | @SerialName("typeName")
97 | val typeName: String = "",
98 | @SerialName("updateTipBeginTime")
99 | val updateTipBeginTime: Int = 0,
100 | @SerialName("updateTipEndTime")
101 | val updateTipEndTime: Int = 0,
102 | @SerialName("updateTipWording")
103 | val updateTipWording: String = "",
104 | @SerialName("valid")
105 | @Serializable(with = NumberToBooleanSerializer::class)
106 | val valid: Boolean,
107 | @SerialName("validArea")
108 | val validArea: Int = 0,
109 | @SerialName("validBefore")
110 | val validBefore: String = "",
111 | @SerialName("validity")
112 | val validity: Int = 0,
113 | @SerialName("whiteList")
114 | val whiteList: String = "",
115 | @SerialName("pc")
116 | val operation: List = emptyList(),
117 | @SerialName("zip")
118 | val zip: JsonElement = JsonNull
119 | ) {
120 | @OptIn(MiraiExperimentalApi::class)
121 | public val compressed: ItemSource? by lazy {
122 | val map: Map = try {
123 | MarketFaceHelper.json.decodeFromJsonElement(zip)
124 | } catch (_: SerializationException) {
125 | emptyMap()
126 | }
127 | map["compressed"]
128 | }
129 |
130 | @OptIn(MiraiExperimentalApi::class)
131 | public val src: ItemSource? by lazy {
132 | val map: Map = try {
133 | MarketFaceHelper.json.decodeFromJsonElement(zip)
134 | } catch (_: SerializationException) {
135 | emptyMap()
136 | }
137 | map["src"]
138 | }
139 | }
--------------------------------------------------------------------------------
/src/main/kotlin/face/ItemInfo.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 |
5 | /**
6 | * @property appId 1. 表情
7 | */
8 | @Serializable
9 | public data class ItemInfo(
10 | @SerialName("appid")
11 | val appId: Int = 0,
12 | @SerialName("itemid")
13 | val itemId: Int = 0,
14 | @SerialName("name")
15 | val name: String = "",
16 | @SerialName("onlinetime")
17 | val onlineTime: String = "",
18 | @SerialName("url")
19 | val url: String = ""
20 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/ItemSource.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class ItemSource(
7 | @SerialName("cdnName")
8 | val cdnName: String = "",
9 | @SerialName("extName")
10 | val extName: String = "",
11 | @SerialName("from")
12 | val from: String = "",
13 | @SerialName("hashName")
14 | val hashName: String = "",
15 | @SerialName("md5")
16 | val md5: String = "",
17 | @SerialName("size")
18 | val size: Int = 0,
19 | @SerialName("src")
20 | val src: String = "",
21 | @SerialName("subPath")
22 | val subPath: String = ""
23 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/KeywordsSerializer.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 | import kotlinx.serialization.builtins.*
5 | import kotlinx.serialization.descriptors.*
6 | import kotlinx.serialization.encoding.*
7 | import kotlinx.serialization.json.*
8 |
9 | @PublishedApi
10 | internal object KeywordsSerializer : KSerializer> {
11 |
12 | override val descriptor: SerialDescriptor get() = JsonElement.serializer().descriptor
13 |
14 | override fun deserialize(decoder: Decoder): List {
15 | return when (val element = decoder.decodeSerializableValue(JsonElement.serializer())) {
16 | is JsonPrimitive -> listOf(element.content)
17 | is JsonArray -> element.map { it.jsonPrimitive.content }
18 | else -> throw SerializationException("data: $element")
19 | }
20 | }
21 |
22 | override fun serialize(encoder: Encoder, value: List) {
23 | encoder.encodeSerializableValue(ListSerializer(String.serializer()), value)
24 | }
25 | }
--------------------------------------------------------------------------------
/src/main/kotlin/face/MD5Info.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class MD5Info(
7 | @SerialName("md5")
8 | val md5: String,
9 | @SerialName("name")
10 | val name: String
11 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/MarketFaceAndroid.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 | import kotlinx.serialization.json.*
5 |
6 | @Serializable
7 | public data class MarketFaceAndroid(
8 | @SerialName("author")
9 | val author: String = "",
10 | @SerialName("childEmojiId")
11 | internal val childEmojiId: String? = null,
12 | @SerialName("commDiyText")
13 | internal val commDiyText: List = emptyList(),
14 | @SerialName("diversionConfig")
15 | val diversion: DiversionConfig = DiversionConfig(),
16 | @SerialName("downloadcount")
17 | val downloadCount: Int = 0,
18 | @SerialName("feetype")
19 | val feeType: Int = 1,
20 | @SerialName("filesize")
21 | val fileSize: String = "0",
22 | @SerialName("id")
23 | val id: String = "",
24 | @SerialName("imgs")
25 | val images: List = emptyList(),
26 | @SerialName("isApng")
27 | @Serializable(with = NumberToBooleanSerializer::class)
28 | val isApng: Boolean = false,
29 | @SerialName("isOriginal")
30 | @Serializable(with = NumberToBooleanSerializer::class)
31 | val isOriginal: Boolean = false,
32 | @SerialName("mark")
33 | val mark: String = "",
34 | @SerialName("name")
35 | val name: String = "",
36 | @SerialName("operationInfo")
37 | val operation: List = emptyList(),
38 | @SerialName("price")
39 | val price: Double = 0.0,
40 | @SerialName("rights")
41 | val rights: String = "",
42 | @SerialName("ringtype")
43 | val ringType: String = "0",
44 | @SerialName("status")
45 | val status: String = "1",
46 | @SerialName("supportApngSize")
47 | val supportApngSize: List = emptyList(),
48 | @SerialName("supportSize")
49 | val supportSize: List = emptyList(),
50 | @SerialName("type")
51 | val type: Int = 0,
52 | @SerialName("updateTime")
53 | val updateTime: Long = 0,
54 | @SerialName("validArea")
55 | val validArea: String = ""
56 | ) {
57 | @Serializable
58 | public data class Image(
59 | @SerialName("diyText")
60 | internal val diyText: List = emptyList(),
61 | @SerialName("id")
62 | val id: String = "",
63 | @SerialName("keywords")
64 | @Serializable(KeywordsSerializer::class)
65 | val keywords: List = emptyList(),
66 | @SerialName("name")
67 | val name: String = "",
68 | @SerialName("param")
69 | val param: String = "",
70 | @SerialName("wHeightInPhone")
71 | val height: Int = 0,
72 | @SerialName("wWidthInPhone")
73 | val width: Int = 0,
74 | @SerialName("voiceData")
75 | internal val voice: JsonElement = JsonNull
76 | )
77 |
78 | @Serializable
79 | public data class Size(
80 | @SerialName("Height")
81 | val height: Int = 0,
82 | @SerialName("Width")
83 | val width: Int = 0
84 | )
85 | }
--------------------------------------------------------------------------------
/src/main/kotlin/face/MarketFaceBaseInfo.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 | import kotlinx.serialization.json.*
5 |
6 | @Serializable
7 | public data class MarketFaceBaseInfo(
8 | @SerialName("authorId")
9 | val authorId: Long,
10 | @SerialName("childMagicEmojiId")
11 | internal val childMagicEmojiId: String = "",
12 | @SerialName("desc")
13 | val description: String,
14 | @SerialName("favourite")
15 | @Serializable(with = NumberToBooleanSerializer::class)
16 | val favourite: Boolean = false,
17 | @SerialName("feeType")
18 | val feeType: Int,
19 | @SerialName("icon")
20 | val icon: Int,
21 | @SerialName("name")
22 | val name: String,
23 | @SerialName("providerId")
24 | val providerId: Long,
25 | @SerialName("qzone")
26 | internal val qzone: Int,
27 | @SerialName("realSize")
28 | val realSize: String,
29 | @SerialName("ringType")
30 | val ringType: Int,
31 | @SerialName("sex")
32 | val sex: Int = 0,
33 | @SerialName("sougou")
34 | internal val sougou: Int,
35 | @SerialName("tag")
36 | val tag: JsonElement,
37 | @SerialName("type")
38 | val type: Int,
39 | @SerialName("updateTipBeginTime")
40 | val updateTipBeginTime: String,
41 | @SerialName("updateTipEndTime")
42 | val updateTipEndTime: String,
43 | @SerialName("updateTipWording")
44 | val updateTipWording: String,
45 | @SerialName("valid")
46 | @Serializable(with = NumberToBooleanSerializer::class)
47 | val valid: Boolean,
48 | @SerialName("validArea")
49 | val validArea: Int,
50 | @SerialName("validBefore")
51 | val validBefore: String = "",
52 | @SerialName("isOriginal")
53 | @Serializable(NumberToBooleanSerializer::class)
54 | val isOriginal: Boolean = false,
55 | @SerialName("isApng")
56 | @Serializable(NumberToBooleanSerializer::class)
57 | val isAPNG: Boolean = false,
58 | @SerialName("QQgif")
59 | @Serializable(NumberToBooleanSerializer::class)
60 | val isGIF: Boolean = false,
61 | @SerialName("productId")
62 | val productId: String = "",
63 | @SerialName("label")
64 | val label: List = emptyList(),
65 | @SerialName("zip")
66 | internal val zip: JsonElement,
67 | @SerialName("__v")
68 | internal val __v: Int = 0,
69 | @SerialName("cfgID")
70 | internal val cfgID: String = "",
71 | @SerialName("_id")
72 | internal val _id: String = "",
73 | @SerialName("id")
74 | internal val id: String = ""
75 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/MarketFaceData.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class MarketFaceData(
7 | @SerialName("appData")
8 | val app: AppData,
9 | @SerialName("data")
10 | val detail: MarketFaceDetail,
11 | @SerialName("timestamp")
12 | val timestamp: Long? = null
13 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/MarketFaceDetail.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 | import kotlinx.serialization.json.*
5 |
6 | @Serializable
7 | public data class MarketFaceDetail(
8 | @SerialName("additionInfo")
9 | val addition: List,
10 | @SerialName("baseInfo")
11 | val base: List,
12 | @SerialName("md5Info")
13 | val md5: List,
14 | @SerialName("operationInfo")
15 | val operation: List = emptyList(),
16 | @SerialName("diyEmojiCommonText")
17 | val commons: List = emptyList(),
18 | @SerialName("diversionConfig")
19 | val diversions: List = emptyList(),
20 | @SerialName("__bgColors")
21 | internal val __bgColors: JsonElement = JsonNull,
22 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/MarketFaceHelper.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import io.ktor.client.call.*
4 | import io.ktor.client.request.*
5 | import io.ktor.client.statement.*
6 | import io.ktor.http.*
7 | import kotlinx.serialization.json.*
8 | import net.mamoe.mirai.*
9 | import net.mamoe.mirai.message.data.*
10 | import net.mamoe.mirai.utils.*
11 | import net.mamoe.mirai.internal.*
12 | import net.mamoe.mirai.internal.network.*
13 | import net.mamoe.mirai.internal.message.data.*
14 | import net.mamoe.mirai.internal.network.protocol.data.proto.*
15 | import xyz.cssxsh.mirai.meme.*
16 | import java.util.*
17 |
18 | @MiraiExperimentalApi
19 | @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
20 | public object MarketFaceHelper {
21 | @PublishedApi
22 | internal val json: Json = Json {
23 | isLenient = true
24 | ignoreUnknownKeys = true
25 | }
26 | @PublishedApi
27 | internal val authors: MutableMap = WeakHashMap()
28 | @PublishedApi
29 | internal val suppliers: MutableMap = WeakHashMap()
30 | @PublishedApi
31 | internal val relations: MutableMap = WeakHashMap()
32 | @PublishedApi
33 | internal val items: MutableMap = WeakHashMap()
34 | @PublishedApi
35 | internal val faces: MutableMap = WeakHashMap()
36 | @PublishedApi
37 | internal val faces2: MutableMap = WeakHashMap()
38 | @PublishedApi
39 | internal val defaultMobileParam: ByteArray = "72 73 63 54 79 70 65 3f 30 3b 76 61 6c 75 65 3d 30".hexToBytes()
40 | @PublishedApi
41 | internal val defaultPbReserve: ByteArray = "0A 06 08 AC 02 10 AC 02 0A 06 08 C8 01 10 C8 01 40 01".hexToBytes()
42 |
43 | public suspend fun queryAuthorDetail(authorId: Long): AuthorDetail {
44 | val cache = authors[authorId]
45 | if (cache != null) return cache
46 |
47 | val bot = Bot.instances.randomOrNull() ?: throw IllegalStateException("No Bot Instance")
48 | bot as QQAndroidBot
49 | val text = http.get("https://open.vip.qq.com/open/getAuthorDetail") {
50 | parameter("authorId", authorId)
51 | parameter("g_tk", bot.client.wLoginSigInfo.bkn)
52 |
53 | headers {
54 | append(
55 | "cookie",
56 | "uin=o${bot.id}; skey=${bot.sKey}; p_uin=o${bot.id}; p_skey=${bot.psKey("vip.qq.com")};"
57 | )
58 | }
59 | }.bodyAsText()
60 |
61 | val result = json.decodeFromString(Restful.serializer(), text)
62 |
63 | check(result.ret == 0) { result.message }
64 |
65 | val detail = json.decodeFromJsonElement(AuthorDetail.serializer(), result.data)
66 | authors[authorId] = detail
67 | return detail
68 | }
69 |
70 | public suspend fun queryRelationId(itemId: Int): RelationIdInfo {
71 | val cache = relations[itemId]
72 | if (cache != null) return cache
73 |
74 | val bot = Bot.instances.randomOrNull() ?: throw IllegalStateException("No Bot Instance")
75 | bot as QQAndroidBot
76 | val text = http.get("https://open.vip.qq.com/open/getRelationId") {
77 | parameter("appId", "1")
78 | parameter("adminItemId", itemId)
79 | parameter("g_tk", bot.client.wLoginSigInfo.bkn)
80 |
81 | headers {
82 | append(
83 | "cookie",
84 | "uin=o${bot.id}; skey=${bot.sKey}; p_uin=o${bot.id}; p_skey=${bot.psKey("vip.qq.com")};"
85 | )
86 | }
87 | }.bodyAsText()
88 |
89 | val result = json.decodeFromString(Restful.serializer(), text)
90 |
91 | check(result.ret == 0) { result.message }
92 | val info = json.decodeFromJsonElement(RelationIdInfo.serializer(), result.data)
93 | relations[itemId] = info
94 | return info
95 | }
96 |
97 | public suspend fun queryRelationId(face: MarketFace): RelationIdInfo = queryRelationId(itemId = face.id)
98 |
99 | public suspend fun queryFaceDetail(itemId: Int): MarketFaceData {
100 | val cache = faces[itemId]
101 | if (cache != null) return cache
102 |
103 | val text = http.get("https://gxh.vip.qq.com/qqshow/admindata/comdata/vipEmoji_item_${itemId}/xydata.json")
104 | .bodyAsText()
105 |
106 | val data = json.decodeFromString(MarketFaceData.serializer(), text)
107 | faces[itemId] = data
108 | return data
109 | }
110 |
111 | public suspend fun queryFaceDetail(face: MarketFace): MarketFaceData = queryFaceDetail(itemId = face.id)
112 |
113 | public suspend fun queryFaceAndroid(itemId: Int): MarketFaceAndroid {
114 | val cache = faces2[itemId]
115 | if (cache != null) return cache
116 |
117 | val text = http.get("https://gxh.vip.qq.com/club/item/parcel/${itemId % 10}/${itemId}_android.json")
118 | .bodyAsText()
119 |
120 | val data = json.decodeFromString(MarketFaceAndroid.serializer(), text)
121 | faces2[itemId] = data
122 | return data
123 | }
124 |
125 | public suspend fun queryFaceAndroid(face: MarketFace): MarketFaceAndroid = queryFaceAndroid(itemId = face.id)
126 |
127 | public suspend fun queryItemData(itemId: Int): ItemData {
128 | val cache = items[itemId]
129 | if (cache != null) return cache
130 |
131 | val bot = Bot.instances.randomOrNull() ?: throw IllegalStateException("No Bot Instance")
132 | bot as QQAndroidBot
133 | val text = http.get("https://zb.vip.qq.com/v2/home/cgi/getItemData") {
134 | parameter("bid", 1)
135 | parameter("id", itemId)
136 | parameter("g_tk", bot.client.wLoginSigInfo.bkn)
137 |
138 | headers {
139 | append(
140 | "cookie",
141 | "uin=o${bot.id}; skey=${bot.sKey}; p_uin=o${bot.id}; p_skey=${bot.psKey("vip.qq.com")};"
142 | )
143 | }
144 | }.bodyAsText()
145 |
146 | val result = json.decodeFromString(Restful.serializer(), text)
147 |
148 | check(result.ret == 0) { result.message }
149 | val data = json.decodeFromJsonElement(ItemData.serializer(), result.data)
150 | items[itemId] = data
151 | return data
152 | }
153 |
154 | public suspend fun queryItemData(face: MarketFace): ItemData = queryItemData(itemId = face.id)
155 |
156 | public suspend fun querySupplierInfo(supplierId: Long, offset: Int): SupplierInfo {
157 | val cache = suppliers[supplierId]
158 | if (cache != null) return cache
159 |
160 | val bot = Bot.instances.randomOrNull() ?: throw IllegalStateException("No Bot Instance")
161 | bot as QQAndroidBot
162 | val text = http.post {
163 | url("https://zb.vip.qq.com/trpc-proxy/qc/supplyerservice/Supplyer/SupplyerInfo")
164 |
165 | setBody(buildJsonObject {
166 | putJsonObject("req") {
167 | put("supplyerid", supplierId)
168 | // offset
169 | put("nextID", offset)
170 | // page size
171 | put("Pagesize", 30)
172 | putJsonObject("stlogin") {
173 | put("ikeytype", 1)
174 | put("iKeyType", 1)
175 | put("iopplat", 2)
176 | put("iOpplat", 2)
177 | put("sClientIp", "")
178 | put("sclientver", "8.9.28")
179 | put("sClientVer", "8.9.28")
180 | put("skey", bot.sKey)
181 | put("sSkey", bot.sKey)
182 | put("lLoginInfo", "")
183 | }
184 | }
185 | putJsonObject("options") {
186 | putJsonObject("context") {
187 | put("businessType", "qqgxh")
188 | }
189 | putJsonObject("naming") {
190 | put("namespace", "Production")
191 | put("env", "formal")
192 | }
193 | }
194 | }.toString())
195 | contentType(ContentType.Application.Json)
196 |
197 | headers {
198 | append(
199 | "cookie",
200 | "uin=o${bot.id}; skey=${bot.sKey}; p_uin=o${bot.id}; p_skey=${bot.psKey("vip.qq.com")};"
201 | )
202 | }
203 | }.bodyAsText()
204 |
205 | val result = json.decodeFromString(Restful.serializer(), text)
206 |
207 | check(result.ret == 0) { result.message }
208 | val info = json.decodeFromJsonElement(SupplierInfo.serializer(), result.data)
209 | suppliers[supplierId] = info
210 | return info
211 | }
212 |
213 | public fun build(itemId: Int, data: MarketFaceData): List {
214 | val info = data.detail.base.single()
215 | val timestamp = requireNotNull(data.timestamp) { "Not Found Timestamp" } / 1_000
216 | val key = timestamp.toString().md5()
217 | .toUHexString("")
218 | .lowercase().substring(0, 16)
219 | .toByteArray()
220 |
221 | return data.detail.md5.map { (md5, name) ->
222 | val delegate = ImMsgBody.MarketFace(
223 | faceName = "[$name]".toByteArray(),
224 | itemType = 6,
225 | faceInfo = 1,
226 | faceId = md5.hexToBytes(),
227 | tabId = itemId,
228 | subType = 3,
229 | mediaType = info.ringType,
230 | key = key,
231 | imageWidth = 200,
232 | imageHeight = 200,
233 | pbReserve = defaultPbReserve
234 | )
235 |
236 | MarketFaceImpl(delegate = delegate)
237 | }
238 | }
239 |
240 | public fun build(data: MarketFaceAndroid): List {
241 | val key = data.updateTime.toString().md5()
242 | .toUHexString("")
243 | .lowercase().substring(0, 16)
244 | .toByteArray()
245 |
246 | return data.images.map { image ->
247 | val delegate = ImMsgBody.MarketFace(
248 | faceName = "[${image.name}]".toByteArray(),
249 | itemType = 6,
250 | faceInfo = 1,
251 | faceId = image.id.hexToBytes(),
252 | tabId = data.id.toInt(),
253 | subType = 3,
254 | mediaType = data.ringType.toIntOrNull() ?: 0,
255 | key = key,
256 | imageWidth = image.width,
257 | imageHeight = image.height,
258 | mobileParam = if (image.param.isEmpty()) EMPTY_BYTE_ARRAY else defaultMobileParam,
259 | pbReserve = defaultPbReserve
260 | )
261 |
262 | MarketFaceImpl(delegate = delegate)
263 | }
264 | }
265 |
266 | public suspend fun source(impl: MarketFace): ByteArray {
267 | impl as MarketFaceImpl
268 | val md5 = impl.delegate.faceId.toUHexString("").lowercase()
269 | val size = if (impl.delegate.tabId < 10_0000) 200 else 300
270 | val url = when (impl.delegate.subType) {
271 | 1 -> "https://gxh.vip.qq.com/club/item/parcel/item/${md5.substring(0, 2)}/$md5/raw${size}.gif"
272 | 2 -> "https://gxh.vip.qq.com/club/item/parcel/item/${md5.substring(0, 2)}/$md5/raw${size}.png"
273 | 3 -> "https://gxh.vip.qq.com/club/item/parcel/item/${md5.substring(0, 2)}/$md5/raw${size}.gif"
274 | else -> "https://gxh.vip.qq.com/club/item/parcel/item/${md5.substring(0, 2)}/$md5/${size}x${size}.png"
275 | }
276 |
277 | return http.get(url).body()
278 | }
279 |
280 | public fun detail(face: MarketFace): String = "https://zb.vip.qq.com/hybrid/emoticonmall/detail?id=${face.id}"
281 | }
--------------------------------------------------------------------------------
/src/main/kotlin/face/MarketFaceItem.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class MarketFaceItem(
7 | @SerialName("appId")
8 | val appId: Long,
9 | @SerialName("desc")
10 | val description: String,
11 | @SerialName("feeType")
12 | val feeType: Int,
13 | @SerialName("itemId")
14 | val itemId: Int,
15 | @SerialName("itemName")
16 | val name: String,
17 | @SerialName("thumbImg")
18 | val thumb: String
19 | ) {
20 | public val url: String by lazy { "https://zb.vip.qq.com/hybrid/emoticonmall/detail?id=${itemId}" }
21 | }
--------------------------------------------------------------------------------
/src/main/kotlin/face/NumberToBooleanSerializer.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 | import kotlinx.serialization.descriptors.*
5 | import kotlinx.serialization.encoding.*
6 |
7 | @PublishedApi
8 | internal object NumberToBooleanSerializer : KSerializer {
9 |
10 | override val descriptor: SerialDescriptor =
11 | PrimitiveSerialDescriptor("NumberToBooleanSerializer", PrimitiveKind.BOOLEAN)
12 |
13 | override fun deserialize(decoder: Decoder): Boolean = decoder.decodeLong() != 0L
14 |
15 | override fun serialize(encoder: Encoder, value: Boolean) = encoder.encodeLong(if (value) 1L else 0L)
16 | }
--------------------------------------------------------------------------------
/src/main/kotlin/face/OperationInfo.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class OperationInfo(
7 | @SerialName("isFree")
8 | @Serializable(with = NumberToBooleanSerializer::class)
9 | val isFree: Boolean = true,
10 | @SerialName("isShow")
11 | @Serializable(with = NumberToBooleanSerializer::class)
12 | val isShow: Boolean = true,
13 | @SerialName("limitBeginTime")
14 | val limitBeginTime: String = "",
15 | @SerialName("limitEndTime")
16 | val limitEndTime: String = "",
17 | @SerialName("limitFreeBeginTime")
18 | val limitFreeBeginTime: String = "",
19 | @SerialName("limitFreeEndTime")
20 | val limitFreeEndTime: String = "",
21 | @SerialName("limitType")
22 | val limitType: Int = 0,
23 | @SerialName("maxVersion")
24 | val maxVersion: String,
25 | @SerialName("minVersion")
26 | val minVersion: String,
27 | @SerialName("platform")
28 | val platform: Int,
29 | @SerialName("price")
30 | val price: Double = 0.0,
31 | @SerialName("productId")
32 | val productId: String = "",
33 | @SerialName("valid")
34 | @Serializable(with = NumberToBooleanSerializer::class)
35 | val valid: Boolean = true,
36 | @SerialName("validity")
37 | val validity: Int = 0,
38 | @SerialName("whiteList")
39 | internal val white: String = "",
40 | @SerialName("__v")
41 | internal val __v: Int = 0,
42 | @SerialName("cfgID")
43 | internal val cfgID: String = "",
44 | @SerialName("_id")
45 | internal val _id: String = "",
46 | @SerialName("id")
47 | internal val id: String = ""
48 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/RelationIdInfo.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 |
5 | @Serializable
6 | public data class RelationIdInfo(
7 | @SerialName("authorHeadImg")
8 | val head: String,
9 | @SerialName("authorId")
10 | val authorId: Long,
11 | @SerialName("authorName")
12 | val name: String,
13 | @SerialName("openItemId")
14 | val itemId: Int
15 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/Restful.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 | import kotlinx.serialization.json.*
5 |
6 | @Serializable
7 | @OptIn(ExperimentalSerializationApi::class)
8 | internal data class Restful(
9 | @JsonNames("data", "response")
10 | val data: JsonElement = JsonNull,
11 | @JsonNames("msg", "message")
12 | val message: String = "",
13 | @JsonNames("ret", "retCode")
14 | val ret: Int = 0,
15 | @SerialName("code")
16 | val code: Int = 200
17 | )
--------------------------------------------------------------------------------
/src/main/kotlin/face/SupplierInfo.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.face
2 |
3 | import kotlinx.serialization.*
4 | import kotlinx.serialization.json.*
5 |
6 | @Serializable
7 | public data class SupplierInfo(
8 | @SerialName("accountLevel")
9 | val accountLevel: Int = 0,
10 | @SerialName("commrsp")
11 | val comm: JsonElement = JsonNull,
12 | @SerialName("dyhAppid")
13 | val appId: String = "",
14 | @SerialName("fansnum")
15 | val fansNum: Int = 0,
16 | @SerialName("isfance")
17 | @Serializable(with = NumberToBooleanSerializer::class)
18 | val isFans: Boolean = false,
19 | @SerialName("nextID")
20 | val offset: Int = 0,
21 | @SerialName("puin")
22 | val uin: String = "",
23 | @SerialName("qqGroup")
24 | val group: Long = 0,
25 | @SerialName("rptOpenitem")
26 | val items: List = emptyList(),
27 | @SerialName("supplyerdes")
28 | val description: String = "",
29 | @SerialName("supplyerface")
30 | val face: String = "",
31 | @SerialName("supplyername")
32 | val name: String = "",
33 | @SerialName("worknum")
34 | val workNum: Int = 0
35 | )
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemeBiliBiliEmote.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import net.mamoe.mirai.console.permission.*
4 | import net.mamoe.mirai.event.events.*
5 | import net.mamoe.mirai.message.data.*
6 | import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
7 | import xyz.cssxsh.mirai.bilibili.data.*
8 | import xyz.cssxsh.mirai.meme.*
9 | import xyz.cssxsh.mirai.meme.service.*
10 | import java.io.*
11 | import java.util.*
12 |
13 | public class MemeBiliBiliEmote : MemeService {
14 | override val name: String = "BiliBili Emote"
15 | override val id: String = "bilibili"
16 | override val description: String = "获取B站表情包"
17 | override var loaded: Boolean = true
18 | override var regex: Regex = """^#B表情""".toRegex()
19 | private set
20 | override val properties: Properties = Properties().apply { put("regex", regex.pattern) }
21 | override lateinit var permission: Permission
22 | private var folder: File = File(System.getProperty("user.dir", ".")).resolve(".bilibili")
23 |
24 | override fun load(folder: File) {
25 | this.folder = folder
26 | when (val re = properties["regex"]) {
27 | is String -> regex = re.toRegex()
28 | is Regex -> regex = re
29 | else -> {}
30 | }
31 | loaded = try {
32 | BiliEmoteData.dynamic
33 | true
34 | } catch (_: Throwable) {
35 | false
36 | }
37 |
38 | }
39 |
40 | override fun enable(permission: Permission) {
41 | this.permission = permission
42 | }
43 |
44 | override fun disable() {}
45 |
46 | override suspend fun MessageEvent.replier(match: MatchResult): MessageChain? {
47 | val content = message.content
48 | val emotes = buildList {
49 | for ((text, item) in BiliEmoteData.dynamic) {
50 | if ("<$text>" in content) {
51 | addAll(item.emote)
52 | continue
53 | }
54 | for (emote in item.emote) {
55 | if (emote.text in content) add(emote)
56 | }
57 | }
58 | }
59 | if (emotes.isEmpty()) return null
60 | return buildMessageChain {
61 | for (emote in emotes) {
62 | val file = folder.resolve(emote.url.substringAfterLast('/')).takeIf { it.exists() }
63 | ?: download(urlString = emote.url, folder = folder)
64 | +file.uploadAsImage(contact = subject)
65 | }
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemeChoYen.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import net.mamoe.mirai.console.permission.*
4 | import net.mamoe.mirai.event.events.*
5 | import net.mamoe.mirai.message.data.*
6 | import xyz.cssxsh.mirai.meme.service.*
7 | import xyz.cssxsh.mirai.skia.*
8 | import xyz.cssxsh.skia.*
9 | import java.io.*
10 | import java.util.*
11 |
12 | /**
13 | * [5000choyen](https://github.com/yurafuca/5000choyen)
14 | */
15 | public class MemeChoYen : MemeService {
16 | override val name: String = "5000兆円"
17 | override val id: String = "choyen"
18 | override val description: String = "5000兆円 生成器"
19 | override val loaded: Boolean = true
20 | override var regex: Regex = """^#choyen\s+(\S+)\s+(\S+)""".toRegex()
21 | private set
22 | override val properties: Properties = Properties().apply { put("regex", regex.pattern) }
23 | override lateinit var permission: Permission
24 |
25 | override fun load(folder: File) {
26 | when (val re = properties["regex"]) {
27 | is String -> regex = re.toRegex()
28 | is Regex -> regex = re
29 | else -> {}
30 | }
31 | }
32 |
33 | override fun enable(permission: Permission) {
34 | this.permission = permission
35 | }
36 |
37 | override fun disable() {}
38 |
39 | override suspend fun MessageEvent.replier(match: MatchResult): Image {
40 | val (top, bottom) = match.destructured
41 |
42 | return choyen(top, bottom).makeSnapshotResource()
43 | .use { resource -> subject.uploadImage(resource = resource) }
44 | }
45 | }
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemeDear.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import kotlinx.coroutines.*
4 | import net.mamoe.mirai.console.permission.*
5 | import net.mamoe.mirai.event.events.*
6 | import net.mamoe.mirai.message.data.*
7 | import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
8 | import xyz.cssxsh.mirai.meme.*
9 | import xyz.cssxsh.mirai.meme.service.*
10 | import xyz.cssxsh.skia.*
11 | import java.io.*
12 | import java.util.*
13 |
14 | public class MemeDear : MemeService {
15 | override val name: String = "Dear"
16 | override val id: String = "dear"
17 | override val description: String = "亲亲 生成器"
18 | override val loaded: Boolean = true
19 | override var regex: Regex = """^#dear\s*(\d+)?""".toRegex()
20 | private set
21 | override val properties: Properties = Properties().apply { put("regex", regex.pattern) }
22 | override lateinit var permission: Permission
23 | private lateinit var loadJob: Job
24 |
25 | override fun load(folder: File) {
26 | when (val re = properties["regex"]) {
27 | is String -> regex = re.toRegex()
28 | is Regex -> regex = re
29 | else -> {}
30 | }
31 | loadJob = MemeService.launch(CoroutineName(name)) {
32 | val dear = folder.resolve("dear.gif")
33 | if (dear.exists().not()) {
34 | download(urlString = "https://tva3.sinaimg.cn/large/003MWcpMly8gv4s019bzsg606o06o40902.gif", folder)
35 | .renameTo(dear)
36 | }
37 | System.setProperty(DEAR_ORIGIN, dear.absolutePath)
38 | }
39 | }
40 |
41 | override fun enable(permission: Permission) {
42 | this.permission = permission
43 | runBlocking {
44 | loadJob.join()
45 | }
46 | }
47 |
48 | override fun disable() {}
49 |
50 | override suspend fun MessageEvent.replier(match: MatchResult): Image {
51 | val id = match.groups[1]?.value?.toLongOrNull()
52 | ?: message.findIsInstance()?.target
53 | ?: sender.id
54 | val image = message.findIsInstance()
55 | val face = when {
56 | image != null -> cache(image = image)
57 | else -> avatar(id = id)
58 | }
59 |
60 | return dear(face).uploadAsImage(subject)
61 | }
62 | }
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemeEmojiKitchen.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import kotlinx.coroutines.*
4 | import kotlinx.serialization.*
5 | import kotlinx.serialization.json.*
6 | import net.mamoe.mirai.console.permission.*
7 | import net.mamoe.mirai.event.events.*
8 | import net.mamoe.mirai.message.data.*
9 | import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
10 | import xyz.cssxsh.mirai.meme.*
11 | import xyz.cssxsh.mirai.meme.service.*
12 | import java.io.*
13 | import java.time.*
14 | import java.util.*
15 | import java.util.zip.*
16 | import kotlin.collections.*
17 |
18 | /**
19 | * [Emoji Kitchen](https://github.com/xsalazar/emoji-kitchen/)
20 | */
21 | public class MemeEmojiKitchen : MemeService {
22 | override val name: String = "Emoji Kitchen"
23 | override val id: String = "emoji"
24 | override val description: String = "Emoji 合成"
25 | override val loaded: Boolean = true
26 | override val regex: Regex = EmojiKitchen.EMOJI_REGEX.toRegex()
27 | override val properties: Properties = Properties()
28 | override lateinit var permission: Permission
29 | private var kitchen: EmojiKitchen = EmojiKitchen(items = emptyMap())
30 | private var folder: File = File(System.getProperty("user.dir", ".")).resolve(".emoji")
31 | private lateinit var loadJob: Job
32 |
33 | override fun load(folder: File) {
34 | this.folder = folder
35 | loadJob = MemeService.launch(CoroutineName(name)) {
36 | folder.mkdirs()
37 |
38 | val archive = folder.resolve("emoji-kitchen-main.zip")
39 | if (OffsetDateTime.parse(EmojiKitchen.LAST_UPDATE).toInstant().toEpochMilli() > archive.lastModified()) {
40 | archive.delete()
41 | }
42 | if (archive.exists().not()) {
43 | try {
44 | download(
45 | urlString = "https://mirror.ghproxy.com/https://github.com/xsalazar/emoji-kitchen-backend/archive/main.zip",
46 | folder = folder
47 | )
48 | } catch (_: Exception) {
49 | archive.delete()
50 | download(
51 | urlString = "https://github.com/xsalazar/emoji-kitchen-backend/archive/main.zip",
52 | folder = folder
53 | )
54 | }.renameTo(archive)
55 | }
56 | val metadata = ZipFile(archive).use { zip ->
57 | val entry = zip.getEntry("emoji-kitchen-backend-main/app/metadata.json")
58 | @OptIn(ExperimentalSerializationApi::class)
59 | Json.decodeFromStream(stream = zip.getInputStream(entry))
60 | }
61 | kitchen = EmojiKitchen(items = metadata.data)
62 | }
63 | }
64 |
65 | override fun enable(permission: Permission) {
66 | this.permission = permission
67 | runBlocking {
68 | loadJob.join()
69 | }
70 | }
71 |
72 | override fun disable() {}
73 |
74 | override suspend fun MessageEvent.replier(match: MatchResult): Message? {
75 | val first = match.value
76 | val second = match.next()?.value ?: return null
77 | val (filename, url) = kitchen.cook(first, second)
78 | ?: kitchen.cook(second, first)
79 | ?: return null
80 |
81 | return (folder.resolve(filename).takeIf { it.exists() } ?: download(urlString = url, folder = folder))
82 | .uploadAsImage(contact = subject)
83 | }
84 | }
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemePetPet.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import kotlinx.coroutines.*
4 | import net.mamoe.mirai.console.permission.*
5 | import net.mamoe.mirai.event.events.*
6 | import net.mamoe.mirai.message.data.*
7 | import xyz.cssxsh.mirai.meme.*
8 | import xyz.cssxsh.mirai.meme.service.*
9 | import xyz.cssxsh.mirai.skia.*
10 | import xyz.cssxsh.skia.*
11 | import java.io.*
12 | import java.util.*
13 |
14 | /**
15 | * [PetPet](https://benisland.neocities.org/petpet)
16 | */
17 | public class MemePetPet : MemeService {
18 | override val name: String = "PetPet Meme"
19 | override val id: String = "petpet"
20 | override val description: String = "摸摸头 生成器"
21 | override val loaded: Boolean = true
22 | override var regex: Regex = """^#pet\s*(\d+)?""".toRegex()
23 | private set
24 | override val properties: Properties = Properties().apply { put("regex", regex.pattern) }
25 | override lateinit var permission: Permission
26 | private lateinit var loadJob: Job
27 |
28 | override fun load(folder: File) {
29 | when (val re = properties["regex"]) {
30 | is String -> regex = re.toRegex()
31 | is Regex -> regex = re
32 | else -> {}
33 | }
34 | loadJob = MemeService.launch(CoroutineName(name)) {
35 | folder.mkdirs()
36 |
37 | val sprite = folder.resolve("sprite.png")
38 | if (sprite.exists().not()) {
39 | download(urlString = "https://benisland.neocities.org/petpet/img/sprite.png", folder)
40 | .renameTo(sprite)
41 | }
42 | System.setProperty(PET_PET_SPRITE, sprite.absolutePath)
43 | }
44 | }
45 |
46 | override fun enable(permission: Permission) {
47 | this.permission = permission
48 | runBlocking {
49 | loadJob.join()
50 | }
51 | }
52 |
53 | override fun disable() {}
54 |
55 | override suspend fun MessageEvent.replier(match: MatchResult): Image {
56 | val id = match.groups[1]?.value?.toLongOrNull()
57 | ?: message.findIsInstance()?.target
58 | ?: sender.id
59 | val image = message.findIsInstance()
60 | val face = when {
61 | image != null -> cache(image = image)
62 | else -> avatar(id = id)
63 | }
64 |
65 | return petpet(face).toImageResource(formatName = "gif")
66 | .use { resource -> subject.uploadImage(resource = resource) }
67 | }
68 | }
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemePornHub.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import net.mamoe.mirai.console.permission.*
4 | import net.mamoe.mirai.event.events.*
5 | import net.mamoe.mirai.message.data.*
6 | import xyz.cssxsh.mirai.meme.service.*
7 | import xyz.cssxsh.mirai.skia.*
8 | import xyz.cssxsh.skia.*
9 | import java.io.*
10 | import java.util.*
11 |
12 | public class MemePornHub : MemeService {
13 | override val name: String = "PornHub Logo"
14 | override val id: String = "pornhub"
15 | override val description: String = "PornHub Logo 生成器"
16 | override val loaded: Boolean = true
17 | override var regex: Regex = """^#ph\s+(\S+)\s+(\S+)""".toRegex()
18 | private set
19 | override val properties: Properties = Properties().apply { put("regex", regex.pattern) }
20 | override lateinit var permission: Permission
21 |
22 | override fun load(folder: File) {
23 | when (val re = properties["regex"]) {
24 | is String -> regex = re.toRegex()
25 | is Regex -> regex = re
26 | else -> {}
27 | }
28 | }
29 |
30 | override fun enable(permission: Permission) {
31 | this.permission = permission
32 | }
33 |
34 | override fun disable() {}
35 |
36 | override suspend fun MessageEvent.replier(match: MatchResult): Image {
37 | val (porn, hub) = match.destructured
38 |
39 | return pornhub(porn, hub).makeSnapshotResource()
40 | .use { resource -> subject.uploadImage(resource = resource) }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemeProjectSekaiStickers.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import kotlinx.coroutines.*
4 | import net.mamoe.mirai.console.permission.*
5 | import net.mamoe.mirai.event.events.*
6 | import net.mamoe.mirai.message.data.*
7 | import xyz.cssxsh.mirai.meme.*
8 | import xyz.cssxsh.mirai.meme.service.*
9 | import xyz.cssxsh.mirai.skia.*
10 | import java.io.*
11 | import java.util.*
12 | import kotlin.collections.*
13 |
14 | /**
15 | * [Project Sekai Stickers](https://st.ayaka.one/)
16 | */
17 | public class MemeProjectSekaiStickers : MemeService {
18 | override val name: String = "Project Sekai Stickers"
19 | override val id: String = "sekai-stickers"
20 | override val description: String = "Stickers 合成"
21 | override val loaded: Boolean = true
22 | override lateinit var regex: Regex
23 | override val properties: Properties = Properties()
24 | override lateinit var permission: Permission
25 | private lateinit var tool: ProjectSekaiStickers
26 | private var folder: File = File(System.getProperty("user.dir", ".")).resolve(".sekai-stickers")
27 | private lateinit var loadJob: Job
28 |
29 | override fun load(folder: File) {
30 | this.folder = folder
31 | loadJob = MemeService.launch(CoroutineName(name)) {
32 | folder.mkdirs()
33 |
34 | val archive = folder.resolve("sekai-stickers-main.zip")
35 | if (archive.exists().not()) {
36 | try {
37 | download(
38 | urlString = "https://mirror.ghproxy.com/https://github.com/TheOriginalAyaka/sekai-stickers/archive/main.zip",
39 | folder = folder
40 | )
41 | } catch (_: Exception) {
42 | archive.delete()
43 | download(
44 | urlString = "https://github.com/TheOriginalAyaka/sekai-stickers/archive/main.zip",
45 | folder = folder
46 | )
47 | }.renameTo(archive)
48 | }
49 | tool = ProjectSekaiStickers(file = archive)
50 | regex = tool.characters
51 | .joinToString(prefix = "#(", separator = "|", postfix = ")\\s*(.*)") { it.name }
52 | .toRegex()
53 | }
54 | }
55 |
56 | override fun enable(permission: Permission) {
57 | this.permission = permission
58 | runBlocking {
59 | loadJob.join()
60 | }
61 | }
62 |
63 | override fun disable() {}
64 |
65 | override suspend fun MessageEvent.replier(match: MatchResult): Message {
66 | val (name, text) = match.destructured
67 | val image = tool.create(name = name) {
68 | if (text.isNotBlank()) this.text = text
69 | }
70 |
71 | return image.makeSnapshotResource()
72 | .use { resource -> subject.uploadImage(resource = resource) }
73 | }
74 | }
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemeRecord.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import io.ktor.client.request.*
4 | import io.ktor.client.statement.*
5 | import io.ktor.http.*
6 | import io.ktor.utils.io.jvm.javaio.*
7 | import net.mamoe.mirai.console.permission.*
8 | import net.mamoe.mirai.event.events.*
9 | import net.mamoe.mirai.message.data.*
10 | import net.mamoe.mirai.message.data.Image.Key.isUploaded
11 | import net.mamoe.mirai.utils.*
12 | import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
13 | import xyz.cssxsh.mirai.hibernate.*
14 | import xyz.cssxsh.mirai.hibernate.entry.*
15 | import xyz.cssxsh.mirai.meme.*
16 | import xyz.cssxsh.mirai.meme.service.*
17 | import java.io.*
18 | import java.util.*
19 |
20 | public class MemeRecord : MemeService {
21 | override val name: String = "Face Record"
22 | override val id: String = "record"
23 | override val description: String = "从群聊记录里获得表情包"
24 | override var loaded: Boolean = true
25 | override var regex: Regex = """^#群友表情\s*(.*)|#([^#\s]+)#""".toRegex()
26 | private set
27 | override val properties: Properties = Properties().apply { put("regex", regex.pattern) }
28 | override lateinit var permission: Permission
29 | private var folder: File = File(System.getProperty("user.dir", ".")).resolve(".record")
30 |
31 | private suspend fun FaceRecord.download(): File {
32 | return folder.listFiles { file -> file.name.startsWith(md5) }?.firstOrNull()
33 | ?: http.prepareGet(url).execute { response ->
34 | val type = response.contentType()?.contentSubtype ?: "mirai"
35 |
36 | val file = folder.resolve("$md5.$type")
37 |
38 | logger.info { "文件 ${file.name} 开始下载" }
39 | file.outputStream().use { output ->
40 | val channel = response.bodyAsChannel()
41 |
42 | while (!channel.isClosedForRead) channel.copyTo(output)
43 | }
44 |
45 | file
46 | }
47 | }
48 |
49 | override fun load(folder: File) {
50 | this.folder = folder
51 | loaded = try {
52 | MiraiHibernateRecorder
53 | true
54 | } catch (_: Throwable) {
55 | false
56 | }
57 | }
58 |
59 | override fun enable(permission: Permission) {
60 | this.permission = permission
61 | }
62 |
63 | override fun disable() {}
64 |
65 | override suspend fun MessageEvent.replier(match: MatchResult): MessageContent? {
66 | val tag = match.groupValues.last { it.isNotEmpty() }
67 | val record = if (tag.startsWith('#')) {
68 | FaceRecord.random()
69 | } else {
70 | FaceRecord.match(tag = tag).randomOrNull()
71 | ?: MiraiHibernateRecorder.face(md5 = tag)
72 | ?: return null
73 | }
74 | val message = record.toMessageContent()
75 | if (message is Image && message.isUploaded(bot).not()) {
76 | return record.download().uploadAsImage(subject)
77 | }
78 | return message
79 | }
80 | }
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemeSchool.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import org.jetbrains.skia.Image as SkImage
4 | import net.mamoe.mirai.console.permission.*
5 | import net.mamoe.mirai.event.events.*
6 | import net.mamoe.mirai.message.data.*
7 | import org.jetbrains.skia.*
8 | import org.jetbrains.skia.paragraph.*
9 | import xyz.cssxsh.mirai.meme.service.*
10 | import xyz.cssxsh.mirai.skia.*
11 | import xyz.cssxsh.skia.*
12 | import java.io.File
13 | import java.util.*
14 |
15 | public class MemeSchool : MemeService {
16 | override val name: String = "学历"
17 | override val id: String = "school"
18 | override val description: String = "学历 生成器"
19 | override val loaded: Boolean = true
20 | override val properties: Properties = Properties()
21 | override lateinit var permission: Permission
22 | override val regex: Regex = """^#学历""".toRegex()
23 |
24 | private val background by lazy {
25 | SkImage.makeFromEncoded(this::class.java.getResource("record.png")!!.readBytes())
26 | }
27 |
28 | override suspend fun MessageEvent.replier(match: MatchResult): Message {
29 | val surface = Surface.makeRaster(background.imageInfo)
30 | val canvas = surface.canvas
31 | canvas.drawImage(background, 0F, 0F)
32 |
33 | val fonts = FontCollection()
34 | .setDynamicFontManager(FontUtils.provider)
35 | .setDefaultFontManager(FontMgr.default, "黑体")
36 | val style = ParagraphStyle().apply {
37 | direction = Direction.RTL
38 | }
39 | message.contentToString()
40 | .removePrefix("#学历\n")
41 | .lines()
42 | .forEachIndexed { index, line ->
43 | val paragraph = ParagraphBuilder(style, fonts)
44 | .pushStyle(
45 | TextStyle()
46 | .setFontSize(42F)
47 | .setColor(0xFF979797u.toInt()))
48 | .addText(line)
49 | .build()
50 | .layout(1000F)
51 | paragraph.paint(canvas, 10F, 450F + index * 138)
52 | }
53 | return surface.makeSnapshotResource().use { subject.uploadImage(it) }
54 | }
55 |
56 | override fun load(folder: File) {}
57 |
58 | override fun enable(permission: Permission) {
59 | this.permission = permission
60 | }
61 |
62 | override fun disable() {}
63 | }
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemeTank.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import net.mamoe.mirai.console.permission.*
4 | import net.mamoe.mirai.console.util.*
5 | import net.mamoe.mirai.event.events.*
6 | import net.mamoe.mirai.message.data.*
7 | import xyz.cssxsh.mirai.meme.*
8 | import xyz.cssxsh.mirai.meme.service.*
9 | import xyz.cssxsh.mirai.skia.*
10 | import xyz.cssxsh.skia.*
11 | import java.io.*
12 | import java.util.*
13 |
14 | /**
15 | * [幻影坦克](https://samarium150.github.io/mirage-tank-images/)
16 | */
17 | public class MemeTank : MemeService {
18 | override val name: String = "幻影坦克"
19 | override val id: String = "tank"
20 | override val description: String = "幻影坦克 生成器"
21 | override var loaded: Boolean = false
22 | override var regex: Regex = """^#tank""".toRegex()
23 | private set
24 | override val properties: Properties = Properties().apply { put("regex", regex.pattern) }
25 | override lateinit var permission: Permission
26 |
27 | override fun load(folder: File) {
28 | when (val re = properties["regex"]) {
29 | is String -> regex = re.toRegex()
30 | is Regex -> regex = re
31 | else -> {}
32 | }
33 |
34 | loaded = SemVersion.parseRangeRequirement(">= 0.7.32")
35 | .test(SemVersion(org.jetbrains.skiko.Version.skiko))
36 | }
37 |
38 | override fun enable(permission: Permission) {
39 | this.permission = permission
40 | }
41 |
42 | override fun disable() {}
43 |
44 | override suspend fun MessageEvent.replier(match: MatchResult): Image? {
45 | val images = message.filterIsInstance()
46 | if (images.size < 2) return null
47 | val top = cache(image = images[0])
48 | val bottom = cache(image = images[1])
49 |
50 | return tank(top, bottom).makeSnapshotResource()
51 | .use { resource -> subject.uploadImage(resource = resource) }
52 | }
53 | }
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemeWeiboEmoticon.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import net.mamoe.mirai.console.permission.*
4 | import net.mamoe.mirai.event.events.*
5 | import net.mamoe.mirai.message.data.*
6 | import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
7 | import xyz.cssxsh.mirai.meme.*
8 | import xyz.cssxsh.mirai.meme.service.*
9 | import xyz.cssxsh.mirai.weibo.data.*
10 | import java.io.*
11 | import java.util.*
12 |
13 | public class MemeWeiboEmoticon : MemeService {
14 | override val name: String = "Weibo Emoticon"
15 | override val id: String = "weibo"
16 | override val description: String = "获取微博表情包"
17 | override var loaded: Boolean = true
18 | override var regex: Regex = """^#微博表情""".toRegex()
19 | private set
20 | override val properties: Properties = Properties().apply { put("regex", regex.pattern) }
21 | override lateinit var permission: Permission
22 | private var folder: File = File(System.getProperty("user.dir", ".")).resolve(".weibo")
23 |
24 | override fun load(folder: File) {
25 | this.folder = folder
26 | when (val re = properties["regex"]) {
27 | is String -> regex = re.toRegex()
28 | is Regex -> regex = re
29 | else -> {}
30 | }
31 | loaded = try {
32 | @Suppress("INVISIBLE_MEMBER")
33 | WeiboEmoticonData
34 | true
35 | } catch (_: Throwable) {
36 | false
37 | }
38 |
39 | }
40 |
41 | override fun enable(permission: Permission) {
42 | this.permission = permission
43 | }
44 |
45 | override fun disable() {}
46 |
47 | override suspend fun MessageEvent.replier(match: MatchResult): MessageChain? {
48 | val content = message.content
49 | val emoticons = buildList {
50 | @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
51 | for ((phrase, emoticon) in WeiboEmoticonData.emoticons) {
52 | if (phrase in content || "<${emoticon.category.ifEmpty { "默认" }}>" in content) add(emoticon)
53 | }
54 | }
55 | if (emoticons.isEmpty()) return null
56 | return buildMessageChain {
57 | for (emoticon in emoticons) {
58 | val file = folder.resolve(emoticon.url.substringAfterLast('/')).takeIf { it.exists() }
59 | ?: download(urlString = emoticon.url, folder = folder)
60 | +file.uploadAsImage(contact = subject)
61 | }
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemeYgo.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import kotlinx.coroutines.*
4 | import net.mamoe.mirai.console.permission.*
5 | import net.mamoe.mirai.contact.*
6 | import net.mamoe.mirai.event.events.*
7 | import net.mamoe.mirai.message.data.*
8 | import xyz.cssxsh.mirai.meme.*
9 | import xyz.cssxsh.mirai.meme.service.*
10 | import xyz.cssxsh.mirai.skia.*
11 | import java.io.*
12 | import java.util.*
13 | import java.util.zip.*
14 |
15 | /**
16 | * [游戏王制卡器](https://ymssx.github.io/ygo/#/)
17 | * @see YgoCard
18 | */
19 | public class MemeYgo : MemeService {
20 | override val name: String = "Ygo"
21 | override val id: String = "ygo"
22 | override val description: String = "Ygo 卡片 生成器"
23 | override val loaded: Boolean = true
24 | override var regex: Regex = """^#(spell|trap|monster)\s*(\d+)?""".toRegex()
25 | private set
26 | override val properties: Properties = Properties().apply { put("regex", regex.pattern) }
27 | override lateinit var permission: Permission
28 | private lateinit var loadJob: Job
29 |
30 | override fun load(folder: File) {
31 | when (val re = properties["regex"]) {
32 | is String -> regex = re.toRegex()
33 | is Regex -> regex = re
34 | else -> {}
35 | }
36 | loadJob = MemeService.launch(CoroutineName(name)) {
37 | folder.mkdirs()
38 |
39 | val ygo = folder.resolve("ygo-card-master")
40 | if (ygo.exists().not()) {
41 | val file = try {
42 | download(urlString = "https://ghproxy.com/https://github.com/ymssx/ygo-card/archive/refs/heads/master.zip", folder)
43 | } catch (_: Exception) {
44 | folder.resolve("ygo-card-master.zip").delete()
45 | download(urlString = "https://github.com/ymssx/ygo-card/archive/refs/heads/master.zip", folder)
46 | }
47 | runInterruptible(Dispatchers.IO) {
48 | ZipFile(file).use { zip ->
49 | for (entry in zip.entries()) {
50 | val item = folder.resolve(entry.name)
51 | if (entry.isDirectory) {
52 | item.mkdirs()
53 | continue
54 | }
55 | item.outputStream().use { zip.getInputStream(entry).transferTo(it) }
56 | }
57 | }
58 | }
59 | file.deleteOnExit()
60 | }
61 | ygo.resolve("source/mold/attribute/jp")
62 | .renameTo(ygo.resolve("source/mold/attribute/ja"))
63 | ygo.resolve("source/mold/attribute/cn")
64 | .renameTo(ygo.resolve("source/mold/attribute/zh"))
65 | System.setProperty(YgoCard.SOURCE_KEY, ygo.path)
66 | }
67 | }
68 |
69 | override fun enable(permission: Permission) {
70 | this.permission = permission
71 | runBlocking {
72 | loadJob.join()
73 | }
74 | }
75 |
76 | override fun disable() {}
77 |
78 | private fun MutableList.push(key: String): String? {
79 | var temp: String? = null
80 | removeAll { line ->
81 | if (line.startsWith(key) && line.getOrNull(key.length) == '=') {
82 | temp = line
83 | true
84 | } else {
85 | false
86 | }
87 | }
88 | return temp?.removePrefix(key)?.removePrefix("=")
89 | }
90 |
91 | override suspend fun MessageEvent.replier(match: MatchResult): Image {
92 | val id = match.groups[2]?.value?.toLongOrNull()
93 | ?: message.findIsInstance()?.target
94 | ?: sender.id
95 | val image = message.findIsInstance()
96 | val face = when {
97 | image != null -> cache(image = image)
98 | else -> avatar(id = id, size = 640)
99 | }
100 | val lines = message.filterIsInstance().last().content
101 | .removePrefix(" \n")
102 | .lineSequence().toMutableList()
103 | val member = (subject as? Group)?.get(id = id)
104 | val profile = (member ?: sender).queryProfile()
105 | lines.removeAll { it.startsWith('#') }
106 | val name = lines.push(key = "name")
107 | ?: member?.remarkOrNameCardOrNick
108 | ?: sender.nameCardOrNick
109 | val attribute = lines.push(key = "attr")
110 | ?.let { YgoCard.Attribute.valueOf(it) }
111 | ?: YgoCard.Attribute.monster.random()
112 | val level = lines.push(key = "level")?.toInt() ?: (profile.qLevel / 16 + 1)
113 | val race = lines.push(key = "race")?.split(',', ' ', '/') ?: listOfNotNull(
114 | member?.specialTitle?.takeUnless { it.isBlank() },
115 | member?.permission?.name,
116 | profile.sex.name
117 | )
118 | val attack = lines.push(key = "atk") ?: "0"
119 | val defend = lines.push(key = "def") ?: "0"
120 | val copyright = lines.push(key = "copyright")
121 | val description = when {
122 | lines.isNotEmpty() -> lines.joinToString(separator = "\n")
123 | else -> profile.sign
124 | }
125 |
126 | val card = when (val type = match.groups[1]?.value) {
127 | "spell" -> {
128 | YgoCard.Spell(
129 | name = name,
130 | description = description,
131 | face = face,
132 | copyright = copyright
133 | )
134 | }
135 | "trap" -> {
136 | YgoCard.Trap(
137 | name = name,
138 | description = description,
139 | face = face,
140 | copyright = copyright
141 | )
142 | }
143 | "monster" -> {
144 | YgoCard.Monster(
145 | name = name,
146 | attribute = attribute,
147 | level = level,
148 | face = face,
149 | race = race,
150 | description = description,
151 | attack = attack,
152 | defend = defend,
153 | copyright = copyright
154 | )
155 | }
156 | else -> throw IllegalArgumentException("card type: $type")
157 | }
158 |
159 | return card.render().makeSnapshotResource()
160 | .use { resource -> subject.uploadImage(resource = resource) }
161 | }
162 | }
--------------------------------------------------------------------------------
/src/main/kotlin/impl/MemeZZKIA.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.impl
2 |
3 | import kotlinx.coroutines.*
4 | import net.mamoe.mirai.console.permission.*
5 | import net.mamoe.mirai.event.events.*
6 | import net.mamoe.mirai.message.data.*
7 | import xyz.cssxsh.mirai.meme.*
8 | import xyz.cssxsh.mirai.meme.service.*
9 | import xyz.cssxsh.mirai.skia.*
10 | import xyz.cssxsh.skia.*
11 | import java.io.*
12 | import java.util.*
13 |
14 | /**
15 | * [zzkia](https://github.com/dcalsky/zzkia)
16 | */
17 | public class MemeZZKIA : MemeService {
18 | override val name: String = "ZZKIA Pinyin"
19 | override val id: String = "zzkia"
20 | override val description: String = "ZZKIA Pinyin 生成器"
21 | override val loaded: Boolean = true
22 | override var regex: Regex = """^#(zzkia|pinyin)\s+((?s).+)""".toRegex()
23 | private set
24 | override val properties: Properties = Properties().apply { put("regex", regex.pattern) }
25 | override lateinit var permission: Permission
26 |
27 | private lateinit var loadJob: Job
28 |
29 | override fun load(folder: File) {
30 | when (val re = properties["regex"]) {
31 | is String -> regex = re.toRegex()
32 | is Regex -> regex = re
33 | else -> {}
34 | }
35 | loadJob = MemeService.launch(CoroutineName(name)) {
36 | try {
37 | @Suppress("INVISIBLE_MEMBER")
38 | MiraiSkiaPlugin.loadJob.join()
39 | } catch (_: NoSuchMethodError) {
40 | //
41 | }
42 | val zzkia = folder.resolve("zzkia.jpg")
43 | if (zzkia.exists().not()) {
44 | try {
45 | download(urlString = "https://raw.githubusercontent.com/dcalsky/bbq/master/zzkia/images/4.jpg", folder)
46 | } catch (_: Exception) {
47 | download(urlString = "https://tvax2.sinaimg.cn/large/d6ca1528gy1h2ur64giqmj20yi0v3q52.jpg", folder)
48 | }.renameTo(zzkia)
49 | }
50 | System.setProperty(ZZKIA_ORIGIN, zzkia.absolutePath)
51 | val fzxs14 = folder.resolve("FZXS14-ex.ttf")
52 | if (fzxs14.exists().not()) {
53 | try {
54 | download(urlString = "https://raw.githubusercontent.com/dcalsky/bbq/master/fonts/FZXS14-ex.ttf", folder)
55 | } catch (_: Exception) {
56 | fzxs14.delete()
57 | download(urlString = "https://font.taofont.com/en_fonts/fonts/f/FZXS14.ttf", folder)
58 | }.renameTo(fzxs14)
59 | }
60 | FontUtils.loadTypeface(path = fzxs14.path)
61 | }
62 | }
63 |
64 | override fun enable(permission: Permission) {
65 | this.permission = permission
66 | runBlocking {
67 | loadJob.join()
68 | }
69 | }
70 |
71 | override fun disable() {}
72 |
73 | override suspend fun MessageEvent.replier(match: MatchResult): Image {
74 | val (_, text) = match.destructured
75 |
76 | return zzkia(text).makeSnapshotResource()
77 | .use { resource -> subject.uploadImage(resource = resource) }
78 | }
79 | }
--------------------------------------------------------------------------------
/src/main/kotlin/package.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme
--------------------------------------------------------------------------------
/src/main/kotlin/service/MemeService.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.meme.service
2 |
3 | import kotlinx.coroutines.*
4 | import net.mamoe.mirai.console.permission.*
5 | import net.mamoe.mirai.event.events.*
6 | import net.mamoe.mirai.message.data.*
7 | import java.io.*
8 | import java.util.*
9 | import kotlin.collections.*
10 | import kotlin.coroutines.*
11 | import kotlin.jvm.*
12 |
13 | /**
14 | * 表情包服务接口
15 | */
16 | public interface MemeService {
17 |
18 | /**
19 | * 服务名称
20 | */
21 | public val name: String
22 |
23 | /**
24 | * 服务ID,将用来构造权限ID和设置缓存文件夹
25 | */
26 | public val id: String
27 |
28 | /**
29 | * 简介
30 | */
31 | public val description: String
32 |
33 | /**
34 | * 已加载
35 | */
36 | public val loaded: Boolean
37 |
38 | /**
39 | * 配置,将会映射到文件
40 | */
41 | public val properties: Properties
42 |
43 | /**
44 | * 权限, 应该为 [load] 的 permission 参数
45 | */
46 | public val permission: Permission
47 |
48 | /**
49 | * 匹配的正则表达式
50 | * @see MessageEvent.replier
51 | */
52 | public val regex: Regex
53 |
54 | /**
55 | * 表情包生成
56 | * @param match 正则表达式匹配结果
57 | * @see regex
58 | */
59 | public suspend fun MessageEvent.replier(match: MatchResult): Message?
60 |
61 | /**
62 | * 加载接口,在 [xyz.cssxsh.mirai.meme.MemeHelperPlugin.onLoad] 时触发
63 | * @param folder 缓存文件夹
64 | * @see xyz.cssxsh.skia.FontUtils
65 | */
66 | @Throws(IOException::class)
67 | public fun load(folder: File)
68 |
69 | /**
70 | * 启动接口,在 [xyz.cssxsh.mirai.meme.MemeHelperPlugin.onEnable] 时触发
71 | * @param permission 分配的权限
72 | */
73 | public fun enable(permission: Permission)
74 |
75 | /**
76 | * 关闭接口,在 [xyz.cssxsh.mirai.meme.MemeHelperPlugin.onDisable] 时触发
77 | */
78 | public fun disable()
79 |
80 | public companion object Loader : Sequence, CoroutineScope {
81 | override var coroutineContext: CoroutineContext = EmptyCoroutineContext
82 | internal set
83 |
84 | internal val instances: MutableSet = HashSet()
85 |
86 | public operator fun get(id: String): MemeService? = instances.find { it.id == id }
87 |
88 | override fun iterator(): Iterator = instances.iterator()
89 | }
90 | }
--------------------------------------------------------------------------------
/src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin:
--------------------------------------------------------------------------------
1 | xyz.cssxsh.mirai.meme.MemeHelperPlugin
--------------------------------------------------------------------------------
/src/main/resources/META-INF/services/xyz.cssxsh.mirai.meme.service.MemeService:
--------------------------------------------------------------------------------
1 | xyz.cssxsh.mirai.meme.impl.MemeBiliBiliEmote
2 | xyz.cssxsh.mirai.meme.impl.MemeChoYen
3 | xyz.cssxsh.mirai.meme.impl.MemeDear
4 | xyz.cssxsh.mirai.meme.impl.MemeEmojiKitchen
5 | xyz.cssxsh.mirai.meme.impl.MemePetPet
6 | xyz.cssxsh.mirai.meme.impl.MemePornHub
7 | xyz.cssxsh.mirai.meme.impl.MemeRecord
8 | xyz.cssxsh.mirai.meme.impl.MemeTank
9 | xyz.cssxsh.mirai.meme.impl.MemeWeiboEmoticon
10 | xyz.cssxsh.mirai.meme.impl.MemeYgo
11 | xyz.cssxsh.mirai.meme.impl.MemeZZKIA
12 | xyz.cssxsh.mirai.meme.impl.MemeSchool
13 | xyz.cssxsh.mirai.meme.impl.MemeProjectSekaiStickers
--------------------------------------------------------------------------------
/src/main/resources/xyz/cssxsh/mirai/meme/impl/record.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cssxsh/meme-helper/89154a34dcc2d3bbd339ccbcc516ed2a8ea1b3f4/src/main/resources/xyz/cssxsh/mirai/meme/impl/record.png
--------------------------------------------------------------------------------
/src/test/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider:
--------------------------------------------------------------------------------
1 | org.slf4j.simple.SimpleServiceProvider
--------------------------------------------------------------------------------