├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .pylintrc
├── .yapfignore
├── LICENSE
├── README.md
├── data
├── boss_data.yaml
├── nicknames.yaml
├── nyb.gif
└── peko_comments.yaml
├── docs
├── config.md
├── deployment.md
└── development.md
├── pekobot-config-example.yaml
├── pekobot
├── __init__.py
├── bot.py
├── cogs
│ ├── __init__.py
│ ├── clanbattles.py
│ ├── gacha.py
│ ├── news.py
│ ├── nicknames.py
│ ├── peko.py
│ ├── pixiv.py
│ └── setu.py
└── utils
│ ├── __init__.py
│ ├── config.py
│ ├── db.py
│ └── files.py
├── pm2.json
├── requirements-dev.txt
├── requirements.txt
├── run.py
└── scripts
└── fetch_boss_data.py
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push, pull_request]
3 | jobs:
4 | yapf:
5 | name: YAPF
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v2
9 | - uses: actions/setup-python@v2
10 | with:
11 | python-version: 3.8
12 | - run: pip install -r requirements-dev.txt
13 | - run: yapf -r -d .
14 | pylint:
15 | name: Pylint
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v2
19 | - uses: actions/setup-python@v2
20 | with:
21 | python-version: 3.8
22 | - run: pip install -r requirements.txt
23 | - run: pip install -r requirements-dev.txt
24 | - run: pylint --rcfile=.pylintrc pekobot
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Python ###
2 | # Byte-compiled / optimized / DLL files
3 | __pycache__/
4 | *.py[cod]
5 | *$py.class
6 |
7 | # C extensions
8 | *.so
9 |
10 | # Distribution / packaging
11 | .Python
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | pip-wheel-metadata/
25 | share/python-wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | *.py,cover
52 | .hypothesis/
53 | .pytest_cache/
54 | pytestdebug.log
55 |
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # Django stuff:
61 | *.log
62 | local_settings.py
63 | db.sqlite3
64 | db.sqlite3-journal
65 |
66 | # Flask stuff:
67 | instance/
68 | .webassets-cache
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 | doc/_build/
76 |
77 | # PyBuilder
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
134 | # pytype static type analyzer
135 | .pytype/
136 |
137 | ### SQLite ###
138 | *.db
139 | *.db-journal
140 |
141 | ### Brotli ###
142 | *.br
143 |
144 | ### Shelve ###
145 | *.db.bak
146 | *.db.dat
147 | *.db.dir
148 |
149 | ### Images ###
150 | images/
151 |
152 | ### Config ###
153 | pekobot-config.yaml
154 |
155 | ### macOS ###
156 | .DS_Store
157 |
158 | ### Visual Studio Code ###
159 | .vscode/
160 |
161 | ### JetBrains ###
162 | .idea/
163 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/mirrors-yapf
3 | rev: v0.30.0
4 | hooks:
5 | - id: yapf
6 | - repo: local
7 | hooks:
8 | - id: pylint
9 | name: pylint
10 | entry: pylint
11 | language: system
12 | types: [python]
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 |
3 | # A comma-separated list of package or module names from where C extensions may
4 | # be loaded. Extensions are loading into the active Python interpreter and may
5 | # run arbitrary code.
6 | extension-pkg-whitelist=
7 |
8 | # Specify a score threshold to be exceeded before program exits with error.
9 | fail-under=10
10 |
11 | # Add files or directories to the blacklist. They should be base names, not
12 | # paths.
13 | ignore=CVS
14 |
15 | # Add files or directories matching the regex patterns to the blacklist. The
16 | # regex matches against base names, not paths.
17 | ignore-patterns=
18 |
19 | # Python code to execute, usually for sys.path manipulation such as
20 | # pygtk.require().
21 | #init-hook=
22 |
23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
24 | # number of processors available to use.
25 | jobs=1
26 |
27 | # Control the amount of potential inferred values when inferring a single
28 | # object. This can help the performance when dealing with large functions or
29 | # complex, nested conditions.
30 | limit-inference-results=100
31 |
32 | # List of plugins (as comma separated values of python module names) to load,
33 | # usually to register additional checkers.
34 | load-plugins=
35 |
36 | # Pickle collected data for later comparisons.
37 | persistent=yes
38 |
39 | # When enabled, pylint would attempt to guess common misconfiguration and emit
40 | # user-friendly hints instead of false-positive error messages.
41 | suggestion-mode=yes
42 |
43 | # Allow loading of arbitrary C extensions. Extensions are imported into the
44 | # active Python interpreter and may run arbitrary code.
45 | unsafe-load-any-extension=no
46 |
47 |
48 | [MESSAGES CONTROL]
49 |
50 | # Only show warnings with the listed confidence levels. Leave empty to show
51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
52 | confidence=
53 |
54 | # Disable the message, report, category or checker with the given id(s). You
55 | # can either give multiple identifiers separated by comma (,) or put this
56 | # option multiple times (only on the command line, not in the configuration
57 | # file where it should appear only once). You can also use "--disable=all" to
58 | # disable everything first and then reenable specific checks. For example, if
59 | # you want to run only the similarities checker, you can use "--disable=all
60 | # --enable=similarities". If you want to run only the classes checker, but have
61 | # no Warning level messages displayed, use "--disable=all --enable=classes
62 | # --disable=W".
63 | disable=print-statement,
64 | parameter-unpacking,
65 | unpacking-in-except,
66 | old-raise-syntax,
67 | backtick,
68 | long-suffix,
69 | old-ne-operator,
70 | old-octal-literal,
71 | import-star-module-level,
72 | non-ascii-bytes-literal,
73 | raw-checker-failed,
74 | bad-inline-option,
75 | locally-disabled,
76 | file-ignored,
77 | suppressed-message,
78 | useless-suppression,
79 | deprecated-pragma,
80 | use-symbolic-message-instead,
81 | apply-builtin,
82 | basestring-builtin,
83 | buffer-builtin,
84 | cmp-builtin,
85 | coerce-builtin,
86 | execfile-builtin,
87 | file-builtin,
88 | long-builtin,
89 | raw_input-builtin,
90 | reduce-builtin,
91 | standarderror-builtin,
92 | unicode-builtin,
93 | xrange-builtin,
94 | coerce-method,
95 | delslice-method,
96 | getslice-method,
97 | setslice-method,
98 | no-absolute-import,
99 | old-division,
100 | dict-iter-method,
101 | dict-view-method,
102 | next-method-called,
103 | metaclass-assignment,
104 | indexing-exception,
105 | raising-string,
106 | reload-builtin,
107 | oct-method,
108 | hex-method,
109 | nonzero-method,
110 | cmp-method,
111 | input-builtin,
112 | round-builtin,
113 | intern-builtin,
114 | unichr-builtin,
115 | map-builtin-not-iterating,
116 | zip-builtin-not-iterating,
117 | range-builtin-not-iterating,
118 | filter-builtin-not-iterating,
119 | using-cmp-argument,
120 | eq-without-hash,
121 | div-method,
122 | idiv-method,
123 | rdiv-method,
124 | exception-message-attribute,
125 | invalid-str-codec,
126 | sys-max-int,
127 | bad-python3-import,
128 | deprecated-string-function,
129 | deprecated-str-translate-call,
130 | deprecated-itertools-function,
131 | deprecated-types-field,
132 | next-method-defined,
133 | dict-items-not-iterating,
134 | dict-keys-not-iterating,
135 | dict-values-not-iterating,
136 | deprecated-operator-function,
137 | deprecated-urllib-function,
138 | xreadlines-attribute,
139 | deprecated-sys-function,
140 | exception-escape,
141 | comprehension-escape,
142 | invalid-name,
143 | too-many-locals,
144 | fixme,
145 | too-few-public-methods
146 |
147 | # Enable the message, report, category or checker with the given id(s). You can
148 | # either give multiple identifier separated by comma (,) or put this option
149 | # multiple time (only on the command line, not in the configuration file where
150 | # it should appear only once). See also the "--disable" option for examples.
151 | enable=c-extension-no-member
152 |
153 |
154 | [REPORTS]
155 |
156 | # Python expression which should return a score less than or equal to 10. You
157 | # have access to the variables 'error', 'warning', 'refactor', and 'convention'
158 | # which contain the number of messages in each category, as well as 'statement'
159 | # which is the total number of statements analyzed. This score is used by the
160 | # global evaluation report (RP0004).
161 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
162 |
163 | # Template used to display messages. This is a python new-style format string
164 | # used to format the message information. See doc for all details.
165 | #msg-template=
166 |
167 | # Set the output format. Available formats are text, parseable, colorized, json
168 | # and msvs (visual studio). You can also give a reporter class, e.g.
169 | # mypackage.mymodule.MyReporterClass.
170 | output-format=text
171 |
172 | # Tells whether to display a full report or only the messages.
173 | reports=no
174 |
175 | # Activate the evaluation score.
176 | score=yes
177 |
178 |
179 | [REFACTORING]
180 |
181 | # Maximum number of nested blocks for function / method body
182 | max-nested-blocks=5
183 |
184 | # Complete name of functions that never returns. When checking for
185 | # inconsistent-return-statements if a never returning function is called then
186 | # it will be considered as an explicit return statement and no message will be
187 | # printed.
188 | never-returning-functions=sys.exit
189 |
190 |
191 | [LOGGING]
192 |
193 | # The type of string formatting that logging methods do. `old` means using %
194 | # formatting, `new` is for `{}` formatting.
195 | logging-format-style=old
196 |
197 | # Logging modules to check that the string format arguments are in logging
198 | # function parameter format.
199 | logging-modules=logging
200 |
201 |
202 | [SPELLING]
203 |
204 | # Limits count of emitted suggestions for spelling mistakes.
205 | max-spelling-suggestions=4
206 |
207 | # Spelling dictionary name. Available dictionaries: none. To make it work,
208 | # install the python-enchant package.
209 | spelling-dict=
210 |
211 | # List of comma separated words that should not be checked.
212 | spelling-ignore-words=
213 |
214 | # A path to a file that contains the private dictionary; one word per line.
215 | spelling-private-dict-file=
216 |
217 | # Tells whether to store unknown words to the private dictionary (see the
218 | # --spelling-private-dict-file option) instead of raising a message.
219 | spelling-store-unknown-words=no
220 |
221 |
222 | [MISCELLANEOUS]
223 |
224 | # List of note tags to take in consideration, separated by a comma.
225 | notes=FIXME,
226 | XXX,
227 | TODO
228 |
229 | # Regular expression of note tags to take in consideration.
230 | #notes-rgx=
231 |
232 |
233 | [TYPECHECK]
234 |
235 | # List of decorators that produce context managers, such as
236 | # contextlib.contextmanager. Add to this list to register other decorators that
237 | # produce valid context managers.
238 | contextmanager-decorators=contextlib.contextmanager
239 |
240 | # List of members which are set dynamically and missed by pylint inference
241 | # system, and so shouldn't trigger E1101 when accessed. Python regular
242 | # expressions are accepted.
243 | generated-members=
244 |
245 | # Tells whether missing members accessed in mixin class should be ignored. A
246 | # mixin class is detected if its name ends with "mixin" (case insensitive).
247 | ignore-mixin-members=yes
248 |
249 | # Tells whether to warn about missing members when the owner of the attribute
250 | # is inferred to be None.
251 | ignore-none=yes
252 |
253 | # This flag controls whether pylint should warn about no-member and similar
254 | # checks whenever an opaque object is returned when inferring. The inference
255 | # can return multiple potential results while evaluating a Python object, but
256 | # some branches might not be evaluated, which results in partial inference. In
257 | # that case, it might be useful to still emit no-member and other checks for
258 | # the rest of the inferred objects.
259 | ignore-on-opaque-inference=yes
260 |
261 | # List of class names for which member attributes should not be checked (useful
262 | # for classes with dynamically set attributes). This supports the use of
263 | # qualified names.
264 | ignored-classes=optparse.Values,thread._local,_thread._local
265 |
266 | # List of module names for which member attributes should not be checked
267 | # (useful for modules/projects where namespaces are manipulated during runtime
268 | # and thus existing member attributes cannot be deduced by static analysis). It
269 | # supports qualified module names, as well as Unix pattern matching.
270 | ignored-modules=
271 |
272 | # Show a hint with possible names when a member name was not found. The aspect
273 | # of finding the hint is based on edit distance.
274 | missing-member-hint=yes
275 |
276 | # The minimum edit distance a name should have in order to be considered a
277 | # similar match for a missing member name.
278 | missing-member-hint-distance=1
279 |
280 | # The total number of similar names that should be taken in consideration when
281 | # showing a hint for a missing member.
282 | missing-member-max-choices=1
283 |
284 | # List of decorators that change the signature of a decorated function.
285 | signature-mutators=
286 |
287 |
288 | [VARIABLES]
289 |
290 | # List of additional names supposed to be defined in builtins. Remember that
291 | # you should avoid defining new builtins when possible.
292 | additional-builtins=
293 |
294 | # Tells whether unused global variables should be treated as a violation.
295 | allow-global-unused-variables=yes
296 |
297 | # List of strings which can identify a callback function by name. A callback
298 | # name must start or end with one of those strings.
299 | callbacks=cb_,
300 | _cb
301 |
302 | # A regular expression matching the name of dummy variables (i.e. expected to
303 | # not be used).
304 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
305 |
306 | # Argument names that match this expression will be ignored. Default to name
307 | # with leading underscore.
308 | ignored-argument-names=_.*|^ignored_|^unused_
309 |
310 | # Tells whether we should check for unused import in __init__ files.
311 | init-import=no
312 |
313 | # List of qualified module names which can have objects that can redefine
314 | # builtins.
315 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
316 |
317 |
318 | [FORMAT]
319 |
320 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
321 | expected-line-ending-format=
322 |
323 | # Regexp for a line that is allowed to be longer than the limit.
324 | ignore-long-lines=^\s*(# )??$
325 |
326 | # Number of spaces of indent required inside a hanging or continued line.
327 | indent-after-paren=4
328 |
329 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
330 | # tab).
331 | indent-string=' '
332 |
333 | # Maximum number of characters on a single line.
334 | max-line-length=100
335 |
336 | # Maximum number of lines in a module.
337 | max-module-lines=1000
338 |
339 | # List of optional constructs for which whitespace checking is disabled. `dict-
340 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
341 | # `trailing-comma` allows a space between comma and closing bracket: (a, ).
342 | # `empty-line` allows space-only lines.
343 | no-space-check=trailing-comma,
344 | dict-separator
345 |
346 | # Allow the body of a class to be on the same line as the declaration if body
347 | # contains single statement.
348 | single-line-class-stmt=no
349 |
350 | # Allow the body of an if to be on the same line as the test if there is no
351 | # else.
352 | single-line-if-stmt=no
353 |
354 |
355 | [SIMILARITIES]
356 |
357 | # Ignore comments when computing similarities.
358 | ignore-comments=yes
359 |
360 | # Ignore docstrings when computing similarities.
361 | ignore-docstrings=yes
362 |
363 | # Ignore imports when computing similarities.
364 | ignore-imports=no
365 |
366 | # Minimum lines number of a similarity.
367 | min-similarity-lines=4
368 |
369 |
370 | [BASIC]
371 |
372 | # Naming style matching correct argument names.
373 | argument-naming-style=snake_case
374 |
375 | # Regular expression matching correct argument names. Overrides argument-
376 | # naming-style.
377 | #argument-rgx=
378 |
379 | # Naming style matching correct attribute names.
380 | attr-naming-style=snake_case
381 |
382 | # Regular expression matching correct attribute names. Overrides attr-naming-
383 | # style.
384 | #attr-rgx=
385 |
386 | # Bad variable names which should always be refused, separated by a comma.
387 | bad-names=foo,
388 | bar,
389 | baz,
390 | toto,
391 | tutu,
392 | tata
393 |
394 | # Bad variable names regexes, separated by a comma. If names match any regex,
395 | # they will always be refused
396 | bad-names-rgxs=
397 |
398 | # Naming style matching correct class attribute names.
399 | class-attribute-naming-style=any
400 |
401 | # Regular expression matching correct class attribute names. Overrides class-
402 | # attribute-naming-style.
403 | #class-attribute-rgx=
404 |
405 | # Naming style matching correct class names.
406 | class-naming-style=PascalCase
407 |
408 | # Regular expression matching correct class names. Overrides class-naming-
409 | # style.
410 | #class-rgx=
411 |
412 | # Naming style matching correct constant names.
413 | const-naming-style=UPPER_CASE
414 |
415 | # Regular expression matching correct constant names. Overrides const-naming-
416 | # style.
417 | #const-rgx=
418 |
419 | # Minimum line length for functions/classes that require docstrings, shorter
420 | # ones are exempt.
421 | docstring-min-length=-1
422 |
423 | # Naming style matching correct function names.
424 | function-naming-style=snake_case
425 |
426 | # Regular expression matching correct function names. Overrides function-
427 | # naming-style.
428 | #function-rgx=
429 |
430 | # Good variable names which should always be accepted, separated by a comma.
431 | good-names=i,
432 | j,
433 | k,
434 | ex,
435 | Run,
436 | _
437 |
438 | # Good variable names regexes, separated by a comma. If names match any regex,
439 | # they will always be accepted
440 | good-names-rgxs=
441 |
442 | # Include a hint for the correct naming format with invalid-name.
443 | include-naming-hint=no
444 |
445 | # Naming style matching correct inline iteration names.
446 | inlinevar-naming-style=any
447 |
448 | # Regular expression matching correct inline iteration names. Overrides
449 | # inlinevar-naming-style.
450 | #inlinevar-rgx=
451 |
452 | # Naming style matching correct method names.
453 | method-naming-style=snake_case
454 |
455 | # Regular expression matching correct method names. Overrides method-naming-
456 | # style.
457 | #method-rgx=
458 |
459 | # Naming style matching correct module names.
460 | module-naming-style=snake_case
461 |
462 | # Regular expression matching correct module names. Overrides module-naming-
463 | # style.
464 | #module-rgx=
465 |
466 | # Colon-delimited sets of names that determine each other's naming style when
467 | # the name regexes allow several styles.
468 | name-group=
469 |
470 | # Regular expression which should only match function or class names that do
471 | # not require a docstring.
472 | no-docstring-rgx=^_
473 |
474 | # List of decorators that produce properties, such as abc.abstractproperty. Add
475 | # to this list to register other decorators that produce valid properties.
476 | # These decorators are taken in consideration only for invalid-name.
477 | property-classes=abc.abstractproperty
478 |
479 | # Naming style matching correct variable names.
480 | variable-naming-style=snake_case
481 |
482 | # Regular expression matching correct variable names. Overrides variable-
483 | # naming-style.
484 | #variable-rgx=
485 |
486 |
487 | [STRING]
488 |
489 | # This flag controls whether inconsistent-quotes generates a warning when the
490 | # character used as a quote delimiter is used inconsistently within a module.
491 | check-quote-consistency=no
492 |
493 | # This flag controls whether the implicit-str-concat should generate a warning
494 | # on implicit string concatenation in sequences defined over several lines.
495 | check-str-concat-over-line-jumps=no
496 |
497 |
498 | [IMPORTS]
499 |
500 | # List of modules that can be imported at any level, not just the top level
501 | # one.
502 | allow-any-import-level=
503 |
504 | # Allow wildcard imports from modules that define __all__.
505 | allow-wildcard-with-all=no
506 |
507 | # Analyse import fallback blocks. This can be used to support both Python 2 and
508 | # 3 compatible code, which means that the block might have code that exists
509 | # only in one or another interpreter, leading to false positives when analysed.
510 | analyse-fallback-blocks=no
511 |
512 | # Deprecated modules which should not be used, separated by a comma.
513 | deprecated-modules=optparse,tkinter.tix
514 |
515 | # Create a graph of external dependencies in the given file (report RP0402 must
516 | # not be disabled).
517 | ext-import-graph=
518 |
519 | # Create a graph of every (i.e. internal and external) dependencies in the
520 | # given file (report RP0402 must not be disabled).
521 | import-graph=
522 |
523 | # Create a graph of internal dependencies in the given file (report RP0402 must
524 | # not be disabled).
525 | int-import-graph=
526 |
527 | # Force import order to recognize a module as part of the standard
528 | # compatibility libraries.
529 | known-standard-library=
530 |
531 | # Force import order to recognize a module as part of a third party library.
532 | known-third-party=enchant
533 |
534 | # Couples of modules and preferred modules, separated by a comma.
535 | preferred-modules=
536 |
537 |
538 | [CLASSES]
539 |
540 | # List of method names used to declare (i.e. assign) instance attributes.
541 | defining-attr-methods=__init__,
542 | __new__,
543 | setUp,
544 | __post_init__
545 |
546 | # List of member names, which should be excluded from the protected access
547 | # warning.
548 | exclude-protected=_asdict,
549 | _fields,
550 | _replace,
551 | _source,
552 | _make
553 |
554 | # List of valid names for the first argument in a class method.
555 | valid-classmethod-first-arg=cls
556 |
557 | # List of valid names for the first argument in a metaclass class method.
558 | valid-metaclass-classmethod-first-arg=cls
559 |
560 |
561 | [DESIGN]
562 |
563 | # Maximum number of arguments for function / method.
564 | max-args=5
565 |
566 | # Maximum number of attributes for a class (see R0902).
567 | max-attributes=7
568 |
569 | # Maximum number of boolean expressions in an if statement (see R0916).
570 | max-bool-expr=5
571 |
572 | # Maximum number of branch for function / method body.
573 | max-branches=12
574 |
575 | # Maximum number of locals for function / method body.
576 | max-locals=15
577 |
578 | # Maximum number of parents for a class (see R0901).
579 | max-parents=7
580 |
581 | # Maximum number of public methods for a class (see R0904).
582 | max-public-methods=20
583 |
584 | # Maximum number of return / yield for function / method body.
585 | max-returns=6
586 |
587 | # Maximum number of statements in function / method body.
588 | max-statements=50
589 |
590 | # Minimum number of public methods for a class (see R0903).
591 | min-public-methods=2
592 |
593 |
594 | [EXCEPTIONS]
595 |
596 | # Exceptions that will emit a warning when being caught. Defaults to
597 | # "BaseException, Exception".
598 | overgeneral-exceptions=BaseException,
599 | Exception
600 |
--------------------------------------------------------------------------------
/.yapfignore:
--------------------------------------------------------------------------------
1 | .git
2 | .github/
3 | env/
4 |
--------------------------------------------------------------------------------
/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 run 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) run 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 run 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, run 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 | run 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 run, 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 run, 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 | .
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 佩可机器人
2 |
3 | ## 简介
4 |
5 | 公主连结R玩家专用Discord机器人。由日服公会メルヘンキングダム负责开发和维护。
6 |
7 | ## 使用说明
8 |
9 | 想要知道佩可机器人有哪些功能?用`!help`查询。想要某个指令的具体描述,用`!help [指令]`查询。
10 |
11 | ## 文档
12 |
13 | - [设置说明](docs/config.md)
14 | - [部署指南](docs/deployment.md)
15 | - [开发者指南](docs/development.md)
16 |
17 | ## 交流方式
18 |
19 | - QQ: 903741415
20 | - Discord: https://discord.gg/w9mzcZe
21 |
--------------------------------------------------------------------------------
/data/boss_data.yaml:
--------------------------------------------------------------------------------
1 | BOSS_HP:
2 | - BOSS_HP_JP
3 | - BOSS_HP_TW
4 | - BOSS_HP_CN
5 | BOSS_HP_CN:
6 | - - 6000000
7 | - 8000000
8 | - 10000000
9 | - 12000000
10 | - 20000000
11 | - - 6000000
12 | - 8000000
13 | - 10000000
14 | - 12000000
15 | - 20000000
16 | - - 6000000
17 | - 8000000
18 | - 10000000
19 | - 12000000
20 | - 20000000
21 | - - 6000000
22 | - 8000000
23 | - 10000000
24 | - 12000000
25 | - 20000000
26 | - - 6000000
27 | - 8000000
28 | - 10000000
29 | - 12000000
30 | - 20000000
31 | - - 6000000
32 | - 8000000
33 | - 10000000
34 | - 12000000
35 | - 20000000
36 | - - 6000000
37 | - 8000000
38 | - 10000000
39 | - 12000000
40 | - 20000000
41 | - - 6000000
42 | - 8000000
43 | - 10000000
44 | - 12000000
45 | - 20000000
46 | BOSS_HP_JP:
47 | - - 6000000
48 | - 8000000
49 | - 10000000
50 | - 12000000
51 | - 15000000
52 | - - 6000000
53 | - 8000000
54 | - 10000000
55 | - 12000000
56 | - 15000000
57 | - - 7000000
58 | - 9000000
59 | - 13000000
60 | - 15000000
61 | - 20000000
62 | - - 15000000
63 | - 16000000
64 | - 18000000
65 | - 19000000
66 | - 20000000
67 | BOSS_HP_TW:
68 | - - 6000000
69 | - 8000000
70 | - 10000000
71 | - 12000000
72 | - 15000000
73 | - - 6000000
74 | - 8000000
75 | - 10000000
76 | - 12000000
77 | - 15000000
78 | - - 7000000
79 | - 9000000
80 | - 13000000
81 | - 15000000
82 | - 20000000
83 | - - 15000000
84 | - 16000000
85 | - 18000000
86 | - 19000000
87 | - 20000000
88 | SCORE_RATE:
89 | - SCORE_RATE_JP
90 | - SCORE_RATE_TW
91 | - SCORE_RATE_CN
92 | SCORE_RATE_CN:
93 | - - 1.0
94 | - 1.0
95 | - 1.3
96 | - 1.3
97 | - 1.5
98 | - - 1.4
99 | - 1.4
100 | - 1.8
101 | - 1.8
102 | - 2.0
103 | - - 2.0
104 | - 2.0
105 | - 2.5
106 | - 2.5
107 | - 3.0
108 | - - 2.0
109 | - 2.0
110 | - 2.5
111 | - 2.5
112 | - 3.0
113 | - - 1.0
114 | - 1.0
115 | - 1.1
116 | - 1.1
117 | - 1.2
118 | - - 1.2
119 | - 1.2
120 | - 1.5
121 | - 1.7
122 | - 2.0
123 | - - 1.0
124 | - 1.0
125 | - 1.3
126 | - 1.3
127 | - 1.5
128 | - - 1.3
129 | - 1.3
130 | - 1.8
131 | - 1.8
132 | - 2.0
133 | SCORE_RATE_JP:
134 | - - 1.2
135 | - 1.2
136 | - 1.3
137 | - 1.4
138 | - 1.5
139 | - - 1.6
140 | - 1.6
141 | - 1.8
142 | - 1.9
143 | - 2.0
144 | - - 2.0
145 | - 2.0
146 | - 2.4
147 | - 2.4
148 | - 2.6
149 | - - 3.5
150 | - 3.5
151 | - 3.7
152 | - 3.8
153 | - 4.0
154 | SCORE_RATE_TW:
155 | - - 1.2
156 | - 1.2
157 | - 1.3
158 | - 1.4
159 | - 1.5
160 | - - 1.6
161 | - 1.6
162 | - 1.8
163 | - 1.9
164 | - 2.0
165 | - - 2.0
166 | - 2.0
167 | - 2.4
168 | - 2.4
169 | - 2.6
170 | - - 3.5
171 | - 3.5
172 | - 3.7
173 | - 3.8
174 | - 4.0
175 |
--------------------------------------------------------------------------------
/data/nicknames.yaml:
--------------------------------------------------------------------------------
1 | yui:
2 | jp_name: ユイ
3 | tc_name: 優衣
4 | cn_name: 优衣
5 | nicknames:
6 | - 种田
7 | - 普田
8 | - 由衣
9 | - 结衣
10 | - ue
--------------------------------------------------------------------------------
/data/nyb.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algobot76/pekobot/8c1ad81d469b75bbe647a5ca7a21db4785bcda01/data/nyb.gif
--------------------------------------------------------------------------------
/data/peko_comments.yaml:
--------------------------------------------------------------------------------
1 | comments:
2 | - 诶嘿嘿,佩可莉姆这个绰号,我还挺喜欢呢
3 | - 难道说,你不喜欢魔物料理?我倒是挺喜欢的
4 | - 咕噜噜噜……啊,肚子叫了……不、不许笑!
5 | - 我们去吃饭吧!一起吃饭关系会变好哦!
6 | - 要给我食物吗?哇啊,我可以爱上你吗?
7 | - 我买了泳衣!怎么样怎么样?适合我吗?
8 | - 这件泳衣还挺方便行动的感觉可以直接潜到海里捕捉海鲜呢!
9 | - 我们来玩海滩运动吧!比如沙滩球啦,沙滩抢旗之类的!我可不会输的哦~
10 | - 我要穿着泳装诱惑你♪开玩笑的!
11 | - 难得穿了泳装,我们就一起尽情游泳,尽情玩闹……创造许许多多的回忆吧!
12 |
--------------------------------------------------------------------------------
/docs/config.md:
--------------------------------------------------------------------------------
1 | # 设置说明
2 |
3 | 可以通过修改`pekobot-config.yaml`配置文件调整佩可机器人的设置。
4 |
5 | ```yaml
6 | discord_token:
7 | cogs:
8 | - nicknames
9 | - gacha
10 | - clanbattles
11 | - news
12 | - peko
13 | - setu
14 | ```
15 |
16 | - `discord_token`: 你的Discord机器人Token
17 | - `cogs`: 佩可机器人需要加载的插件。如果你不想加载某个插件(例如:setu),把它从配置文件中删掉即可。
18 |
--------------------------------------------------------------------------------
/docs/deployment.md:
--------------------------------------------------------------------------------
1 | # 部署指南
2 |
3 | ## 申请Discord机器人
4 |
5 | 在部署佩可机器人之前,需要去Discord官网申请一个机器人。具体操作请参考[这篇教程](https://discordpy.readthedocs.io/en/latest/discord.html)。
6 |
7 | ## 安装PM2
8 |
9 | [PM2](https://pm2.keymetrics.io/)是个开源的进程管理工具。我们用PM2来管理佩可机器人的进程。
10 |
11 | ### Ubuntu
12 |
13 | ```bash
14 | sudo apt update
15 | sudo apt install node npm
16 | npm install -g pm2
17 | ```
18 |
19 | ## 安装Python
20 |
21 | ### Ubuntu
22 |
23 | ```bash
24 | sudo apt update
25 | sudo apt install make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
26 | curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
27 | exec $SHELL
28 | pyenv install 3.8.3
29 | ```
30 |
31 | ## 安装机器人的依赖
32 |
33 | ### Ubuntu
34 |
35 | ```bash
36 | mkdir apps
37 | cd apps
38 | git clone https://github.com/Marchen-Kingdom/pekobot.git
39 | cd pekobot
40 | pyenv shell 3.8.3
41 | python -m venv env
42 | source env/bin/activate
43 | pip install -r requirements.txt
44 | ```
45 |
46 | ## 启动机器人
47 |
48 | ```bash
49 | pm2 start pm2.json
50 | ```
51 |
--------------------------------------------------------------------------------
/docs/development.md:
--------------------------------------------------------------------------------
1 | # 开发者指南
2 |
3 | ```bash
4 | pip install -r requirements.txt
5 | pip install -r requirements-dev.txt
6 | pre-commit install
7 | ```
8 |
--------------------------------------------------------------------------------
/pekobot-config-example.yaml:
--------------------------------------------------------------------------------
1 | discord_token:
2 | pixiv:
3 | username:
4 | password:
5 | cogs:
6 | - nicknames
7 | - gacha
8 | - clanbattles
9 | - news
10 | - peko
11 | - setu
--------------------------------------------------------------------------------
/pekobot/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algobot76/pekobot/8c1ad81d469b75bbe647a5ca7a21db4785bcda01/pekobot/__init__.py
--------------------------------------------------------------------------------
/pekobot/bot.py:
--------------------------------------------------------------------------------
1 | """An extension of commands.Bot"""
2 | import logging
3 |
4 | import aiohttp
5 | from discord.ext import commands
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | @commands.command(name="help", aliases=("帮助", ))
11 | async def help_(ctx: commands.Context):
12 | """查看使用说明"""
13 |
14 | logger.info("%s (%s) is asking for help.", ctx.author, ctx.guild)
15 |
16 | cogs = [cog for _, cog in ctx.bot.cogs.items()]
17 |
18 | manual = "使用说明\n"
19 | manual += "=======\n"
20 |
21 | for cog in cogs:
22 | cog_name = cog.qualified_name
23 | manual += f"{cog_name}:\n"
24 |
25 | commands_ = cog.get_commands()
26 | for command in commands_:
27 | command_name = command.name
28 | command_aliases = command.aliases
29 | cmd = command_name
30 | if command_aliases:
31 | for alias in command_aliases:
32 | cmd += f"|{alias}"
33 | command_description = f"![{cmd}]:{command.help}\n"
34 | manual += command_description
35 | manual += "-------\n"
36 | await ctx.send(manual)
37 |
38 |
39 | class Pekobot(commands.Bot):
40 | """Representation of Pekobot.
41 |
42 | Attributes:
43 | session: A client session of aiohttp.
44 | g: A custom context dict.
45 | """
46 | def __init__(self, **kwargs):
47 | super().__init__(command_prefix=kwargs.pop("command_prefix"))
48 | self.session = aiohttp.ClientSession(loop=self.loop)
49 |
50 | self.g = dict() # inspired by flask.g
51 | self.g["pcr_db"] = kwargs.pop("pcr_db")
52 |
53 | self.remove_command("help")
54 | self.add_command(help_)
55 |
--------------------------------------------------------------------------------
/pekobot/cogs/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algobot76/pekobot/8c1ad81d469b75bbe647a5ca7a21db4785bcda01/pekobot/cogs/__init__.py
--------------------------------------------------------------------------------
/pekobot/cogs/clanbattles.py:
--------------------------------------------------------------------------------
1 | """Clan battles cog"""
2 | import datetime
3 | import logging
4 | import os
5 | import shelve
6 | import sqlite3
7 | from typing import Optional, Tuple
8 |
9 | import discord
10 | from discord.ext import commands
11 |
12 | from pekobot.bot import Pekobot
13 | from pekobot.utils import db
14 |
15 | logger = logging.getLogger(__name__)
16 |
17 | META_FILE_PATH = "clanbattles-meta.db"
18 |
19 | CLAN_MEMBER_TABLE = "clan_member"
20 | CREATE_CLAN_MEMBER_TABLE = f"""
21 | CREATE TABLE {CLAN_MEMBER_TABLE}(
22 | member_id INTEGER PRIMARY KEY,
23 | member_name TEXT,
24 | member_nick TEXT
25 | )
26 | """
27 | GET_ALL_CLAN_MEMBERS = f'''
28 | SELECT member_name, member_nick FROM {CLAN_MEMBER_TABLE};
29 | '''
30 | COUNT_CLAN_MEMBER_BY_ID = f'''
31 | SELECT COUNT(*) FROM {CLAN_MEMBER_TABLE}
32 | WHERE member_id=%d;
33 | '''
34 | ADD_NEW_CLAN_MEMBER = f"""
35 | INSERT INTO {CLAN_MEMBER_TABLE} (member_id, member_name, member_nick)
36 | VALUES (%d, '%s', '%s');
37 | """
38 | DELETE_MEMBER_FROM_CLAN = f'''
39 | DELETE FROM {CLAN_MEMBER_TABLE}
40 | WHERE member_id=%d;
41 | '''
42 |
43 | CLAN_BATTLE_TABLE = "clan_battle"
44 | CREATE_CLAN_BATTLE_TABLE = f"""
45 | CREATE TABLE IF NOT EXISTS {CLAN_BATTLE_TABLE} (
46 | date TEXT PRIMARY KEY,
47 | name TEXt
48 | )
49 | """
50 | CREATE_NEW_CLAN_BATTLE = f"""
51 | INSERT INTO {CLAN_BATTLE_TABLE} (date, name)
52 | VALUES ('%s', '%s');
53 | """
54 | COUNT_CLAN_BATTLE = f"""
55 | SELECT COUNT(*) from {CLAN_BATTLE_TABLE}
56 | WHERE date='%s';
57 | """
58 | GET_CLAN_BATTLE_BY_DATE = f"""
59 | SELECT date, name FROM {CLAN_BATTLE_TABLE}
60 | WHERE date='%s'
61 | """
62 | GET_ALL_CLAN_BATTLES = f"""
63 | SELECT date, name FROM {CLAN_BATTLE_TABLE};
64 | """
65 | DELETE_CLAN_BATTLE_BY_DATE = f"""
66 | DELETE FROM {CLAN_BATTLE_TABLE}
67 | WHERE date='%s';
68 | """
69 |
70 |
71 | class MissingDateError(commands.CommandError):
72 | """Exception raised when date is missing."""
73 |
74 |
75 | class ClanBattles(commands.Cog, name="公会战插件"):
76 | """The clan battles cog.
77 |
78 | Attributes:
79 | bot: A Pekobot instance.
80 | connections: A dict that holds DB connections.
81 | """
82 | def __init__(self, bot: Pekobot):
83 | self.bot = bot
84 | self.connections = dict()
85 |
86 | @commands.command(name="create-clan", aliases=("建会", ))
87 | @commands.guild_only()
88 | @commands.has_permissions(administrator=True)
89 | async def create_clan(self, ctx: commands.Context):
90 | """创建公会。"""
91 |
92 | logger.info("Creating a clan for the guild %s.", ctx.guild)
93 | guild_id = ctx.guild.id
94 | conn = self._get_db_connection(guild_id)
95 | cursor = conn.cursor()
96 |
97 | if not self._clan_exists(conn):
98 | cursor.execute(CREATE_CLAN_MEMBER_TABLE)
99 | logger.info("The clan has been created.")
100 | await ctx.send("建会成功")
101 | else:
102 | logger.warning("The clan already exists.")
103 | await ctx.send("公会已存在")
104 |
105 | @commands.command(name="join-clan", aliases=("入会", ))
106 | @commands.guild_only()
107 | async def join_clan(self, ctx: commands.Context):
108 | """加入公会。"""
109 |
110 | logger.info("%s (%s) is joining the clan.", ctx.author, ctx.guild)
111 | guild_id = ctx.guild.id
112 | conn = self._get_db_connection(guild_id)
113 | cursor = conn.cursor()
114 |
115 | if not self._clan_exists(conn):
116 | logger.error("The clan has not been created yet.")
117 | await ctx.send("公会尚未建立")
118 | else:
119 | author = ctx.author
120 | if self._member_exists(conn, author.id):
121 | logger.warning("%s is already in the clan.", author)
122 | await ctx.send("你已是公会成员")
123 | return
124 |
125 | if author.nick:
126 | nick = author.nick
127 | else:
128 | nick = ""
129 |
130 | cursor.execute(ADD_NEW_CLAN_MEMBER % (author.id, author, nick))
131 | conn.commit()
132 | logger.info("%s has joined the clan.", author)
133 | await ctx.send("入会成功")
134 |
135 | @commands.command(name="leave-clan", aliases=("退会", ))
136 | @commands.guild_only()
137 | async def leave_clan(self, ctx: commands.Context):
138 | """退出公会。"""
139 |
140 | logger.info("%s (%s) is leaving the clan.", ctx.author, ctx.guild)
141 | guild_id = ctx.guild.id
142 | conn = self._get_db_connection(guild_id)
143 | cursor = conn.cursor()
144 |
145 | if not self._clan_exists(conn):
146 | logger.error("The clan has not been created yet.")
147 | await ctx.send("公会尚未建立")
148 | else:
149 | author = ctx.author
150 | if not self._member_exists(conn, author.id):
151 | logger.warning("%s is not in the clan yet.", author)
152 | await ctx.send("你还不是公会成员")
153 | else:
154 | cursor.execute(DELETE_MEMBER_FROM_CLAN % author.id)
155 | conn.commit()
156 | logger.info("%s has left the clan.", author)
157 | await ctx.send("退会成功")
158 |
159 | @commands.command(name="list-members", aliases=("查看成员", ))
160 | @commands.guild_only()
161 | async def list_members(self, ctx: commands.Context):
162 | """查看公会成员。"""
163 |
164 | logger.info("%s (%s) is listing all members of the clan.", ctx.author,
165 | ctx.guild)
166 | guild_id = ctx.guild.id
167 | conn = self._get_db_connection(guild_id)
168 | cursor = conn.cursor()
169 |
170 | if not self._clan_exists(conn):
171 | logger.error("The clan has not been created yet.")
172 | await ctx.send("公会尚未建立")
173 | else:
174 | cursor.execute(GET_ALL_CLAN_MEMBERS)
175 | display_names = []
176 | for name, nick in cursor.fetchall():
177 | if not nick:
178 | display_names.append(name)
179 | else:
180 | display_names.append(nick)
181 | if not display_names:
182 | await ctx.send("暂无成员入会")
183 | return
184 | report = "公会成员\n"
185 | report += "=======\n"
186 | report += '\n'.join(display_names)
187 | await ctx.send(report)
188 |
189 | @commands.command(name="start-clan-battle", aliases=("开始会战", ))
190 | @commands.guild_only()
191 | @commands.has_permissions(administrator=True)
192 | async def start_clan_battle(self, ctx: commands.Context, date="", name=""):
193 | """开始会战"""
194 |
195 | logger.info("%s (%s) is creating a new clan battle.", ctx.author,
196 | ctx.guild)
197 | if not self._check_date(date):
198 | logger.error("Invalid date: %s", date)
199 | await ctx.send("请输入合法日期(YYYY-MM-DD)")
200 | return
201 |
202 | guild_id = ctx.guild.id
203 | conn = self._get_db_connection(guild_id)
204 | cursor = conn.cursor()
205 |
206 | cursor.execute(CREATE_CLAN_BATTLE_TABLE)
207 |
208 | if self._clan_battle_exists(conn, date):
209 | logger.warning("Clan battle %s already exists.", date)
210 | await ctx.send("公会战已存在")
211 | return
212 |
213 | cursor.execute(CREATE_NEW_CLAN_BATTLE % (date, name))
214 | conn.commit()
215 | logger.info("The clan battle %s has been created.", date)
216 | await ctx.send("成功创建公会战")
217 |
218 | # Set this clan battle as the current clan battle.
219 | self._set_current_battle(guild_id, date, name)
220 | logger.info("Current clan battle has been updated.")
221 | if name:
222 | await ctx.send(f"正在进行中的公会战已更新为:{date} ({name})")
223 | else:
224 | await ctx.send(f"正在进行中的公会战已更新为:{date}")
225 |
226 | @commands.command(name="current-clan-battle", aliases=("当前会战", ))
227 | @commands.guild_only()
228 | async def show_current_clan_battle(self, ctx: commands.Context):
229 | """显示正在进行中公会战。"""
230 |
231 | logger.info("%s (%s) is requesting the current clan battle.",
232 | ctx.author, ctx.guild)
233 | guild_id = ctx.guild.id
234 | data = self._get_current_battle(guild_id)
235 | if not data:
236 | logger.warning("Current clan battle does not exist.")
237 | await ctx.send("目前无进行中的公会战")
238 | return
239 |
240 | date, name = data
241 | logger.info("Current clan battle: %s.", data)
242 | if name:
243 | await ctx.send(f"当前公会战:{date} ({name})")
244 | else:
245 | await ctx.send(f"当前公会战:{date}")
246 |
247 | @commands.command(name="list-clan-battles", aliases=("查看会战", ))
248 | @commands.guild_only()
249 | async def list_clan_battles(self, ctx: commands.Context):
250 | """列举已有的公会战。"""
251 |
252 | logger.info("%s (%s) is listing all clan battles.", ctx.author,
253 | ctx.guild)
254 | guild_id = ctx.guild.id
255 | conn = self._get_db_connection(guild_id)
256 | cursor = conn.cursor()
257 |
258 | cursor.execute(GET_ALL_CLAN_BATTLES)
259 | battles = cursor.fetchall()
260 | if not battles:
261 | logger.warning("Clan battles not found.")
262 | await ctx.send("暂无公会战数据")
263 | return
264 |
265 | report = "所有公会战\n"
266 | report += "=======\n"
267 | for date, name in battles:
268 | if not name:
269 | report += f"{date}\n"
270 | else:
271 | report += f"{date} ({name})\n"
272 | await ctx.send(report)
273 |
274 | @commands.command(name="delete-clan-battle", aliases=("删除会战", ))
275 | @commands.guild_only()
276 | @commands.has_permissions(administrator=True)
277 | async def delete_clan_battle(self, ctx: commands.Context, date=""):
278 | """删除公会战数据。"""
279 |
280 | logger.info("%s (%s) is deleting a clan battle.", ctx.author,
281 | ctx.guild)
282 | if not self._check_date(date):
283 | logger.error("Invalid date: %s", date)
284 | await ctx.send("请输入合法日期(YYYY-MM-DD)")
285 | return
286 |
287 | guild_id = ctx.guild.id
288 | conn = self._get_db_connection(guild_id)
289 | cursor = conn.cursor()
290 |
291 | logger.info("The clan battle %s will be deleted.", date)
292 | if not self._clan_battle_exists(conn, date):
293 | logger.warning("The clan battle %s does not exist.", date)
294 | await ctx.send("此公会战不存在")
295 | else:
296 | cursor.execute(DELETE_CLAN_BATTLE_BY_DATE % date)
297 | conn.commit()
298 | logger.info("The clan battle %s has been deleted.", date)
299 | await ctx.send("公会战已删除")
300 |
301 | data = self._get_current_battle(guild_id)
302 | if data:
303 | curr_date, _ = data
304 | if date == curr_date:
305 | self._set_current_battle(guild_id, "", "")
306 | logger.info("Current clan battle has been reset.")
307 | await ctx.send("正在进行中的公会战已被重置")
308 |
309 | @commands.command(name="set-clan-battle", aliases=("设置会战", ))
310 | @commands.guild_only()
311 | @commands.has_permissions(administrator=True)
312 | async def set_clan_battle(self, ctx: commands.Context, date=""):
313 | """设置正在进行中的公会战。"""
314 |
315 | logger.info("%s (%s) is setting the current clan battle.", ctx.author,
316 | ctx.guild)
317 | if not self._check_date(date):
318 | logger.error("Invalid date: %s", date)
319 | await ctx.send("请输入合法日期(YYYY-MM-DD)")
320 | return
321 |
322 | guild_id = ctx.guild.id
323 | conn = self._get_db_connection(guild_id)
324 | cursor = conn.cursor()
325 |
326 | if not self._clan_battle_exists(conn, date):
327 | logger.warning("The clan battle %s does not exist.", date)
328 | await ctx.send("此公会战不存在")
329 | else:
330 | cursor.execute(GET_CLAN_BATTLE_BY_DATE % date)
331 | _, name = cursor.fetchone()
332 | self._set_current_battle(guild_id, date, name)
333 | logger.info("The current clan battle has been set to %s.", date)
334 | await ctx.send(f"正在进行中的会战已设置为:{date}")
335 |
336 | @commands.command(name="export-data", aliases=("导出数据", ))
337 | @commands.guild_only()
338 | @commands.has_permissions(administrator=True)
339 | async def export_data(self, ctx: commands.Context):
340 | """导出公会战数据。"""
341 |
342 | logger.info("%s (%s) is export data.", ctx.author, ctx.guild)
343 | id_ = ctx.author.id
344 | guild_id = ctx.guild.id
345 | user = self.bot.get_user(id_)
346 |
347 | db_file = f"clanbattles-{guild_id}.db"
348 | if os.path.exists(db_file):
349 | await user.send(file=discord.File(db_file))
350 | logger.info("Data haS been exported.")
351 | else:
352 | logger.warning("Data does not exist.")
353 | await user.send("数据不存在")
354 |
355 | def _get_db_connection(self, guild_id: int) -> sqlite3.Connection:
356 | """Gets the DB connection for a given guild.
357 |
358 | Args:
359 | guild_id: The ID of a guild.
360 | """
361 |
362 | db_file_name = f"clanbattles-{guild_id}.db"
363 | try:
364 | conn = self.connections[db_file_name]
365 | return conn
366 | except KeyError:
367 | conn = sqlite3.connect(db_file_name)
368 | self.connections[db_file_name] = conn
369 | return conn
370 |
371 | @staticmethod
372 | def _clan_exists(conn: sqlite3.Connection) -> bool:
373 | """Checks if a clan exists.
374 |
375 | Args:
376 | conn: A DB connection.
377 |
378 | Returns:
379 | A bool that indicates if the clan exists.
380 | """
381 |
382 | return db.table_exists(conn, CLAN_MEMBER_TABLE)
383 |
384 | @staticmethod
385 | def _member_exists(conn: sqlite3.Connection, member_id: int) -> bool:
386 | """Checks if a member alreay exists in a clan.
387 |
388 | Args:
389 | conn: A DB connection.
390 | member_id: The ID of a member.
391 |
392 | Returns:
393 | A bool that shows if the member already exists.
394 | """
395 |
396 | cursor = conn.cursor()
397 | cursor.execute(COUNT_CLAN_MEMBER_BY_ID % member_id)
398 | if cursor.fetchone()[0] != 0:
399 | return True
400 | return False
401 |
402 | @staticmethod
403 | def _clan_battle_exists(conn: sqlite3.Connection, date: str) -> bool:
404 | cursor = conn.cursor()
405 | cursor.execute(COUNT_CLAN_BATTLE % date)
406 | if cursor.fetchone()[0] != 0:
407 | return True
408 | return False
409 |
410 | @staticmethod
411 | def _check_date(date: str) -> bool:
412 | """Checks if date is in YYYY-MM-DD format.
413 |
414 | A MissingDateError will be raised if the date is an empty string.
415 |
416 | Args:
417 | date: A date string.
418 |
419 | Returns:
420 | A bool that indicates if the date is valid.
421 | """
422 |
423 | if not date:
424 | raise MissingDateError("Date is missing in user input.")
425 | try:
426 | datetime.datetime.strptime(date, '%Y-%m-%d')
427 | return True
428 | except ValueError:
429 | return False
430 |
431 | @staticmethod
432 | def _get_current_battle(guild_id: int) -> Optional[Tuple[str, str]]:
433 | """Gets the current clan battle.
434 |
435 | Args:
436 | guild_id: ID of a guild
437 |
438 | Returns:
439 | A tuple that contains the battle's date and name. Or None if such
440 | info is not found.
441 | """
442 | with shelve.open(META_FILE_PATH, writeback=True) as s:
443 | guild_id = str(guild_id)
444 | try:
445 | date = s[guild_id]["current_battle_date"]
446 | name = s[guild_id]["current_battle_name"]
447 | return date, name
448 | except KeyError:
449 | s[guild_id] = {
450 | "current_battle_date": "",
451 | "current_battle_name": ""
452 | }
453 | return None
454 |
455 | @staticmethod
456 | def _set_current_battle(guild_id: int, date: str, name: str = ""):
457 | """Sets the current clan battle.
458 |
459 | Args:
460 | guild_id: ID of a guild.
461 | date: A date string.
462 | name: An optional name.
463 | """
464 | with shelve.open(META_FILE_PATH, writeback=True) as s:
465 | guild_id = str(guild_id)
466 | s[guild_id] = {
467 | "current_battle_date": date,
468 | "current_battle_name": name
469 | }
470 |
471 | # pylint: disable=invalid-overridden-method
472 | async def cog_command_error(self, ctx: commands.Context,
473 | error: commands.CommandError):
474 | if isinstance(error, MissingDateError):
475 | await ctx.send("请输入公会战日期")
476 |
477 |
478 | def setup(bot):
479 | """A helper function used to load the cog."""
480 |
481 | bot.add_cog(ClanBattles(bot))
482 |
--------------------------------------------------------------------------------
/pekobot/cogs/gacha.py:
--------------------------------------------------------------------------------
1 | """Gacha cog"""
2 | import asyncio
3 | import io
4 | import random
5 |
6 | import discord
7 | from PIL import Image
8 | from discord.ext import commands
9 |
10 |
11 | def get_unit_icon_id(unit_id, rarity):
12 | """Generates a unit icon ID.
13 |
14 | Args:
15 | unit_id: ID of a unit.
16 | rarity: Rarirty of a unit (1-3).
17 |
18 | Returns:
19 | IF of a unit icon.
20 | """
21 | return int(unit_id) + rarity * 10
22 |
23 |
24 | async def download_unit_icon(session, unit_icon_id):
25 | """Downloads an icon.
26 |
27 | Args:
28 | session: A client session of aiohttp.
29 | unit_icon_id: ID of a unit icon.
30 |
31 | Returns:
32 | A tuple that contains the ID of a unit icon and its data in bytes.
33 | """
34 | url = f"https://redive.estertion.win/icon/unit/{unit_icon_id}.webp"
35 | async with session.get(url) as resp:
36 | data = io.BytesIO(await resp.read())
37 | return unit_icon_id, data
38 |
39 |
40 | def combine_images_h(images):
41 | """Combines images horizontally.
42 |
43 | Args:
44 | images: A list of images.
45 |
46 | Returns:
47 | A single image.
48 | """
49 | sample_image = images[0]
50 | result = Image.new("RGB",
51 | (sample_image.width * len(images), sample_image.height))
52 | for i, image in enumerate(images):
53 | result.paste(image, (sample_image.width * i, 0))
54 | return result
55 |
56 |
57 | def bytes_to_image(buf):
58 | """Converts bytes to an image.
59 |
60 | Args:
61 | buf: Image data in bytes.
62 |
63 | Returns:
64 | An image.
65 | """
66 | return Image.open(buf)
67 |
68 |
69 | def image_to_bytes(image):
70 | """Converts an image to its data in bytes.
71 |
72 | Args:
73 | image: An image
74 |
75 | Returns:
76 | Image data in bytes.
77 | """
78 | buf = io.BytesIO()
79 | image.save(buf, format="PNG")
80 | buf.seek(0)
81 | return buf
82 |
83 |
84 | class Gacha(commands.Cog, name="抽卡插件"):
85 | """Gacha cog"""
86 | def __init__(self, bot):
87 | self.bot = bot
88 |
89 | @commands.command(name="gacha", aliases=("抽卡", "扭蛋"))
90 | async def rolls(self, ctx, n="10"):
91 | """模拟抽卡(默认:10连)。"""
92 |
93 | # n must be a number
94 | if not n.isdigit():
95 | await ctx.send("草,别输入一些乱七八糟的东西啊!!!")
96 | return
97 |
98 | n = int(n)
99 |
100 | # n must be >= 1 and <=10
101 | if n < 1 or n > 10:
102 | await ctx.send("请输入1和10之间的数字")
103 | return
104 | conn = self.bot.g.get("pcr_db")
105 | cursor = conn.cursor()
106 | cursor.execute('''
107 | SELECT unit_id, rarity, is_limited, comment
108 | FROM unit_data
109 | ''')
110 |
111 | characters = dict()
112 | for row in cursor.fetchall():
113 | unit_id, rarity, is_limited, comment = row
114 | if rarity == 2:
115 | rarity -= 1
116 | if comment:
117 | characters[str(unit_id)] = (rarity, is_limited)
118 |
119 | items = list(characters.items())
120 |
121 | # only keep non-limited characters
122 | items = [item for item in items if item[1][1] == 0]
123 |
124 | # TODO: Allow duplicate characters
125 | selected = random.choices(items, k=n)
126 | download_requests = []
127 | # TODO: Cache image files
128 | for item in selected:
129 | unit_id = item[0]
130 | rarity = item[1][0]
131 | unit_icon_id = get_unit_icon_id(unit_id, rarity)
132 | download_requests.append(
133 | download_unit_icon(self.bot.session, unit_icon_id))
134 | downloaded_images = await asyncio.gather(*download_requests)
135 | images = [bytes_to_image(image[1]) for image in downloaded_images]
136 | gacha_result = image_to_bytes(combine_images_h(images))
137 | await ctx.send("素敵な仲間が増えますよ!")
138 | await ctx.send(file=discord.File(gacha_result, "gacha_result.png"))
139 |
140 |
141 | def setup(bot):
142 | """A helper function used to load the cog."""
143 |
144 | bot.add_cog(Gacha(bot))
145 |
--------------------------------------------------------------------------------
/pekobot/cogs/news.py:
--------------------------------------------------------------------------------
1 | """News cog"""
2 | import logging
3 | from typing import List, Tuple
4 |
5 | import aiohttp
6 | import discord
7 | from bs4 import BeautifulSoup
8 | from discord.ext import commands
9 |
10 | from pekobot.bot import Pekobot
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | async def fetch_news(
16 | session: aiohttp.ClientSession) -> List[Tuple[str, str, str]]:
17 | """Fetches news from https://priconne-redive.jp/news.
18 |
19 | Args:
20 | session: A client session of aiohttp.
21 |
22 | Returns:
23 | A list of articles.
24 | """
25 |
26 | async with session.get("https://priconne-redive.jp/news/") as resp:
27 | articles = []
28 | soup = BeautifulSoup(await resp.text(), 'html.parser')
29 | for article in soup.find_all("div", class_="article_box"):
30 | link = article.find("a")["href"]
31 | title = article.find("h4").get_text()
32 | description_div = article.find("div", class_="description")
33 | description = description_div.find("p").get_text()
34 | articles.append((link, title, description))
35 | return articles
36 |
37 |
38 | class News(commands.Cog, name="新闻插件"):
39 | """The news cog.
40 |
41 | Attributes:
42 | bot: A Pekobot instance.
43 | """
44 | def __init__(self, bot: Pekobot):
45 | self.bot = bot
46 |
47 | @commands.command(name="news", aliases=("新闻", ))
48 | async def get_news(self, ctx: commands.Context):
49 | """查看官方新闻。"""
50 |
51 | author = ctx.author
52 | logger.info("%s is requesting news.", author)
53 | articles = await fetch_news(self.bot.session)
54 | logger.info("Fetched %d articles.", len(articles))
55 | if not articles:
56 | await ctx.send("找不到官方新闻(。╯︵╰。) ")
57 | return
58 | description = self._get_description(articles)
59 | embed = discord.Embed(title="官方新闻", description=description)
60 | await ctx.send(embed=embed)
61 |
62 | @staticmethod
63 | def _get_description(articles: List[Tuple[str, str, str]]) -> str:
64 | result = '=======\n'
65 | for link, title, _ in articles:
66 | result += f"{title}\n"
67 | result += f"链接:{link}\n"
68 | result += "-------\n"
69 | return result
70 |
71 |
72 | def setup(bot: Pekobot):
73 | """A helper function used to load the cog."""
74 |
75 | bot.add_cog(News(bot))
76 |
--------------------------------------------------------------------------------
/pekobot/cogs/nicknames.py:
--------------------------------------------------------------------------------
1 | """Nicknames cog"""
2 | import os
3 |
4 | from discord.ext import commands
5 |
6 | from pekobot.utils import files
7 |
8 | NICKNAMES_FILE_PATH = os.path.join("data", "nicknames.yaml")
9 |
10 |
11 | class Nicknames(commands.Cog, name="昵称插件"):
12 | """The Nicknames cog.
13 |
14 | Attributes:
15 | bot: A Pekobot instance.
16 | data: Data of nicknames.
17 | """
18 | def __init__(self, bot):
19 | self.bot = bot
20 | self.data = files.load_yaml_file(NICKNAMES_FILE_PATH)
21 |
22 | @commands.command(name="whois", aliases=("谁是", ))
23 | async def whois(self, ctx, nickname):
24 | """通过昵称查找角色。"""
25 |
26 | for _, v in self.data.items():
27 | if nickname in v["nicknames"]:
28 | await ctx.send(
29 | f"{v['cn_name']} (繁:{v['tc_name']},日:{v['jp_name']})")
30 | else:
31 | await ctx.send("不认识的孩子呢~~~")
32 |
33 |
34 | def setup(bot):
35 | """A helper function used to load the Nicknames cog."""
36 |
37 | bot.add_cog(Nicknames(bot))
38 |
--------------------------------------------------------------------------------
/pekobot/cogs/peko.py:
--------------------------------------------------------------------------------
1 | """Peko cog"""
2 | import logging
3 | import os
4 | import random
5 |
6 | import discord
7 | from discord.ext import commands
8 |
9 | from pekobot.bot import Pekobot
10 | from pekobot.utils import files
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 | # NYB = New Year Burst
15 | NYB_GIF_PATH = os.path.join("data", "nyb.gif")
16 |
17 | NYB_TEXT = '''
18 | 正在播放:New Year Burst
19 | ──●━━━━ 1:05/1:30
20 | ⇆ ㅤ◁ ㅤㅤ❚❚ ㅤㅤ▷ ㅤ↻
21 | '''
22 |
23 | STATUS_REPORT_FORMAT = '''
24 | 状态报告
25 |
26 | 服务器ID: {id}
27 | 目前涩图数量:{num_setu}
28 | '''
29 |
30 | PEKO_COMMENTS_FILE_PATH = os.path.join("data", "peko_comments.yaml")
31 |
32 |
33 | class Peko(commands.Cog, name="佩可插件"):
34 | """The Peko cog.
35 |
36 | Attributes:
37 | bot: A Pekobot instance.
38 | comments: A list of comments by Pecorine.
39 | """
40 | def __init__(self, bot):
41 | self.bot = bot
42 | data = files.load_yaml_file(PEKO_COMMENTS_FILE_PATH)
43 | self.comments = data.get("comments", [])
44 |
45 | @commands.Cog.listener()
46 | async def on_member_join(self, member: discord.Member):
47 | """Triggered when a new member joined the server.
48 |
49 | Args:
50 | member: a discord member.
51 | """
52 |
53 | channel = member.guild.system_channel
54 | if channel is not None:
55 | await channel.send('等你好久了 {0.mention}.'.format(member))
56 |
57 | @commands.Cog.listener()
58 | async def on_command_error(self, ctx: commands.Context,
59 | error: commands.CommandError):
60 | """Triggered when there is a command error.
61 |
62 | Args:
63 | ctx: A command context.
64 | error: A command error.
65 | """
66 |
67 | if isinstance(error, commands.CommandNotFound):
68 | await ctx.send("老娘不认识的指令呢~~")
69 | return
70 | if isinstance(error, commands.NSFWChannelRequired):
71 | await ctx.send("你在想啥呢?变态ヽ(`⌒´メ)ノ")
72 | return
73 | if isinstance(error, commands.MissingPermissions):
74 | if "administrator" in error.missing_perms:
75 | await ctx.send("此功能只对管理员开放")
76 | return
77 | raise error
78 |
79 | @commands.Cog.listener()
80 | async def on_message(self, msg):
81 | """Triggered when there is a new message.
82 |
83 | Args:
84 | msg: A new message.
85 | """
86 |
87 | if "春黑" in msg.content:
88 | await msg.channel.send(file=discord.File(NYB_GIF_PATH))
89 | await msg.channel.send(NYB_TEXT)
90 |
91 | @commands.command(name="tap", aliases=("戳", "👇"))
92 | async def send_random_comment(self, ctx: commands.Context):
93 | """让佩可说出一个随机台词。"""
94 |
95 | logger.info("Pekobot has been tapped by %s.", ctx.author)
96 | comment = random.choice(self.comments)
97 | await ctx.send(comment)
98 |
99 | @commands.command(name="status", aliases=("状态", ))
100 | async def status(self, ctx: discord.ext.commands.Context):
101 | """查看机器人状态。"""
102 |
103 | logger.info("Pekobot's status has been queried by %s.", ctx.author)
104 | guild_id = ctx.guild.id
105 | setu_dir = os.path.join("images", "setu")
106 | setu_count = count_files(setu_dir)
107 | report = STATUS_REPORT_FORMAT.format(id=guild_id, num_setu=setu_count)
108 | await ctx.send(report)
109 |
110 |
111 | def setup(bot: Pekobot):
112 | """A helper function used to load the cog."""
113 |
114 | bot.add_cog(Peko(bot))
115 |
116 |
117 | def count_files(dir_path):
118 | """Counts the number of files in a directory.
119 |
120 | Args:
121 | dir_path: A directory path.
122 | """
123 |
124 | count = 0
125 | for path in os.listdir(dir_path):
126 | if os.path.isfile(os.path.join(dir_path, path)):
127 | count += 1
128 | return count
129 |
--------------------------------------------------------------------------------
/pekobot/cogs/pixiv.py:
--------------------------------------------------------------------------------
1 | # pylint: skip-file
2 | from discord.ext import commands
3 | from pixivpy3 import AppPixivAPI
4 |
5 | from pekobot.utils import config
6 |
7 |
8 | class Pixiv(commands.Cog):
9 | def __init__(self, bot):
10 | self.bot = bot
11 | conf = config.load_config()
12 | self.api = AppPixivAPI()
13 | self.api.login(conf["pixiv_username"], conf["pixiv_password"])
14 |
15 | @commands.command(name="Pixiv", aliases=("pixiv", "PIXIV"))
16 | async def pixiv(self, ctx, option):
17 | mode = ""
18 | if option == "推荐":
19 | mode = "day"
20 | elif option == "色图" or option == "涩图" or option == "setu":
21 | mode = "day_r18"
22 |
23 | result = self.api.illust_ranking(mode)
24 | illusts = result.illusts[:10]
25 | for illust in illusts:
26 | await ctx.send(
27 | f"{illust.title} - https://www.pixiv.net/artworks/{illust.id}")
28 | else:
29 | await ctx.send(f"未知选项:{option}")
30 |
31 |
32 | def setup(bot):
33 | bot.add_cog(Pixiv(bot))
34 |
--------------------------------------------------------------------------------
/pekobot/cogs/setu.py:
--------------------------------------------------------------------------------
1 | """Setu cog"""
2 | import logging
3 | import os
4 | import random
5 |
6 | from discord import File
7 | from discord.ext import commands
8 |
9 | from pekobot.bot import Pekobot
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 | SETU_PATH = os.path.join("images", "setu")
14 |
15 |
16 | class Setu(commands.Cog, name="色图插件"):
17 | """The Setu cog
18 |
19 | Attributes:
20 | bot: A Pekobot instance.
21 | """
22 | def __init__(self, bot: Pekobot):
23 | self.bot = bot
24 |
25 | @commands.command(name="setu", aliases=("色图", "涩图"))
26 | @commands.is_nsfw()
27 | async def send_setu(self, ctx: commands.Context):
28 | """来一份色图。"""
29 |
30 | author = ctx.author
31 | logger.info("%s is requesting a setu.", author)
32 | setu_images = os.listdir(SETU_PATH)
33 | selected_image = random.choice(setu_images)
34 | await ctx.send(file=File(os.path.join(SETU_PATH, selected_image)))
35 |
36 |
37 | def setup(bot):
38 | """A helper fuction used to load the cog."""
39 |
40 | bot.add_cog(Setu(bot))
41 |
--------------------------------------------------------------------------------
/pekobot/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algobot76/pekobot/8c1ad81d469b75bbe647a5ca7a21db4785bcda01/pekobot/utils/__init__.py
--------------------------------------------------------------------------------
/pekobot/utils/config.py:
--------------------------------------------------------------------------------
1 | """A module that contains config-related functions."""
2 | from typing import List, TypedDict
3 |
4 | import yaml
5 |
6 |
7 | class Conf(TypedDict):
8 | """Representation of Pekobot configuration."""
9 | discord_token: str
10 | pixiv_username: str
11 | pixiv_password: str
12 | cogs: List[str]
13 |
14 |
15 | def load_config(config_path: str = "pekobot-config.yaml") -> Conf:
16 | """Loads the Pekobot config.
17 |
18 | Args:
19 | config_path: File path to the config file.
20 |
21 | Returns:
22 | A dict that contains the content of the config.
23 | """
24 |
25 | with open(config_path) as f:
26 | data = yaml.load(f, Loader=yaml.FullLoader)
27 |
28 | token = data["discord_token"]
29 | cogs = data["cogs"]
30 | pixiv_username = data["pixiv"]["username"]
31 | pixiv_password = data["pixiv"]["password"]
32 |
33 | return dict(discord_token=token,
34 | pixiv_username=pixiv_username,
35 | pixiv_password=pixiv_password,
36 | cogs=cogs)
37 |
--------------------------------------------------------------------------------
/pekobot/utils/db.py:
--------------------------------------------------------------------------------
1 | """A utility module built on top of sqlite3."""
2 | import sqlite3
3 |
4 |
5 | def table_exists(conn: sqlite3.Connection, table_name: str) -> bool:
6 | """Checks if a table exists.
7 |
8 | Args:
9 | conn: A DB connection.
10 | table_name: The table to look for.
11 |
12 | Returns:
13 | A bool that indicates if the table exists.
14 | """
15 |
16 | query = f'''
17 | SELECT COUNT(name) FROM sqlite_master
18 | WHERE type='table' AND name='{table_name}';
19 | '''
20 | cursor = conn.cursor()
21 | cursor.execute(query)
22 |
23 | if cursor.fetchone()[0] == 1:
24 | return True
25 | return False
26 |
--------------------------------------------------------------------------------
/pekobot/utils/files.py:
--------------------------------------------------------------------------------
1 | """File utilities"""
2 | from typing import AnyStr, Dict
3 |
4 | import yaml
5 |
6 |
7 | def load_yaml_file(file_path: AnyStr) -> Dict:
8 | """Loads the data from a YAML file.
9 |
10 | Args:
11 | file_path: A file path.
12 |
13 | Returns:
14 | File data as a dict.
15 | """
16 | with open(file_path, "r") as f:
17 | data = yaml.load(f, Loader=yaml.FullLoader)
18 | return data
19 |
--------------------------------------------------------------------------------
/pm2.json:
--------------------------------------------------------------------------------
1 | {
2 | "apps": [
3 | {
4 | "name": "pekobot",
5 | "script": "run.py",
6 | "interpreter": "env/bin/python"
7 | }
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | pylint==2.5.3
2 | pre-commit==2.6.0
3 | yapf==0.30.0
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp==3.6.2
2 | beautifulsoup4==4.9.1
3 | brotli==1.0.7
4 | discord.py==1.3.4
5 | git+https://github.com/upbit/pixivpy.git
6 | Pillow==7.2.0
7 | PyYAML==5.3.1
8 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | # pylint: skip-file
2 | import asyncio
3 | import logging
4 | import os
5 | import sqlite3
6 |
7 | import aiohttp
8 | import brotli
9 |
10 | from pekobot.bot import Pekobot
11 | from pekobot.utils import config
12 |
13 | # Setup logging
14 | logger = logging.getLogger('pekobot')
15 | logger.setLevel(logging.INFO)
16 | stream_handler = logging.StreamHandler()
17 | file_handler = logging.FileHandler(filename='pekobot.log',
18 | encoding='utf-8',
19 | mode='w')
20 | formatter = logging.Formatter('[{asctime}] [{levelname}] {name}: {message}',
21 | '%Y-%m-%d %H:%M:%S',
22 | style='{')
23 | stream_handler.setFormatter(formatter)
24 | file_handler.setFormatter(formatter)
25 | logger.addHandler(stream_handler)
26 | logger.addHandler(file_handler)
27 |
28 | conf = config.load_config()
29 |
30 |
31 | async def run():
32 | # Download redive_jp.db from https://redive.estertion.win/ if it doesn't exist
33 | if not os.path.exists("redive_jp.db"):
34 | async with aiohttp.ClientSession() as session:
35 | async with session.get(
36 | "https://redive.estertion.win/db/redive_jp.db.br") as resp:
37 | brotli_result = await resp.read()
38 | data = brotli.decompress(brotli_result)
39 | with open("redive_jp.db", "wb") as f:
40 | f.write(data)
41 |
42 | pcr_db = sqlite3.connect("redive_jp.db")
43 | bot = Pekobot(command_prefix=("!", "!"), pcr_db=pcr_db)
44 | for cog in conf["cogs"]:
45 | bot.load_extension(f"pekobot.cogs.{cog}")
46 | logger.info("%s has been loaded.", cog)
47 |
48 | @bot.event
49 | async def on_ready():
50 | logger.info('%s has connected to Discord!', bot.user)
51 |
52 | try:
53 | await bot.start(conf['discord_token'])
54 | except KeyboardInterrupt:
55 | pcr_db.close()
56 | await bot.logout()
57 |
58 |
59 | loop = asyncio.get_event_loop()
60 | loop.run_until_complete(run())
61 |
--------------------------------------------------------------------------------
/scripts/fetch_boss_data.py:
--------------------------------------------------------------------------------
1 | # pylint: skip-file
2 | import asyncio
3 | import json
4 | import os
5 |
6 | import aiohttp
7 | import yaml
8 |
9 | BOSS_DATA_URL = "https://raw.githubusercontent.com/Ice-Cirno/HoshinoBot/master/hoshino/modules/pcrclanbattle/clanbattle/config.json"
10 | OUTPUT_PATH = os.path.join("data", "boss_data.yaml")
11 |
12 |
13 | async def run():
14 | async with aiohttp.ClientSession() as session:
15 | async with session.get(BOSS_DATA_URL) as resp:
16 | if resp.status != 200:
17 | print("Failed to fetch the boss data!")
18 | return
19 | content = await resp.text()
20 | data = json.loads(content)
21 | for k in ["_comment_CN", "_comment_TW"]:
22 | if k in data:
23 | del data[k]
24 | with open(OUTPUT_PATH, "w") as out:
25 | yaml.dump(data, out)
26 |
27 |
28 | asyncio.run(run())
29 |
--------------------------------------------------------------------------------