├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── workflows │ └── pypi.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── examples ├── ding-dong-bot.py ├── hotreload_bot.py ├── http_bot.py └── room-bot.py ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── src └── wechaty_puppet_itchat │ ├── __init__.py │ ├── browser.py │ ├── config.py │ ├── itchat │ ├── __init__.py │ ├── async_components │ │ ├── __init__.py │ │ ├── contact.py │ │ ├── hotreload.py │ │ ├── login.py │ │ ├── messages.py │ │ └── register.py │ ├── components │ │ ├── __init__.py │ │ ├── contact.py │ │ ├── hotreload.py │ │ ├── login.py │ │ ├── messages.py │ │ └── register.py │ ├── config.py │ ├── content.py │ ├── core.py │ ├── log.py │ ├── returnvalues.py │ ├── storage │ │ ├── __init__.py │ │ ├── messagequeue.py │ │ └── templates.py │ └── utils.py │ ├── puppet.py │ ├── user │ ├── contact.py │ ├── login.py │ └── message.py │ ├── utils.py │ ├── version.py │ └── version_test.py └── tests └── smoke_testing_test.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | indent_style = space 15 | trim_trailing_whitespace = false 16 | 17 | # 4 tab indentation 18 | [Makefile] 19 | indent_style = tab 20 | indent_size = 4 21 | 22 | [*.py] 23 | indent_size = 4 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help you fix your problem 4 | title: "\U0001F41B\U0001F41B Bug Report: title-of-your-problem" 5 | labels: "\U0001F41B bug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## requirements 11 | 12 | * version of python-wechaty [required] 13 | * version of python-wechaty-puppet-itchat [required] 14 | * version of wechaty docker image [optional] 15 | 16 | ## Describe your problem 17 | 18 | > A clear and concise description of what the bug is. 19 | 20 | ## Reproduce your problem 21 | 22 | ```python 23 | simple code to reproduce your problem 24 | ``` 25 | 26 | ## Error info 27 | 28 | ```shell 29 | # copy your log info at here from your terminal 30 | ``` 31 | 32 | ## Your experiments 33 | 34 | > please tell us your experiments and ideas about this issue. It's valuable for us to help you find the solution. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'Feature request' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question 4 | title: '⁉ ⁉' 5 | labels: 'question' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## requirements 11 | 12 | * version of python-wechaty [required] 13 | * version of python-wechaty-puppet-itchat [required] 14 | * version of wechaty docker image [optional] 15 | 16 | ## Describe your problem 17 | 18 | > A clear and concise description of what the bug is. 19 | 20 | ## Reproduce your problem 21 | 22 | ```python 23 | simple code to reproduce your problem 24 | ``` 25 | 26 | ## Error info 27 | 28 | ```shell 29 | # copy your log info at here from your terminal 30 | ``` 31 | 32 | ## Your experiments 33 | 34 | > please tell us your experiments and ideas about this issue. It's valuable for us to help you find the solution. 35 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: PyPI 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-python@v1 12 | with: 13 | python-version: 3.7 14 | - name: Install dependencies 15 | run: | 16 | python -m pip install --upgrade pip 17 | make install 18 | # - name: Test 19 | # run: make test 20 | 21 | pack: 22 | name: Pack 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-python@v1 28 | with: 29 | python-version: 3.7 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install setuptools wheel twine 34 | make install 35 | - name: Pack Testing 36 | run: | 37 | make dist 38 | echo "To be add: pack testing" 39 | deploy: 40 | if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/v')) 41 | name: Deploy 42 | needs: [ build, pack ] 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v2 46 | - uses: actions/setup-python@v1 47 | with: 48 | python-version: 3.7 49 | - name: Install dependencies 50 | run: | 51 | python -m pip install --upgrade pip 52 | pip install setuptools wheel twine 53 | make install 54 | - name: Check Branch 55 | id: check-branch 56 | run: | 57 | if [[ ${{ github.ref }} =~ ^refs/heads/(main|v[0-9]+\.[0-9]+.*)$ ]]; then 58 | echo ::set-output name=match::true 59 | fi # See: https://stackoverflow.com/a/58869470/1123955 60 | - name: Is A Publish Branch 61 | if: steps.check-branch.outputs.match == 'true' 62 | env: 63 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 64 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 65 | run: | 66 | make deploy-version 67 | python setup.py sdist bdist_wheel 68 | twine upload --skip-existing dist/* 69 | - name: Is Not A Publish Branch 70 | if: steps.check-branch.outputs.match != 'true' 71 | run: echo 'Not A Publish Branch' 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .pytype/ 131 | .idea/ 132 | 133 | test 134 | logs 135 | *.pkl 136 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # the hook execution directory in under git root directory 2 | repos: 3 | - repo: local 4 | hooks: 5 | 6 | - id: pylint 7 | name: pylint 8 | description: "Pylint: Checks for errors in Python code" 9 | language: system 10 | entry: make pylint 11 | require_serial: true 12 | stages: [push] 13 | types: [python] 14 | 15 | - id: pycodestyle 16 | name: pycodestyle 17 | description: "pycodestyle: Check your Python code against styles conventions in PEP 8" 18 | language: system 19 | entry: make pycodestyle 20 | require_serial: true 21 | stages: [push] 22 | types: [python] 23 | 24 | - id: flake8 25 | name: flake8 26 | description: "flake8: Tool For Style Guide Enforcement" 27 | language: system 28 | entry: make flake8 29 | require_serial: true 30 | stages: [push] 31 | types: [python] 32 | 33 | - id: mypy 34 | name: mypy 35 | description: "mypy: an optional static type checker for Python" 36 | language: system 37 | entry: make mypy 38 | require_serial: true 39 | stages: [push] 40 | types: [python] 41 | 42 | - id: pytest 43 | name: pytest 44 | description: "pytest: run python pytest unit test" 45 | language: system 46 | entry: make pytest 47 | require_serial: true 48 | stages: [push] 49 | types: [python] 50 | 51 | # - id: bump-version 52 | # name: bump-version 53 | # description: "Bumped Version: bump the version when a new commit come in" 54 | # language: system 55 | # entry: make version 56 | # require_serial: true 57 | # stages: [push] 58 | # types: [python] 59 | -------------------------------------------------------------------------------- /.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 | # https://github.com/pytorch/pytorch/issues/1942#issuecomment-315681074 7 | # extension-pkg-whitelist=numpy,torch 8 | 9 | # Add files or directories to the blacklist. They should be base names, not 10 | # paths. 11 | ignore=CVS 12 | 13 | # Add files or directories matching the regex patterns to the blacklist. The 14 | # regex matches against base names, not paths. 15 | ignore-patterns= 16 | 17 | # Python code to execute, usually for sys.path manipulation such as 18 | # pygtk.require(). 19 | #init-hook= 20 | 21 | # Use multiple processes to speed up Pylint. 22 | jobs=1 23 | 24 | # List of plugins (as comma separated values of python modules names) to load, 25 | # usually to register additional checkers. 26 | load-plugins= 27 | 28 | # Pickle collected data for later comparisons. 29 | persistent=yes 30 | 31 | # Specify a configuration file. 32 | #rcfile= 33 | 34 | # Allow loading of arbitrary C extensions. Extensions are imported into the 35 | # active Python interpreter and may run arbitrary code. 36 | unsafe-load-any-extension=no 37 | 38 | 39 | [MESSAGES CONTROL] 40 | 41 | # Only show warnings with the listed confidence levels. Leave empty to show 42 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 43 | confidence= 44 | 45 | # Disable the message, report, category or checker with the given id(s). You 46 | # can either give multiple identifiers separated by comma (,) or put this 47 | # option multiple times (only on the command line, not in the configuration 48 | # file where it should appear only once).You can also use "--disable=all" to 49 | # disable everything first and then reenable specific checks. For example, if 50 | # you want to run only the similarities checker, you can use "--disable=all 51 | # --enable=similarities". If you want to run only the classes checker, but have 52 | # no Warning level messages displayed, use"--disable=all --enable=classes 53 | # --disable=W" 54 | disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,C0103,W1401,E203,C0326 55 | 56 | # Enable the message, report, category or checker with the given id(s). You can 57 | # either give multiple identifier separated by comma (,) or put this option 58 | # multiple time (only on the command line, not in the configuration file where 59 | # it should appear only once). See also the "--disable" option for examples. 60 | enable= 61 | 62 | 63 | [REPORTS] 64 | 65 | # Python expression which should return a note less than 10 (10 is the highest 66 | # note). You have access to the variables errors warning, statement which 67 | # respectively contain the number of errors / warnings messages and the total 68 | # number of statements analyzed. This is used by the global evaluation report 69 | # (RP0004). 70 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 71 | 72 | # Template used to display messages. This is a python new-style format string 73 | # used to format the message information. See doc for all details 74 | #msg-template= 75 | 76 | # Set the output format. Available formats are text, parseable, colorized, json 77 | # and msvs (visual studio).You can also give a reporter class, eg 78 | # mypackage.mymodule.MyReporterClass. 79 | output-format=text 80 | 81 | # Tells whether to display a full report or only the messages 82 | reports=no 83 | 84 | # Activate the evaluation score. 85 | score=yes 86 | 87 | 88 | [REFACTORING] 89 | 90 | # Maximum number of nested blocks for function / method body 91 | max-nested-blocks=5 92 | 93 | 94 | [LOGGING] 95 | 96 | # Logging modules to check that the string format arguments are in logging 97 | # function parameter format 98 | logging-modules=logging 99 | 100 | 101 | [VARIABLES] 102 | 103 | # List of additional names supposed to be defined in builtins. Remember that 104 | # you should avoid to define new builtins when possible. 105 | additional-builtins= 106 | 107 | # Tells whether unused global variables should be treated as a violation. 108 | allow-global-unused-variables=yes 109 | 110 | # List of strings which can identify a callback function by name. A callback 111 | # name must start or end with one of those strings. 112 | callbacks=cb_,_cb 113 | 114 | # A regular expression matching the name of dummy variables (i.e. expectedly 115 | # not used). 116 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 117 | 118 | # Argument names that match this expression will be ignored. Default to name 119 | # with leading underscore 120 | ignored-argument-names=_.*|^ignored_|^unused_ 121 | 122 | # Tells whether we should check for unused import in __init__ files. 123 | init-import=no 124 | 125 | # List of qualified module names which can have objects that can redefine 126 | # builtins. 127 | redefining-builtins-modules=six.moves,future.builtins 128 | 129 | 130 | [TYPECHECK] 131 | 132 | # List of decorators that produce context managers, such as 133 | # contextlib.contextmanager. Add to this list to register other decorators that 134 | # produce valid context managers. 135 | contextmanager-decorators=contextlib.contextmanager 136 | 137 | # List of members which are set dynamically and missed by pylint inference 138 | # system, and so shouldn't trigger E1101 when accessed. Python regular 139 | # expressions are accepted. 140 | generated-members= 141 | 142 | # Tells whether missing members accessed in mixin class should be ignored. A 143 | # mixin class is detected if its name ends with "mixin" (case insensitive). 144 | ignore-mixin-members=yes 145 | 146 | # This flag controls whether pylint should warn about no-member and similar 147 | # checks whenever an opaque object is returned when inferring. The inference 148 | # can return multiple potential results while evaluating a Python object, but 149 | # some branches might not be evaluated, which results in partial inference. In 150 | # that case, it might be useful to still emit no-member and other checks for 151 | # the rest of the inferred objects. 152 | ignore-on-opaque-inference=yes 153 | 154 | # List of class names for which member attributes should not be checked (useful 155 | # for classes with dynamically set attributes). This supports the use of 156 | # qualified names. 157 | ignored-classes=optparse.Values,thread._local,_thread._local 158 | 159 | # List of module names for which member attributes should not be checked 160 | # (useful for modules/projects where namespaces are manipulated during runtime 161 | # and thus existing member attributes cannot be deduced by static analysis. It 162 | # supports qualified module names, as well as Unix pattern matching. 163 | ignored-modules= 164 | 165 | # Show a hint with possible names when a member name was not found. The aspect 166 | # of finding the hint is based on edit distance. 167 | missing-member-hint=yes 168 | 169 | # The minimum edit distance a name should have in order to be considered a 170 | # similar match for a missing member name. 171 | missing-member-hint-distance=1 172 | 173 | # The total number of similar names that should be taken in consideration when 174 | # showing a hint for a missing member. 175 | missing-member-max-choices=1 176 | 177 | 178 | [FORMAT] 179 | 180 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 181 | expected-line-ending-format= 182 | 183 | # Regexp for a line that is allowed to be longer than the limit. 184 | ignore-long-lines=^\s*(# )??$ 185 | 186 | # Number of spaces of indent required inside a hanging or continued line. 187 | indent-after-paren=4 188 | 189 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 190 | # tab). 191 | indent-string=' ' 192 | 193 | # Maximum number of characters on a single line. 194 | max-line-length=100 195 | 196 | # Maximum number of lines in a module 197 | max-module-lines=1000 198 | 199 | # List of optional constructs for which whitespace checking is disabled. `dict- 200 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 201 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 202 | # `empty-line` allows space-only lines. 203 | no-space-check=trailing-comma,dict-separator 204 | 205 | # Allow the body of a class to be on the same line as the declaration if body 206 | # contains single statement. 207 | single-line-class-stmt=no 208 | 209 | # Allow the body of an if to be on the same line as the test if there is no 210 | # else. 211 | single-line-if-stmt=no 212 | 213 | 214 | [SPELLING] 215 | 216 | # Spelling dictionary name. Available dictionaries: none. To make it working 217 | # install python-enchant package. 218 | spelling-dict= 219 | 220 | # List of comma separated words that should not be checked. 221 | spelling-ignore-words= 222 | 223 | # A path to a file that contains private dictionary; one word per line. 224 | spelling-private-dict-file= 225 | 226 | # Tells whether to store unknown words to indicated private dictionary in 227 | # --spelling-private-dict-file option instead of raising a message. 228 | spelling-store-unknown-words=no 229 | 230 | 231 | [SIMILARITIES] 232 | 233 | # Ignore comments when computing similarities. 234 | ignore-comments=yes 235 | 236 | # Ignore docstrings when computing similarities. 237 | ignore-docstrings=yes 238 | 239 | # Ignore imports when computing similarities. 240 | ignore-imports=no 241 | 242 | # Minimum lines number of a similarity. 243 | min-similarity-lines=4 244 | 245 | 246 | [MISCELLANEOUS] 247 | 248 | # List of note tags to take in consideration, separated by a comma. 249 | notes=FIXME,XXX,TODO 250 | 251 | 252 | [BASIC] 253 | 254 | # Huan(202003) 255 | string-quote=single-avoid-escape 256 | 257 | # Naming hint for argument names 258 | argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 259 | 260 | # Regular expression matching correct argument names 261 | argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 262 | 263 | # Naming hint for attribute names 264 | attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 265 | 266 | # Regular expression matching correct attribute names 267 | attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 268 | 269 | # Bad variable names which should always be refused, separated by a comma 270 | bad-names=foo,bar,baz,toto,tutu,tata 271 | 272 | # Naming hint for class attribute names 273 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 274 | 275 | # Regular expression matching correct class attribute names 276 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 277 | 278 | # Naming hint for class names 279 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 280 | 281 | # Regular expression matching correct class names 282 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 283 | 284 | # Naming hint for constant names 285 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 286 | 287 | # Regular expression matching correct constant names 288 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 289 | 290 | # Minimum line length for functions/classes that require docstrings, shorter 291 | # ones are exempt. 292 | docstring-min-length=-1 293 | 294 | # Naming hint for function names 295 | function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 296 | 297 | # Regular expression matching correct function names 298 | function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 299 | 300 | # Good variable names which should always be accepted, separated by a comma 301 | good-names=i,j,k,ex,Run,_ 302 | 303 | # Include a hint for the correct naming format with invalid-name 304 | include-naming-hint=no 305 | 306 | # Naming hint for inline iteration names 307 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 308 | 309 | # Regular expression matching correct inline iteration names 310 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 311 | 312 | # Naming hint for method names 313 | method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 314 | 315 | # Regular expression matching correct method names 316 | method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 317 | 318 | # Naming hint for module names 319 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 320 | 321 | # Regular expression matching correct module names 322 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 323 | 324 | # Colon-delimited sets of names that determine each other's naming style when 325 | # the name regexes allow several styles. 326 | name-group= 327 | 328 | # Regular expression which should only match function or class names that do 329 | # not require a docstring. 330 | no-docstring-rgx=^_ 331 | 332 | # List of decorators that produce properties, such as abc.abstractproperty. Add 333 | # to this list to register other decorators that produce valid properties. 334 | property-classes=abc.abstractproperty 335 | 336 | # Naming hint for variable names 337 | variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 338 | 339 | # Regular expression matching correct variable names 340 | variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 341 | 342 | 343 | [DESIGN] 344 | 345 | # Maximum number of arguments for function / method 346 | max-args=9 347 | 348 | # Maximum number of attributes for a class (see R0902). 349 | max-attributes=7 350 | 351 | # Maximum number of boolean expressions in a if statement 352 | max-bool-expr=5 353 | 354 | # Maximum number of branch for function / method body 355 | max-branches=12 356 | 357 | # Maximum number of locals for function / method body 358 | max-locals=15 359 | 360 | # Maximum number of parents for a class (see R0901). 361 | max-parents=7 362 | 363 | # Maximum number of public methods for a class (see R0904). 364 | max-public-methods=20 365 | 366 | # Maximum number of return / yield for function / method body 367 | max-returns=6 368 | 369 | # Maximum number of statements in function / method body 370 | max-statements=50 371 | 372 | # Minimum number of public methods for a class (see R0903). 373 | min-public-methods=2 374 | 375 | 376 | [IMPORTS] 377 | 378 | # Allow wildcard imports from modules that define __all__. 379 | allow-wildcard-with-all=no 380 | 381 | # Analyse import fallback blocks. This can be used to support both Python 2 and 382 | # 3 compatible code, which means that the block might have code that exists 383 | # only in one or another interpreter, leading to false positives when analysed. 384 | analyse-fallback-blocks=no 385 | 386 | # Deprecated modules which should not be used, separated by a comma 387 | deprecated-modules=optparse,tkinter.tix 388 | 389 | # Create a graph of external dependencies in the given file (report RP0402 must 390 | # not be disabled) 391 | ext-import-graph= 392 | 393 | # Create a graph of every (i.e. internal and external) dependencies in the 394 | # given file (report RP0402 must not be disabled) 395 | import-graph= 396 | 397 | # Create a graph of internal dependencies in the given file (report RP0402 must 398 | # not be disabled) 399 | int-import-graph= 400 | 401 | # Force import order to recognize a module as part of the standard 402 | # compatibility libraries. 403 | known-standard-library= 404 | 405 | # Force import order to recognize a module as part of a third party library. 406 | known-third-party=enchant 407 | 408 | 409 | [CLASSES] 410 | 411 | # List of method names used to declare (i.e. assign) instance attributes. 412 | defining-attr-methods=__init__,__new__,setUp 413 | 414 | # List of member names, which should be excluded from the protected access 415 | # warning. 416 | exclude-protected=_asdict,_fields,_replace,_source,_make 417 | 418 | # List of valid names for the first argument in a class method. 419 | valid-classmethod-first-arg=cls 420 | 421 | # List of valid names for the first argument in a metaclass class method. 422 | valid-metaclass-classmethod-first-arg=mcs 423 | 424 | 425 | [EXCEPTIONS] 426 | 427 | # Exceptions that will emit a warning when being caught. Defaults to 428 | # "Exception" 429 | overgeneral-exceptions=Exception 430 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Python Wechaty 2 | # 3 | # GitHb: https://github.com/wechaty/python-wechaty 4 | # Author: Huan LI git.io/zixia 5 | # 6 | 7 | SOURCE_GLOB=$(wildcard bin/*.py src/wechaty_puppet_itchat/puppet.py tests/**/*.py) 8 | 9 | # 10 | # Huan(202003) 11 | # F811: https://github.com/PyCQA/pyflakes/issues/320#issuecomment-469337000 12 | # 13 | IGNORE_PEP=E203,E221,E241,E272,E501,F811 14 | 15 | # help scripts to find the right place of wechaty module 16 | export PYTHONPATH=src/ 17 | 18 | .PHONY: all 19 | all : clean lint 20 | 21 | .PHONY: clean 22 | clean: 23 | rm -fr dist/* 24 | 25 | .PHONY: lint 26 | lint: pylint pycodestyle flake8 mypy pytype 27 | 28 | .PHONY: pylint 29 | pylint: 30 | pylint \ 31 | --load-plugins pylint_quotes \ 32 | --disable E0401,W0511,W1203,C0302 \ 33 | $(SOURCE_GLOB) 34 | 35 | .PHONY: pycodestyle 36 | pycodestyle: 37 | pycodestyle \ 38 | --statistics \ 39 | --count \ 40 | --ignore="${IGNORE_PEP}" \ 41 | $(SOURCE_GLOB) 42 | 43 | .PHONY: flake8 44 | flake8: 45 | flake8 \ 46 | --ignore="${IGNORE_PEP}" \ 47 | $(SOURCE_GLOB) 48 | 49 | .PHONY: mypy 50 | mypy: 51 | MYPYPATH=stubs/ mypy \ 52 | $(SOURCE_GLOB) 53 | 54 | .PHONE: pytype 55 | pytype: 56 | pytype \ 57 | --disable=import-error,pyi-error \ 58 | src/wechaty_puppet_itchat/puppet.py 59 | 60 | .PHONY: uninstall-git-hook 61 | uninstall-git-hook: 62 | pre-commit clean 63 | pre-commit gc 64 | pre-commit uninstall 65 | pre-commit uninstall --hook-type pre-push 66 | 67 | .PHONY: install-git-hook 68 | install-git-hook: 69 | # cleanup existing pre-commit configuration (if any) 70 | pre-commit clean 71 | pre-commit gc 72 | # setup pre-commit 73 | # Ensures pre-commit hooks point to latest versions 74 | pre-commit autoupdate 75 | pre-commit install 76 | pre-commit install --overwrite --hook-type pre-push 77 | 78 | .PHONY: install 79 | install: 80 | pip3 install -r requirements.txt 81 | pip3 install -r requirements-dev.txt 82 | # install pre-commit related hook scripts 83 | $(MAKE) install-git-hook 84 | 85 | .PHONY: pytest 86 | pytest: 87 | python3 -m pytest src/ tests/ -sv 88 | 89 | .PHONY: test-unit 90 | test-unit: pytest 91 | 92 | .PHONY: test 93 | test: lint pytest 94 | 95 | code: 96 | code . 97 | 98 | .PHONY: run 99 | run: 100 | python3 bin/run.py 101 | 102 | .PHONY: dist 103 | dist: 104 | python3 setup.py sdist bdist_wheel 105 | 106 | .PHONY: publish 107 | publish: 108 | PATH=~/.local/bin:${PATH} twine upload dist/* 109 | 110 | .PHONY: version 111 | version: 112 | @newVersion=$$(awk -F. '{print $$1"."$$2"."$$3+1}' < VERSION) \ 113 | && echo $${newVersion} > VERSION \ 114 | && git add VERSION \ 115 | && git commit -m "🔥 update version to $${newVersion}" > /dev/null \ 116 | && git tag "v$${newVersion}" \ 117 | && echo "Bumped version to $${newVersion}" 118 | 119 | .PHONY: deploy-version 120 | deploy-version: 121 | echo "VERSION = '$$(cat VERSION)'" > src/wechaty_puppet_itchat/version.py 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wechaty-puppet-itchat [![Python 3.7](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/release/python-370/) 2 | 3 | [![Powered by Wechaty](https://img.shields.io/badge/Powered%20By-Wechaty-brightgreen.svg)](https://github.com/wechaty/wechaty) 4 | 5 | ![Service](https://wechaty.github.io/wechaty-puppet-service/images/hostie.png) 6 | 7 | Python Puppet for Wechaty 8 | 9 | # Quick Start 10 | 11 | ## Installation 12 | 13 | ```shell 14 | pip install wechaty-puppet-itchat 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```python 20 | import asyncio 21 | from wechaty_puppet_itchat import PuppetItChat 22 | from wechaty_puppet import PuppetOptions 23 | from wechaty import Wechaty, WechatyOptions 24 | 25 | 26 | async def main(): 27 | options = WechatyOptions( 28 | puppet=PuppetItChat(PuppetOptions()) 29 | ) 30 | bot = Wechaty(options) 31 | await bot.start() 32 | 33 | asyncio.run(main()) 34 | 35 | 36 | ``` 37 | 38 | ## History 39 | 40 | ### v0.0.6 (Oct 03, 2022) 41 | 42 | 1. enable itchat great again by @fangjiyuan 43 | 2. itchat example 44 | 45 | ### v0.0.4 (October 12, 2021) 46 | 47 | 1. Fix Bugs 48 | 2. Move src/itchat to src/wechaty_puppet_itchat/itchat 49 | 3. Add examples 50 | 51 | ### v0.0.3 (September 27, 2021) 52 | 53 | 1. Fix Bugs 54 | 55 | ### v0.0.2 (September 18, 2021) 56 | 57 | 1. Fix Bugs 58 | 2. Add Receive Message 59 | 60 | ### v0.0.1 (September 10, 2021) 61 | 62 | 1. Add CI/CD 63 | 2. Add Scan/Login 64 | 3. Add Send Message 65 | 66 | ### v0.0.0 (July 1, 2021) 67 | 68 | 1. Init Code 69 | 70 | ## Authors 71 | 72 | - [@Lyle](https://github.com/lyleshaw) - Lyle Shaw (肖良玉) 73 | - [@wj-Mcat](https://github.com/wj-Mcat) - Jingjing WU (吴京京) 74 | 75 | ## Copyright & License 76 | 77 | * Code & Docs © 2020-now Huan LI \ 78 | * Code released under the Apache-2.0 License 79 | * Docs released under Creative Commons 80 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.6 2 | -------------------------------------------------------------------------------- /examples/ding-dong-bot.py: -------------------------------------------------------------------------------- 1 | """example code for ding-dong-bot with oop style""" 2 | from typing import List, Optional, Union 3 | import asyncio 4 | from datetime import datetime 5 | from wechaty_puppet import get_logger 6 | from wechaty import ( 7 | MessageType, 8 | FileBox, 9 | RoomMemberQueryFilter, 10 | Wechaty, 11 | Contact, 12 | Room, 13 | Message, 14 | Image, 15 | MiniProgram, 16 | Friendship, 17 | FriendshipType, 18 | EventReadyPayload 19 | ) 20 | 21 | logger = get_logger(__name__) 22 | 23 | 24 | class MyBot(Wechaty): 25 | """ 26 | listen wechaty event with inherited functions, which is more friendly for 27 | oop developer 28 | """ 29 | 30 | def __init__(self) -> None: 31 | """initialization function 32 | """ 33 | self.login_user: Optional[Contact] = None 34 | super().__init__() 35 | 36 | async def on_ready(self, payload: EventReadyPayload) -> None: 37 | """listen for on-ready event""" 38 | logger.info('ready event %s...', payload) 39 | 40 | # pylint: disable=R0912,R0914,R0915 41 | async def on_message(self, msg: Message) -> None: 42 | """ 43 | listen for message event 44 | """ 45 | from_contact: Contact = msg.talker() 46 | text: str = msg.text() 47 | room: Optional[Room] = msg.room() 48 | msg_type: MessageType = msg.type() 49 | file_box: Optional[FileBox] = None 50 | if text == 'ding': 51 | conversation: Union[ 52 | Room, Contact] = from_contact if room is None else room 53 | await conversation.ready() 54 | await conversation.say('dong') 55 | file_box = FileBox.from_url( 56 | 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/' 57 | 'u=1116676390,2305043183&fm=26&gp=0.jpg', 58 | name='ding-dong.jpg') 59 | await conversation.say(file_box) 60 | 61 | elif msg_type == MessageType.MESSAGE_TYPE_IMAGE: 62 | logger.info('receving image file') 63 | # file_box: FileBox = await msg.to_file_box() 64 | image: Image = msg.to_image() 65 | 66 | hd_file_box: FileBox = await image.hd() 67 | await hd_file_box.to_file('./hd-image.jpg', overwrite=True) 68 | 69 | thumbnail_file_box: FileBox = await image.thumbnail() 70 | await thumbnail_file_box.to_file('./thumbnail-image.jpg', overwrite=True) 71 | artwork_file_box: FileBox = await image.artwork() 72 | await artwork_file_box.to_file('./artwork-image.jpg', overwrite=True) 73 | # reply the image 74 | await msg.say(hd_file_box) 75 | 76 | # pylint: disable=C0301 77 | elif msg_type in [MessageType.MESSAGE_TYPE_AUDIO, MessageType.MESSAGE_TYPE_ATTACHMENT, MessageType.MESSAGE_TYPE_VIDEO]: 78 | logger.info('receving file ...') 79 | file_box = await msg.to_file_box() 80 | if file_box: 81 | await file_box.to_file(file_box.name) 82 | 83 | elif msg_type == MessageType.MESSAGE_TYPE_MINI_PROGRAM: 84 | logger.info('receving mini-program ...') 85 | mini_program: Optional[MiniProgram] = await msg.to_mini_program() 86 | if mini_program: 87 | await msg.say(mini_program) 88 | 89 | elif text == 'get room members' and room: 90 | logger.info('get room members ...') 91 | room_members: List[Contact] = await room.member_list() 92 | names: List[str] = [ 93 | room_member.name for room_member in room_members] 94 | await msg.say('\n'.join(names)) 95 | 96 | elif text.startswith('remove room member:'): 97 | logger.info('remove room member:') 98 | if not room: 99 | await msg.say('this is not room zone') 100 | return 101 | 102 | room_member_name = text[len('remove room member:') + 1:] 103 | 104 | room_member: Optional[Contact] = await room.member( 105 | query=RoomMemberQueryFilter(name=room_member_name) 106 | ) 107 | if room_member: 108 | if self.login_user and self.login_user.contact_id in room.payload.admin_ids: 109 | await room.delete(room_member) 110 | else: 111 | await msg.say('登录用户不是该群管理员...') 112 | 113 | else: 114 | await msg.say(f'can not fine room member by name<{room_member_name}>') 115 | elif text.startswith('get room topic'): 116 | logger.info('get room topic') 117 | if room: 118 | topic: Optional[str] = await room.topic() 119 | if topic: 120 | await msg.say(topic) 121 | 122 | elif text.startswith('rename room topic:'): 123 | logger.info('rename room topic ...') 124 | if room: 125 | new_topic = text[len('rename room topic:') + 1:] 126 | await msg.say(new_topic) 127 | elif text.startswith('add new friend:'): 128 | logger.info('add new friendship ...') 129 | identity_info = text[len('add new friend:'):] 130 | weixin_contact: Optional[Contact] = await self.Friendship.search(weixin=identity_info) 131 | phone_contact: Optional[Contact] = await self.Friendship.search(phone=identity_info) 132 | contact: Optional[Contact] = weixin_contact or phone_contact 133 | if contact: 134 | await self.Friendship.add(contact, 'hello world ...') 135 | 136 | elif text.startswith('at me'): 137 | if room: 138 | talker = msg.talker() 139 | await room.say('hello', mention_ids=[talker.contact_id]) 140 | 141 | elif text.startswith('my alias'): 142 | talker = msg.talker() 143 | alias = await talker.alias() 144 | await msg.say('your alias is:' + (alias or '')) 145 | 146 | elif text.startswith('set alias:'): 147 | talker = msg.talker() 148 | new_alias = text[len('set alias:'):] 149 | 150 | # set your new alias 151 | alias = await talker.alias(new_alias) 152 | # get your new alias 153 | alias = await talker.alias() 154 | await msg.say('your new alias is:' + (alias or '')) 155 | 156 | elif text.startswith('find friends:'): 157 | friend_name: str = text[len('find friends:'):] 158 | friend = await self.Contact.find(friend_name) 159 | if friend: 160 | logger.info('find only one friend <%s>', friend) 161 | 162 | friends: List[Contact] = await self.Contact.find_all(friend_name) 163 | 164 | logger.info('find friend<%d>', len(friends)) 165 | logger.info(friends) 166 | 167 | else: 168 | pass 169 | 170 | if msg.type() == MessageType.MESSAGE_TYPE_UNSPECIFIED: 171 | talker = msg.talker() 172 | assert isinstance(talker, Contact) 173 | 174 | async def on_login(self, contact: Contact) -> None: 175 | """login event 176 | 177 | Args: 178 | contact (Contact): the account logined 179 | """ 180 | logger.info('Contact<%s> has logined ...', contact) 181 | self.login_user = contact 182 | 183 | async def on_friendship(self, friendship: Friendship) -> None: 184 | """when receive a new friendship application, or accept a new friendship 185 | 186 | Args: 187 | friendship (Friendship): contains the status and friendship info, 188 | eg: hello text, friend contact object 189 | """ 190 | MAX_ROOM_MEMBER_COUNT = 500 191 | # 1. receive a new friendship from someone 192 | if friendship.type() == FriendshipType.FRIENDSHIP_TYPE_RECEIVE: 193 | hello_text: str = friendship.hello() 194 | 195 | # accept friendship when there is a keyword in hello text 196 | if 'wechaty' in hello_text.lower(): 197 | await friendship.accept() 198 | 199 | # 2. you have a new friend to your contact list 200 | elif friendship.type() == FriendshipType.FRIENDSHIP_TYPE_CONFIRM: 201 | # 2.1 invite the user to wechaty group 202 | # find the topic of room which contains Wechaty keyword 203 | wechaty_rooms: List[Room] = await self.Room.find_all('Wechaty') 204 | 205 | # 2.2 find the suitable room 206 | for wechaty_room in wechaty_rooms: 207 | members: List[Contact] = await wechaty_room.member_list() 208 | if len(members) < MAX_ROOM_MEMBER_COUNT: 209 | contact: Contact = friendship.contact() 210 | await wechaty_room.add(contact) 211 | break 212 | 213 | async def on_room_join(self, room: Room, invitees: List[Contact], 214 | inviter: Contact, date: datetime) -> None: 215 | """on_room_join when there are new contacts to the room 216 | 217 | Args: 218 | room (Room): the room instance 219 | invitees (List[Contact]): the new contacts to the room 220 | inviter (Contact): the inviter who share qrcode or manual invite someone 221 | date (datetime): the datetime to join the room 222 | """ 223 | # 1. say something to welcome the new arrivals 224 | names: List[str] = [] 225 | for invitee in invitees: 226 | await invitee.ready() 227 | names.append(invitee.name) 228 | 229 | await room.say(f'welcome {",".join(names)} to the wechaty group !') 230 | 231 | 232 | async def main() -> None: 233 | """doc""" 234 | bot = MyBot() 235 | await bot.start() 236 | 237 | asyncio.run(main()) 238 | -------------------------------------------------------------------------------- /examples/hotreload_bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import uvicorn 3 | from fastapi import Depends, FastAPI 4 | from wechaty import Wechaty, get_logger, ContactQueryFilter, WechatyOptions,Contact,FileBox,Message,RoomQueryFilter 5 | import sys 6 | import os 7 | if os.name == 'nt': 8 | sys.path.insert(0,os.path.realpath(f'{os.path.abspath(os.path.dirname(os.path.dirname(__file__)))}\src')) 9 | print(os.path.realpath(f'{os.path.abspath(os.path.dirname(os.path.dirname(__file__)))}\src')) 10 | sys.path.insert(0,os.path.realpath(f'{os.path.abspath(os.path.dirname(os.path.dirname(__file__)))}/src')) 11 | 12 | # sys.path.append('..\src\wechaty_puppet_itchat\itchat') 13 | from wechaty_puppet_itchat import PuppetItChat 14 | from grpclib.exceptions import StreamTerminatedError 15 | from pydantic import BaseModel 16 | import requests 17 | import os 18 | from wechaty_puppet.exceptions import WechatyPuppetError 19 | 20 | os.environ['ASYNC_COMPONENTS'] = 'ITCHAT_UOS_ASYNC' 21 | welcome = """=============== Powered by Python-Wechaty-puppet-itchat =============== 22 | 修改代码后可以热重启的bot示例。 23 | """ 24 | print(welcome) 25 | log = get_logger('MyBot') 26 | 27 | # 创建json数据模型 28 | class Item(BaseModel): 29 | msg_type: str 30 | name: str = None 31 | msg: str = None 32 | imagebase64: bytes = None 33 | contact_id: str = None 34 | alias: str = None 35 | weixin: str = None 36 | 37 | 38 | # 继承wechaty类,并设置回调函数 39 | class MyBot(Wechaty): 40 | def __call__(self): 41 | return self 42 | async def start(self): 43 | """ 44 | start wechaty bot 45 | :return: 46 | """ 47 | try: 48 | 49 | await self.init_puppet() 50 | await self.init_puppet_event_bridge(self.puppet) 51 | 52 | log.info('starting puppet ...') 53 | await self.puppet.start() 54 | 55 | self.started = True 56 | 57 | except (requests.exceptions.ConnectionError, StreamTerminatedError, OSError): 58 | 59 | log.error('The network is not good, the bot will try to restart after 60 seconds.') 60 | await asyncio.sleep(60) 61 | await self.restart() 62 | 63 | else: 64 | pass 65 | 66 | # 创建一个bot实例 67 | puppet = PuppetItChat(options=None) 68 | bot = MyBot(options=WechatyOptions(puppet=puppet)) 69 | app = FastAPI() 70 | 71 | 72 | # 设置web启动运行bot实例 73 | @app.on_event("startup") 74 | async def bot_main(): 75 | asyncio.create_task(bot.start()) 76 | return 200 77 | 78 | # 设置发送消息函数 79 | @app.get('/send_msg2') 80 | def send_msg2(): 81 | return 200 82 | # 设置发送消息函数 83 | @app.post('/send_msg') 84 | async def send_msg(item: Item, bot=Depends(bot)): 85 | try: 86 | await send_report(bot, item.msg_type, item.name, item.msg) 87 | return 200 88 | except Exception as e: 89 | log.exception(e) 90 | return 404 91 | 92 | async def send_report(bot, msg_type, name, msg): 93 | log.info('Bot_' + 'send_report()') 94 | try: 95 | if msg_type == 'group_msg': 96 | # room = bot.puppet.itchat.search_chatrooms(name = name ) 97 | # room = room[0]['UserName'] 98 | # room = bot.Room.load(room) 99 | # await room.say(msg) 100 | room = await bot.Room.find(query=RoomQueryFilter(topic=name)) 101 | await room.say(msg) 102 | elif msg_type == 'private_msg': 103 | contact = bot.puppet.itchat.search_friends(name=name) 104 | print(contact) 105 | # contact =await bot.Contact.find(query=ContactQueryFilter(name=name)) 106 | contact = contact[0].get('UserName') 107 | contact = bot.Contact.load(contact) 108 | await contact.say(msg) 109 | # print(contact["contact_id"]) 110 | # await bot.puppet.message_send_text(conversation_id=contact[1].get("UserName"),message=msg) 111 | # await bot.puppet.itchat.send_msg(msg = 'test', toUserName=name) 112 | return 200 113 | else: 114 | return 0 115 | except Exception as e: 116 | log.exception(e) 117 | # 118 | # 设置web退出关闭bot实例 119 | @app.on_event("shutdown") 120 | async def bot_stop(): 121 | asyncio.create_task(bot.stop()) 122 | return 200 123 | # async def main(): 124 | # config = uvicorn.Config("hotreload_bot:app",port=19002,log_level='info',reload=True) 125 | # server = uvicorn.Server(config) 126 | # await server.serve() 127 | # 128 | # 主程序启动web运行 129 | if __name__ == '__main__': 130 | # hotreload_bot为python文件名称 131 | # 本地运行可设置host为127.0.0.1 132 | # 远程调用可设置host为0.0.0.1,Port需要放行(友情提示:如果用云主机需要在云端放行端口) 133 | uvicorn.run(app="hotreload_bot:app", host="127.0.0.1", port=19002, reload=True, log_level="info",debug=True) 134 | # asyncio.run(main()) 135 | -------------------------------------------------------------------------------- /examples/http_bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from quart import Quart 3 | from wechaty import Wechaty, get_logger, ContactQueryFilter,RoomQueryFilter, WechatyOptions,Contact,FileBox,Message,WechatyPlugin,RoomQueryFilter 4 | from quart import Quart,request 5 | import sys 6 | import os 7 | if os.name == 'nt': 8 | sys.path.insert(0,os.path.realpath(f'{os.path.abspath(os.path.dirname(os.path.dirname(__file__)))}\src')) 9 | sys.path.insert(0,os.path.realpath(f'{os.path.abspath(os.path.dirname(os.path.dirname(__file__)))}/src')) 10 | 11 | from wechaty_puppet_itchat import PuppetItChat 12 | from wechaty_plugin_contrib.contrib.ding_dong_plugin import DingDongPlugin 13 | import os 14 | os.environ['ASYNC_COMPONENTS'] = 'ITCHAT_UOS_ASYNC' 15 | welcome = """=============== Powered by Python-Wechaty-puppet-itchat =============== 16 | 修改代码后可以热重启的bot示例。 17 | """ 18 | print(welcome) 19 | log = get_logger('MyBot') 20 | class httpbotplugin(WechatyPlugin): 21 | async def blueprint(self, app: Quart) -> None: 22 | 23 | @app.get('/send_msg2') 24 | async def say_hello(): 25 | return 'sucess' 26 | @app.post('/send_msg') 27 | async def send_msg(): 28 | item = await request.get_json() 29 | try: 30 | msg = get_file_url_str(item.get('msg'),None) 31 | await send_report(item.get('msg_type'), item.get('name'), msg) 32 | return '200' 33 | except Exception as e: 34 | log.exception(e) 35 | return 404 36 | 37 | async def send_report(msg_type, name, msg): 38 | log.info('Bot_' + 'send_report()') 39 | try: 40 | if msg_type == 'group_msg': 41 | room = await self.bot.Room.find(query = RoomQueryFilter(topic = name)) 42 | await room.ready() 43 | await room.say(msg) 44 | elif msg_type == 'private_msg': 45 | contact = await self.bot.Contact.find(query=ContactQueryFilter(name = name)) 46 | await contact.say(msg) 47 | return '200' 48 | else: 49 | return '0' 50 | except Exception as e: 51 | log.exception(e) 52 | def get_file_url_str(path,base64_code=None): 53 | if base64_code is None: 54 | if path.startswith(('http://','https://','HTTP://','HTTPS://')): 55 | if path.endswith(('.xls','.xlsx','.pdf','.txt','csv','.doc','.docx','.XLS','.XLSX','.PDF','.TXT','.CSV','.DOC','.DOCX')): 56 | #获取url里的文件后缀 57 | filename = os.path.basename(path) 58 | #传送网络文件 59 | fileBox = FileBox.from_url(path,filename) 60 | #fileBox=path+'1' 61 | elif path.endswith(('BMP','JPG','JPEG','PNG','GIF','bmp','jpg','jpeg','png','gif')): 62 | #传送网络图片 63 | fileBox = FileBox.from_url(path,'linshi.jpg') 64 | #fileBox=path+'2' 65 | else: 66 | #传送无后缀的网络图片 67 | fileBox = FileBox.from_url(path,'linshi.jpg') 68 | #fileBox=path+'3' 69 | #发送本地文件 70 | #fileBox = FileBox.from_file('/Users/fangjiyuan/Desktop/一组外呼情况汇总.xlsx') 71 | 72 | elif path.startswith('/') or path.startswith('\\',2): 73 | if path.endswith(('.xls','.xlsx','.pdf','.txt','csv','.doc','.docx','.XLS','.XLSX','.PDF','.TXT','.CSV','.DOC','.DOCX')): 74 | #获取本地路径里的文件后缀 75 | filename = os.path.basename(path) 76 | #传送本地文件 77 | fileBox = FileBox.from_file(path,filename) 78 | #fileBox=path+'1' 79 | elif path.endswith(('BMP','JPG','JPEG','PNG','GIF','bmp','jpg','jpeg','png','gif')): 80 | #传送本地图片 81 | fileBox = FileBox.from_file(path,'linshi.jpg') 82 | #fileBox=path+'2' 83 | elif path.endswith('silk'): 84 | fileBox = FileBox.from_file(path,'linshi.silk') 85 | elif path.endswith(('MP4','mp4')): 86 | fileBox = FileBox.from_file(path,'tmp.mp4') 87 | else: 88 | #传送本地无后缀的图片 89 | fileBox = FileBox.from_file(path,'linshi.jpg') 90 | else: 91 | #发送纯文本 92 | fileBox = path 93 | else: 94 | fileBox = FileBox.from_base64(base64_code,'linshi.jpg') 95 | return fileBox 96 | 97 | 98 | # 主程序启动web运行 99 | if __name__ == '__main__': 100 | puppet = PuppetItChat(options=None) 101 | bot = Wechaty(options=WechatyOptions(puppet=puppet,host='127.0.0.1',port=19002)) 102 | bot.use([httpbotplugin(),DingDongPlugin()]) 103 | asyncio.run(bot.start()) 104 | -------------------------------------------------------------------------------- /examples/room-bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import List, Optional 3 | 4 | from wechaty_grpc.wechaty.puppet import MessageType 5 | 6 | from wechaty_puppet_itchat.puppet import PuppetItChat, PuppetOptions 7 | 8 | from wechaty import ( 9 | Wechaty, 10 | Message, 11 | WechatyOptions, 12 | FileBox, 13 | Contact, 14 | Friendship 15 | ) 16 | from loguru import logger 17 | 18 | SUPPORTED_MESSAGE_FILE_TYPES: List[MessageType] = [ 19 | MessageType.MESSAGE_TYPE_ATTACHMENT, 20 | MessageType.MESSAGE_TYPE_EMOTICON, 21 | MessageType.MESSAGE_TYPE_IMAGE, 22 | MessageType.MESSAGE_TYPE_VIDEO 23 | ] 24 | 25 | 26 | class Bot(Wechaty): 27 | async def on_scan(self, qr_code: str, status, 28 | data=None): 29 | pass 30 | 31 | async def on_login(self, contact: Contact): 32 | contact = self.Contact.load(contact_id='filehelper') 33 | await contact.say('hi') 34 | 35 | async def on_message(self, msg: Message): 36 | logger.info('receive on message event ...') 37 | text = msg.text() 38 | talker = msg.talker() if msg.room() is None else msg.room() 39 | 40 | if msg.type() in SUPPORTED_MESSAGE_FILE_TYPES: 41 | file_box: Optional[FileBox] = await msg.to_file_box() 42 | await file_box.to_file(file_box.name, overwrite=True) 43 | await talker.say(file_box) 44 | 45 | file_box = FileBox.from_url( 46 | 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/' 47 | 'u=1116676390,2305043183&fm=26&gp=0.jpg', 48 | name='ding-dong.jpg') 49 | 50 | file_box1 = FileBox.from_url( 51 | 'https://arxiv.org/pdf/2102.03322.pdf', 52 | name='2102.03322.pdf') 53 | 54 | if text == 'ding': 55 | await talker.say('dong') 56 | if text == 'img': 57 | await talker.say(file_box) 58 | if text == 'file': 59 | await talker.say(file_box1) 60 | if text == 'get_room_topic': 61 | topic = await msg.room().topic() 62 | print(topic) 63 | await talker.say(topic) 64 | 65 | async def on_friendship(self, friendship: Friendship): 66 | await friendship.accept() 67 | 68 | 69 | async def main(): 70 | puppet = PuppetItChat( 71 | options=PuppetOptions() 72 | ) 73 | bot = Bot(options=WechatyOptions( 74 | puppet=puppet, 75 | )) 76 | await bot.start() 77 | 78 | 79 | asyncio.run(main()) 80 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | mypy 3 | mypy-extensions 4 | pycodestyle 5 | pylint 6 | pylint-quotes 7 | pytest 8 | pytype 9 | semver 10 | wechaty_grpc 11 | wechaty-puppet~=0.3dev2 12 | pre-commit 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyppeteer 2 | dataclasses_json 3 | pyee 4 | requests 5 | PyQRCode 6 | dataclasses 7 | grpclib 8 | wechaty 9 | loguru 10 | # git+git://github.com/why2lyj/ItChat-UOS@master#egg=itchat 11 | itchat-uos==1.5.0.dev0 12 | node-semver~=0.8.1 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | setup 3 | """ 4 | import semver 5 | import setuptools 6 | 7 | 8 | def versioning(version: str) -> str: 9 | """version to specification""" 10 | sem_ver = semver.parse(version, loose=None) 11 | 12 | # major = sem_ver['major'] 13 | # minor = sem_ver['minor'] 14 | # patch = str(sem_ver['patch']) 15 | # 16 | # if minor % 2: 17 | # patch = 'dev' + patch 18 | # 19 | # fin_ver = '%d.%d.%s' % ( 20 | # major, 21 | # minor, 22 | # patch, 23 | # ) 24 | 25 | return sem_ver 26 | 27 | 28 | def get_install_requires() -> str: 29 | """get install_requires""" 30 | with open('requirements.txt', 'r') as requirements_fh: 31 | return requirements_fh.read().splitlines() 32 | 33 | 34 | def setup() -> None: 35 | """setup""" 36 | 37 | with open('README.md', 'r') as fh: 38 | long_description = fh.read() 39 | 40 | version = '0.0.2' 41 | with open('VERSION', 'r') as fh: 42 | version = versioning(fh.readline()) 43 | 44 | setuptools.setup( 45 | name='wechaty-puppet-itchat', 46 | version=version, 47 | author='Lyle Shaw', 48 | author_email='x@lyleshaw.com', 49 | description='Itchat Puppet for Wechaty', 50 | long_description=long_description, 51 | long_description_content_type='text/markdown', 52 | license='Apache-2.0', 53 | url='https://github.com/lyleshaw/python-wechaty-puppet-itchat', 54 | packages=setuptools.find_packages('src'), 55 | package_dir={'': 'src'}, 56 | install_requires=get_install_requires(), 57 | classifiers=[ 58 | 'Programming Language :: Python :: 3.7', 59 | 'License :: OSI Approved :: Apache Software License', 60 | 'Operating System :: OS Independent', 61 | ], 62 | ) 63 | 64 | 65 | setup() 66 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | doc 3 | """ 4 | from .puppet import PuppetItChat 5 | 6 | from .version import VERSION 7 | 8 | 9 | __version__ = VERSION 10 | 11 | __all__ = [ 12 | 'PuppetItChat', 13 | 14 | '__version__' 15 | ] 16 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/browser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Jingjing WU (吴京京) 5 | 6 | 2020-now @ Copyright Wechaty 7 | 8 | Licensed under the Apache License, Version 2.0 (the 'License'); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an 'AS IS' BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | """ 20 | from __future__ import annotations 21 | 22 | import os 23 | import pickle 24 | import random 25 | import re 26 | import time 27 | from typing import Optional 28 | from datetime import datetime 29 | 30 | from requests import Session 31 | from wechaty_puppet import get_logger 32 | from dataclasses import dataclass, field 33 | 34 | from wechaty_puppet.exceptions import WechatyPuppetError 35 | from wechaty_puppet_itchat.config import ( 36 | CACHE_DIR, 37 | BASE_URL, 38 | USER_AGENT, 39 | UOS_PATCH_EXTSPAM, 40 | UOS_PATCH_CLIENT_VERSION, 41 | LOGIN_TIMEOUT 42 | ) 43 | 44 | logger = get_logger('Browser') 45 | 46 | WX_UIN = 'wxuin' 47 | 48 | @dataclass 49 | class LoginCode: 50 | uuid: str 51 | datetime: datetime = field(default_factory=datetime.now) 52 | 53 | def is_timeout(self) -> bool: 54 | """check if the uuid is timeout""" 55 | now = datetime.now() 56 | return (now - self.datetime).seconds > LOGIN_TIMEOUT 57 | 58 | 59 | class Browser: 60 | _session: Optional[Session] = None 61 | 62 | def __init__(self, session: Session): 63 | """every instance """ 64 | self.session = session 65 | self.is_alive: bool = False 66 | 67 | self.login_info: dict = { 68 | 'login_uuid': None 69 | } 70 | 71 | # 1. init login code 72 | uuid = self.get_qr_uuid() 73 | if not uuid: 74 | raise WechatyPuppetError('can"t fetch the login info from server ...') 75 | 76 | self.login_code: LoginCode = LoginCode( 77 | uuid=uuid 78 | ) 79 | 80 | @staticmethod 81 | def instance() -> Browser: 82 | """singleton instance for global session""" 83 | if Browser._session: 84 | return Browser(Browser._session) 85 | 86 | # 1. load form 87 | os.makedirs(CACHE_DIR, exist_ok=True) 88 | cache_file = os.path.join(CACHE_DIR, 'session.pkl') 89 | if os.path.exists(cache_file): 90 | with open(cache_file, 'rb', encoding='utf-8') as f: 91 | session = pickle.load(f) 92 | assert isinstance(session, Session) 93 | return Browser(session) 94 | 95 | return Browser(Session()) 96 | 97 | def get_login_uuid(self) -> Optional[str]: 98 | """get login uuid of qrcode""" 99 | logger.info('get login info ...') 100 | cookies: dict = self.session.cookies.get_dict() 101 | if WX_UIN in cookies: 102 | url = f'{BASE_URL}/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin={cookies[WX_UIN]}' 103 | headers = {'User-Agent': USER_AGENT} 104 | r = self.session.get(url, headers=headers).json() 105 | if 'uuid' in r and r.get('ret') in (0, '0'): 106 | return r['uuid'] 107 | return None 108 | 109 | def have_login(self, uuid: str) -> bool: 110 | url = '%s/cgi-bin/mmwebwx-bin/login' % BASE_URL 111 | local_time = int(time.time()) 112 | params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % ( 113 | uuid, int(-local_time / 1579), local_time) 114 | headers = {'User-Agent': USER_AGENT} 115 | response = self.session.get(url, params=params, headers=headers) 116 | regx = r'window.code=(\d+)' 117 | data = re.search(regx, response.text) 118 | if data and data.group(1) == '200': 119 | self.init_login_info(response.text) 120 | return self.is_alive 121 | return False 122 | 123 | def init_login_info(self, login_str): 124 | regx = r'window.redirect_uri="(\S+)";' 125 | self.login_info['url'] = re.search(regx, login_str).group(1) 126 | headers = {'User-Agent': USER_AGENT, 127 | 'client-version': UOS_PATCH_CLIENT_VERSION, 128 | 'extspam': UOS_PATCH_EXTSPAM, 129 | 'referer': 'https://wx.qq.com/?&lang=zh_CN&target=t' 130 | } 131 | 132 | response = self.session.get( 133 | self.login_info['url'], 134 | headers=headers, 135 | allow_redirects=False 136 | ) 137 | 138 | self.login_info['url'] = self.login_info['url'][:self.login_info['url'].rfind('/')] 139 | for indexUrl, detailedUrl in ( 140 | ("wx2.qq.com", ("file.wx2.qq.com", "webpush.wx2.qq.com")), 141 | ("wx8.qq.com", ("file.wx8.qq.com", "webpush.wx8.qq.com")), 142 | ("qq.com", ("file.wx.qq.com", "webpush.wx.qq.com")), 143 | ("web2.wechat.com", ("file.web2.wechat.com", "webpush.web2.wechat.com")), 144 | ("wechat.com", ("file.web.wechat.com", "webpush.web.wechat.com"))): 145 | file_url, sync_url = ['https://%s/cgi-bin/mmwebwx-bin' % url for url in detailedUrl] 146 | if indexUrl in self.login_info['url']: 147 | self.login_info['fileUrl'], self.login_info['syncUrl'] = \ 148 | file_url, sync_url 149 | break 150 | else: 151 | self.login_info['fileUrl'] = self.login_info['syncUrl'] = self.login_info['url'] 152 | self.login_info['deviceid'] = 'e' + repr(random.random())[2:17] 153 | self.login_info['logintime'] = int(time.time() * 1e3) 154 | self.login_info['BaseRequest'] = {} 155 | cookies = self.session.cookies.get_dict() 156 | self.login_info['skey'] = self.login_info['BaseRequest']['Skey'] = "" 157 | self.login_info['wxsid'] = self.login_info['BaseRequest']['Sid'] = cookies["wxsid"] 158 | self.login_info['wxuin'] = self.login_info['BaseRequest']['Uin'] = cookies["wxuin"] 159 | self.login_info['pass_ticket'] = self.login_info['BaseRequest']['DeviceID'] = self.login_info['deviceid'] 160 | if not all([key in self.login_info for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]): 161 | logger.error('Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % response.text) 162 | self.is_alive = False 163 | else: 164 | self.is_alive = True 165 | 166 | def get_qr_uuid(self) -> Optional[str]: 167 | url = '%s/jslogin' % BASE_URL 168 | params = { 169 | 'appid': 'wx782c26e4c19acffb', 170 | 'fun': 'new', 171 | 'redirect_uri': 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop', 172 | 'lang': 'zh_CN'} 173 | headers = {'User-Agent': USER_AGENT} 174 | response = self.session.get(url, params=params, headers=headers) 175 | regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";' 176 | data = re.search(regx, response.text) 177 | if data and data.group(1) == '200': 178 | return data.group(2) 179 | return None 180 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Huan LI (李卓桓) 5 | Jingjing WU (吴京京) 6 | 7 | 2020-now @ Copyright Wechaty 8 | 9 | Licensed under the Apache License, Version 2.0 (the 'License'); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an 'AS IS' BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | import os 22 | import platform 23 | from wechaty_puppet import get_logger # type: ignore 24 | 25 | VERSION = '1.4.1' 26 | BASE_URL = 'https://login.weixin.qq.com' 27 | OS = platform.system() # Windows, Linux, Darwin 28 | DIR = os.getcwd() 29 | DEFAULT_QR = 'QR.png' 30 | TIMEOUT = (10, 60) 31 | 32 | USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36' 33 | 34 | UOS_PATCH_CLIENT_VERSION = '2.0.0' 35 | UOS_PATCH_EXTSPAM = 'Gp8ICJkIEpkICggwMDAwMDAwMRAGGoAI1GiJSIpeO1RZTq9QBKsRbPJdi84ropi16EYI10WB6g74sGmRwSNXjPQnYUKYotKkvLGpshucCaeWZMOylnc6o2AgDX9grhQQx7fm2DJRTyuNhUlwmEoWhjoG3F0ySAWUsEbH3bJMsEBwoB//0qmFJob74ffdaslqL+IrSy7LJ76/G5TkvNC+J0VQkpH1u3iJJs0uUYyLDzdBIQ6Ogd8LDQ3VKnJLm4g/uDLe+G7zzzkOPzCjXL+70naaQ9medzqmh+/SmaQ6uFWLDQLcRln++wBwoEibNpG4uOJvqXy+ql50DjlNchSuqLmeadFoo9/mDT0q3G7o/80P15ostktjb7h9bfNc+nZVSnUEJXbCjTeqS5UYuxn+HTS5nZsPVxJA2O5GdKCYK4x8lTTKShRstqPfbQpplfllx2fwXcSljuYi3YipPyS3GCAqf5A7aYYwJ7AvGqUiR2SsVQ9Nbp8MGHET1GxhifC692APj6SJxZD3i1drSYZPMMsS9rKAJTGz2FEupohtpf2tgXm6c16nDk/cw+C7K7me5j5PLHv55DFCS84b06AytZPdkFZLj7FHOkcFGJXitHkX5cgww7vuf6F3p0yM/W73SoXTx6GX4G6Hg2rYx3O/9VU2Uq8lvURB4qIbD9XQpzmyiFMaytMnqxcZJcoXCtfkTJ6pI7a92JpRUvdSitg967VUDUAQnCXCM/m0snRkR9LtoXAO1FUGpwlp1EfIdCZFPKNnXMeqev0j9W9ZrkEs9ZWcUEexSj5z+dKYQBhIICviYUQHVqBTZSNy22PlUIeDeIs11j7q4t8rD8LPvzAKWVqXE+5lS1JPZkjg4y5hfX1Dod3t96clFfwsvDP6xBSe1NBcoKbkyGxYK0UvPGtKQEE0Se2zAymYDv41klYE9s+rxp8e94/H8XhrL9oGm8KWb2RmYnAE7ry9gd6e8ZuBRIsISlJAE/e8y8xFmP031S6Lnaet6YXPsFpuFsdQs535IjcFd75hh6DNMBYhSfjv456cvhsb99+fRw/KVZLC3yzNSCbLSyo9d9BI45Plma6V8akURQA/qsaAzU0VyTIqZJkPDTzhuCl92vD2AD/QOhx6iwRSVPAxcRFZcWjgc2wCKh+uCYkTVbNQpB9B90YlNmI3fWTuUOUjwOzQRxJZj11NsimjOJ50qQwTTFj6qQvQ1a/I+MkTx5UO+yNHl718JWcR3AXGmv/aa9rD1eNP8ioTGlOZwPgmr2sor2iBpKTOrB83QgZXP+xRYkb4zVC+LoAXEoIa1+zArywlgREer7DLePukkU6wHTkuSaF+ge5Of1bXuU4i938WJHj0t3D8uQxkJvoFi/EYN/7u2P1zGRLV4dHVUsZMGCCtnO6BBigFMAA=' 36 | CACHE_DIR = '.wechaty' 37 | LOGIN_TIMEOUT = 60 38 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import Core 2 | from .config import VERSION, ASYNC_COMPONENTS 3 | from .log import set_logging 4 | 5 | if ASYNC_COMPONENTS: 6 | from itchat.async_components import load_components 7 | else: 8 | from itchat.components import load_components 9 | 10 | 11 | __version__ = VERSION 12 | 13 | 14 | instanceList = [] 15 | 16 | def load_async_itchat() -> Core: 17 | """load async-based itchat instance 18 | 19 | Returns: 20 | Core: the abstract interface of itchat 21 | """ 22 | from itchat.async_components import load_components 23 | load_components(Core) 24 | return Core() 25 | 26 | 27 | def load_sync_itchat() -> Core: 28 | """load sync-based itchat instance 29 | 30 | Returns: 31 | Core: the abstract interface of itchat 32 | """ 33 | from itchat.components import load_components 34 | load_components(Core) 35 | return Core() 36 | 37 | 38 | if ASYNC_COMPONENTS: 39 | instance = load_async_itchat() 40 | else: 41 | instance = load_sync_itchat() 42 | 43 | 44 | instanceList = [instance] 45 | 46 | # I really want to use sys.modules[__name__] = originInstance 47 | # but it makes auto-fill a real mess, so forgive me for my following ** 48 | # actually it toke me less than 30 seconds, god bless Uganda 49 | 50 | # components.login 51 | login = instance.login 52 | get_QRuuid = instance.get_QRuuid 53 | get_QR = instance.get_QR 54 | check_login = instance.check_login 55 | web_init = instance.web_init 56 | show_mobile_login = instance.show_mobile_login 57 | start_receiving = instance.start_receiving 58 | get_msg = instance.get_msg 59 | logout = instance.logout 60 | # components.contact 61 | update_chatroom = instance.update_chatroom 62 | update_friend = instance.update_friend 63 | get_contact = instance.get_contact 64 | get_friends = instance.get_friends 65 | get_chatrooms = instance.get_chatrooms 66 | get_mps = instance.get_mps 67 | set_alias = instance.set_alias 68 | set_pinned = instance.set_pinned 69 | accept_friend = instance.accept_friend 70 | get_head_img = instance.get_head_img 71 | create_chatroom = instance.create_chatroom 72 | set_chatroom_name = instance.set_chatroom_name 73 | delete_member_from_chatroom = instance.delete_member_from_chatroom 74 | add_member_into_chatroom = instance.add_member_into_chatroom 75 | # components.messages 76 | send_raw_msg = instance.send_raw_msg 77 | send_msg = instance.send_msg 78 | upload_file = instance.upload_file 79 | send_file = instance.send_file 80 | send_image = instance.send_image 81 | send_video = instance.send_video 82 | send = instance.send 83 | revoke = instance.revoke 84 | # components.hotreload 85 | dump_login_status = instance.dump_login_status 86 | load_login_status = instance.load_login_status 87 | # components.register 88 | auto_login = instance.auto_login 89 | configured_reply = instance.configured_reply 90 | msg_register = instance.msg_register 91 | run = instance.run 92 | # other functions 93 | search_friends = instance.search_friends 94 | search_chatrooms = instance.search_chatrooms 95 | search_mps = instance.search_mps 96 | set_logging = set_logging 97 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/async_components/__init__.py: -------------------------------------------------------------------------------- 1 | from .contact import load_contact 2 | from .hotreload import load_hotreload 3 | from .login import load_login 4 | from .messages import load_messages 5 | from .register import load_register 6 | 7 | def load_components(core): 8 | load_contact(core) 9 | load_hotreload(core) 10 | load_login(core) 11 | load_messages(core) 12 | load_register(core) 13 | TEXT = 'Text' 14 | MAP = 'Map' 15 | CARD = 'Card' 16 | NOTE = 'Note' 17 | SHARING = 'Sharing' 18 | PICTURE = 'Picture' 19 | RECORDING = VOICE = 'Recording' 20 | ATTACHMENT = 'Attachment' 21 | VIDEO = 'Video' 22 | FRIENDS = 'Friends' 23 | SYSTEM = 'System' 24 | 25 | INCOME_MSG = [TEXT, MAP, CARD, NOTE, SHARING, PICTURE, 26 | RECORDING, VOICE, ATTACHMENT, VIDEO, FRIENDS, SYSTEM] 27 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/async_components/contact.py: -------------------------------------------------------------------------------- 1 | import time, re, io 2 | import json, copy 3 | import logging 4 | 5 | from .. import config, utils 6 | from ..components.contact import accept_friend 7 | from ..returnvalues import ReturnValue 8 | from ..storage import contact_change 9 | from ..utils import update_info_dict 10 | 11 | logger = logging.getLogger('itchat') 12 | 13 | def load_contact(core): 14 | core.update_chatroom = update_chatroom 15 | core.update_friend = update_friend 16 | core.get_contact = get_contact 17 | core.get_friends = get_friends 18 | core.get_chatrooms = get_chatrooms 19 | core.get_mps = get_mps 20 | core.set_alias = set_alias 21 | core.set_pinned = set_pinned 22 | core.accept_friend = accept_friend 23 | core.get_head_img = get_head_img 24 | core.create_chatroom = create_chatroom 25 | core.set_chatroom_name = set_chatroom_name 26 | core.delete_member_from_chatroom = delete_member_from_chatroom 27 | core.add_member_into_chatroom = add_member_into_chatroom 28 | 29 | def update_chatroom(core, userName, detailedMember=False): 30 | if not isinstance(userName, list): 31 | userName = [userName] 32 | url = '%s/webwxbatchgetcontact?type=ex&r=%s' % ( 33 | core.loginInfo['url'], int(time.time())) 34 | headers = { 35 | 'ContentType': 'application/json; charset=UTF-8', 36 | 'User-Agent' : config.USER_AGENT } 37 | data = { 38 | 'BaseRequest': core.loginInfo['BaseRequest'], 39 | 'Count': len(userName), 40 | 'List': [{ 41 | 'UserName': u, 42 | 'ChatRoomId': '', } for u in userName], } 43 | chatroomList = json.loads(core.s.post(url, data=json.dumps(data), headers=headers 44 | ).content.decode('utf8', 'replace')).get('ContactList') 45 | if not chatroomList: 46 | return ReturnValue({'BaseResponse': { 47 | 'ErrMsg': 'No chatroom found', 48 | 'Ret': -1001, }}) 49 | 50 | if detailedMember: 51 | def get_detailed_member_info(encryChatroomId, memberList): 52 | url = '%s/webwxbatchgetcontact?type=ex&r=%s' % ( 53 | core.loginInfo['url'], int(time.time())) 54 | headers = { 55 | 'ContentType': 'application/json; charset=UTF-8', 56 | 'User-Agent' : config.USER_AGENT, } 57 | data = { 58 | 'BaseRequest': core.loginInfo['BaseRequest'], 59 | 'Count': len(memberList), 60 | 'List': [{ 61 | 'UserName': member['UserName'], 62 | 'EncryChatRoomId': encryChatroomId} \ 63 | for member in memberList], } 64 | return json.loads(core.s.post(url, data=json.dumps(data), headers=headers 65 | ).content.decode('utf8', 'replace'))['ContactList'] 66 | MAX_GET_NUMBER = 50 67 | for chatroom in chatroomList: 68 | totalMemberList = [] 69 | for i in range(int(len(chatroom['MemberList']) / MAX_GET_NUMBER + 1)): 70 | memberList = chatroom['MemberList'][i*MAX_GET_NUMBER: (i+1)*MAX_GET_NUMBER] 71 | totalMemberList += get_detailed_member_info(chatroom['EncryChatRoomId'], memberList) 72 | chatroom['MemberList'] = totalMemberList 73 | 74 | update_local_chatrooms(core, chatroomList) 75 | r = [core.storageClass.search_chatrooms(userName=c['UserName']) 76 | for c in chatroomList] 77 | return r if 1 < len(r) else r[0] 78 | 79 | def update_friend(core, userName): 80 | if not isinstance(userName, list): 81 | userName = [userName] 82 | url = '%s/webwxbatchgetcontact?type=ex&r=%s' % ( 83 | core.loginInfo['url'], int(time.time())) 84 | headers = { 85 | 'ContentType': 'application/json; charset=UTF-8', 86 | 'User-Agent' : config.USER_AGENT } 87 | data = { 88 | 'BaseRequest': core.loginInfo['BaseRequest'], 89 | 'Count': len(userName), 90 | 'List': [{ 91 | 'UserName': u, 92 | 'EncryChatRoomId': '', } for u in userName], } 93 | friendList = json.loads(core.s.post(url, data=json.dumps(data), headers=headers 94 | ).content.decode('utf8', 'replace')).get('ContactList') 95 | 96 | update_local_friends(core, friendList) 97 | r = [core.storageClass.search_friends(userName=f['UserName']) 98 | for f in friendList] 99 | return r if len(r) != 1 else r[0] 100 | 101 | @contact_change 102 | def update_local_chatrooms(core, l): 103 | ''' 104 | get a list of chatrooms for updating local chatrooms 105 | return a list of given chatrooms with updated info 106 | ''' 107 | for chatroom in l: 108 | # format new chatrooms 109 | utils.emoji_formatter(chatroom, 'NickName') 110 | for member in chatroom['MemberList']: 111 | if 'NickName' in member: 112 | utils.emoji_formatter(member, 'NickName') 113 | if 'DisplayName' in member: 114 | utils.emoji_formatter(member, 'DisplayName') 115 | if 'RemarkName' in member: 116 | utils.emoji_formatter(member, 'RemarkName') 117 | # update it to old chatrooms 118 | oldChatroom = utils.search_dict_list( 119 | core.chatroomList, 'UserName', chatroom['UserName']) 120 | if oldChatroom: 121 | update_info_dict(oldChatroom, chatroom) 122 | # - update other values 123 | memberList = chatroom.get('MemberList', []) 124 | oldMemberList = oldChatroom['MemberList'] 125 | if memberList: 126 | for member in memberList: 127 | oldMember = utils.search_dict_list( 128 | oldMemberList, 'UserName', member['UserName']) 129 | if oldMember: 130 | update_info_dict(oldMember, member) 131 | else: 132 | oldMemberList.append(member) 133 | else: 134 | core.chatroomList.append(chatroom) 135 | oldChatroom = utils.search_dict_list( 136 | core.chatroomList, 'UserName', chatroom['UserName']) 137 | # delete useless members 138 | if len(chatroom['MemberList']) != len(oldChatroom['MemberList']) and \ 139 | chatroom['MemberList']: 140 | existsUserNames = [member['UserName'] for member in chatroom['MemberList']] 141 | delList = [] 142 | for i, member in enumerate(oldChatroom['MemberList']): 143 | if member['UserName'] not in existsUserNames: 144 | delList.append(i) 145 | delList.sort(reverse=True) 146 | for i in delList: 147 | del oldChatroom['MemberList'][i] 148 | # - update OwnerUin 149 | if oldChatroom.get('ChatRoomOwner') and oldChatroom.get('MemberList'): 150 | owner = utils.search_dict_list(oldChatroom['MemberList'], 151 | 'UserName', oldChatroom['ChatRoomOwner']) 152 | oldChatroom['OwnerUin'] = (owner or {}).get('Uin', 0) 153 | # - update IsAdmin 154 | if 'OwnerUin' in oldChatroom and oldChatroom['OwnerUin'] != 0: 155 | oldChatroom['IsAdmin'] = \ 156 | oldChatroom['OwnerUin'] == int(core.loginInfo['wxuin']) 157 | else: 158 | oldChatroom['IsAdmin'] = None 159 | # - update core 160 | newcore = utils.search_dict_list(oldChatroom['MemberList'], 161 | 'UserName', core.storageClass.userName) 162 | oldChatroom['core'] = newcore or copy.deepcopy(core.loginInfo['User']) 163 | return { 164 | 'Type' : 'System', 165 | 'Text' : [chatroom['UserName'] for chatroom in l], 166 | 'SystemInfo' : 'chatrooms', 167 | 'FromUserName' : core.storageClass.userName, 168 | 'ToUserName' : core.storageClass.userName, } 169 | 170 | @contact_change 171 | def update_local_friends(core, l): 172 | ''' 173 | get a list of friends or mps for updating local contact 174 | ''' 175 | fullList = core.memberList + core.mpList 176 | for friend in l: 177 | if 'NickName' in friend: 178 | utils.emoji_formatter(friend, 'NickName') 179 | if 'DisplayName' in friend: 180 | utils.emoji_formatter(friend, 'DisplayName') 181 | if 'RemarkName' in friend: 182 | utils.emoji_formatter(friend, 'RemarkName') 183 | oldInfoDict = utils.search_dict_list( 184 | fullList, 'UserName', friend['UserName']) 185 | if oldInfoDict is None: 186 | oldInfoDict = copy.deepcopy(friend) 187 | if oldInfoDict['VerifyFlag'] & 8 == 0: 188 | core.memberList.append(oldInfoDict) 189 | else: 190 | core.mpList.append(oldInfoDict) 191 | else: 192 | update_info_dict(oldInfoDict, friend) 193 | 194 | @contact_change 195 | def update_local_uin(core, msg): 196 | ''' 197 | content contains uins and StatusNotifyUserName contains username 198 | they are in same order, so what I do is to pair them together 199 | 200 | I caught an exception in this method while not knowing why 201 | but don't worry, it won't cause any problem 202 | ''' 203 | uins = re.search('([^<]*?)<', msg['Content']) 204 | usernameChangedList = [] 205 | r = { 206 | 'Type': 'System', 207 | 'Text': usernameChangedList, 208 | 'SystemInfo': 'uins', } 209 | if uins: 210 | uins = uins.group(1).split(',') 211 | usernames = msg['StatusNotifyUserName'].split(',') 212 | if 0 < len(uins) == len(usernames): 213 | for uin, username in zip(uins, usernames): 214 | if not '@' in username: continue 215 | fullContact = core.memberList + core.chatroomList + core.mpList 216 | userDicts = utils.search_dict_list(fullContact, 217 | 'UserName', username) 218 | if userDicts: 219 | if userDicts.get('Uin', 0) == 0: 220 | userDicts['Uin'] = uin 221 | usernameChangedList.append(username) 222 | logger.debug('Uin fetched: %s, %s' % (username, uin)) 223 | else: 224 | if userDicts['Uin'] != uin: 225 | logger.debug('Uin changed: %s, %s' % ( 226 | userDicts['Uin'], uin)) 227 | else: 228 | if '@@' in username: 229 | core.storageClass.updateLock.release() 230 | update_chatroom(core, username) 231 | core.storageClass.updateLock.acquire() 232 | newChatroomDict = utils.search_dict_list( 233 | core.chatroomList, 'UserName', username) 234 | if newChatroomDict is None: 235 | newChatroomDict = utils.struct_friend_info({ 236 | 'UserName': username, 237 | 'Uin': uin, 238 | 'core': copy.deepcopy(core.loginInfo['User'])}) 239 | core.chatroomList.append(newChatroomDict) 240 | else: 241 | newChatroomDict['Uin'] = uin 242 | elif '@' in username: 243 | core.storageClass.updateLock.release() 244 | update_friend(core, username) 245 | core.storageClass.updateLock.acquire() 246 | newFriendDict = utils.search_dict_list( 247 | core.memberList, 'UserName', username) 248 | if newFriendDict is None: 249 | newFriendDict = utils.struct_friend_info({ 250 | 'UserName': username, 251 | 'Uin': uin, }) 252 | core.memberList.append(newFriendDict) 253 | else: 254 | newFriendDict['Uin'] = uin 255 | usernameChangedList.append(username) 256 | logger.debug('Uin fetched: %s, %s' % (username, uin)) 257 | else: 258 | logger.debug('Wrong length of uins & usernames: %s, %s' % ( 259 | len(uins), len(usernames))) 260 | else: 261 | logger.debug('No uins in 51 message') 262 | logger.debug(msg['Content']) 263 | return r 264 | 265 | def get_contact(core, update=False): 266 | if not update: 267 | return utils.contact_deep_copy(core, core.chatroomList) 268 | def _get_contact(seq=0): 269 | url = '%s/webwxgetcontact?r=%s&seq=%s&skey=%s' % (core.loginInfo['url'], 270 | int(time.time()), seq, core.loginInfo['skey']) 271 | headers = { 272 | 'ContentType': 'application/json; charset=UTF-8', 273 | 'User-Agent' : config.USER_AGENT, } 274 | try: 275 | r = core.s.get(url, headers=headers) 276 | except: 277 | logger.info('Failed to fetch contact, that may because of the amount of your chatrooms') 278 | for chatroom in core.get_chatrooms(): 279 | core.update_chatroom(chatroom['UserName'], detailedMember=True) 280 | return 0, [] 281 | j = json.loads(r.content.decode('utf-8', 'replace')) 282 | return j.get('Seq', 0), j.get('MemberList') 283 | seq, memberList = 0, [] 284 | while 1: 285 | seq, batchMemberList = _get_contact(seq) 286 | memberList.extend(batchMemberList) 287 | if seq == 0: 288 | break 289 | chatroomList, otherList = [], [] 290 | for m in memberList: 291 | if m['Sex'] != 0: 292 | otherList.append(m) 293 | elif '@@' in m['UserName']: 294 | chatroomList.append(m) 295 | elif '@' in m['UserName']: 296 | # mp will be dealt in update_local_friends as well 297 | otherList.append(m) 298 | if chatroomList: 299 | update_local_chatrooms(core, chatroomList) 300 | if otherList: 301 | update_local_friends(core, otherList) 302 | return utils.contact_deep_copy(core, chatroomList) 303 | 304 | def get_friends(core, update=False): 305 | if update: 306 | core.get_contact(update=True) 307 | return utils.contact_deep_copy(core, core.memberList) 308 | 309 | def get_chatrooms(core, update=False, contactOnly=False): 310 | if contactOnly: 311 | return core.get_contact(update=True) 312 | else: 313 | if update: 314 | core.get_contact(True) 315 | return utils.contact_deep_copy(core, core.chatroomList) 316 | 317 | def get_mps(core, update=False): 318 | if update: core.get_contact(update=True) 319 | return utils.contact_deep_copy(core, core.mpList) 320 | 321 | def set_alias(core, userName, alias): 322 | oldFriendInfo = utils.search_dict_list( 323 | core.memberList, 'UserName', userName) 324 | if oldFriendInfo is None: 325 | return ReturnValue({'BaseResponse': { 326 | 'Ret': -1001, }}) 327 | url = '%s/webwxoplog?lang=%s&pass_ticket=%s' % ( 328 | core.loginInfo['url'], 'zh_CN', core.loginInfo['pass_ticket']) 329 | data = { 330 | 'UserName' : userName, 331 | 'CmdId' : 2, 332 | 'RemarkName' : alias, 333 | 'BaseRequest' : core.loginInfo['BaseRequest'], } 334 | headers = { 'User-Agent' : config.USER_AGENT} 335 | r = core.s.post(url, json.dumps(data, ensure_ascii=False).encode('utf8'), 336 | headers=headers) 337 | r = ReturnValue(rawResponse=r) 338 | if r: 339 | oldFriendInfo['RemarkName'] = alias 340 | return r 341 | 342 | def set_pinned(core, userName, isPinned=True): 343 | url = '%s/webwxoplog?pass_ticket=%s' % ( 344 | core.loginInfo['url'], core.loginInfo['pass_ticket']) 345 | data = { 346 | 'UserName' : userName, 347 | 'CmdId' : 3, 348 | 'OP' : int(isPinned), 349 | 'BaseRequest' : core.loginInfo['BaseRequest'], } 350 | headers = { 'User-Agent' : config.USER_AGENT} 351 | r = core.s.post(url, json=data, headers=headers) 352 | return ReturnValue(rawResponse=r) 353 | 354 | def accept_friend(core, userName, v4= '', autoUpdate=True): 355 | url = f"{core.loginInfo['url']}/webwxverifyuser?r={int(time.time())}&pass_ticket={core.loginInfo['pass_ticket']}" 356 | data = { 357 | 'BaseRequest': core.loginInfo['BaseRequest'], 358 | 'Opcode': 3, # 3 359 | 'VerifyUserListSize': 1, 360 | 'VerifyUserList': [{ 361 | 'Value': userName, 362 | 'VerifyUserTicket': v4, }], 363 | 'VerifyContent': '', 364 | 'SceneListCount': 1, 365 | 'SceneList': [33], 366 | 'skey': core.loginInfo['skey'], } 367 | headers = { 368 | 'ContentType': 'application/json; charset=UTF-8', 369 | 'User-Agent' : config.USER_AGENT } 370 | r = core.s.post(url, headers=headers, 371 | data=json.dumps(data, ensure_ascii=False).encode('utf8', 'replace')) 372 | if autoUpdate: 373 | core.update_friend(userName) 374 | return ReturnValue(rawResponse=r) 375 | 376 | def get_head_img(core, userName=None, chatroomUserName=None, picDir=None): 377 | ''' get head image 378 | * if you want to get chatroom header: only set chatroomUserName 379 | * if you want to get friend header: only set userName 380 | * if you want to get chatroom member header: set both 381 | ''' 382 | params = { 383 | 'userName': userName or chatroomUserName or core.storageClass.userName, 384 | 'skey': core.loginInfo['skey'], 385 | 'type': 'big', } 386 | url = '%s/webwxgeticon' % core.loginInfo['url'] 387 | if chatroomUserName is None: 388 | infoDict = core.storageClass.search_friends(userName=userName) 389 | if infoDict is None: 390 | return ReturnValue({'BaseResponse': { 391 | 'ErrMsg': 'No friend found', 392 | 'Ret': -1001, }}) 393 | else: 394 | if userName is None: 395 | url = '%s/webwxgetheadimg' % core.loginInfo['url'] 396 | else: 397 | chatroom = core.storageClass.search_chatrooms(userName=chatroomUserName) 398 | if chatroomUserName is None: 399 | return ReturnValue({'BaseResponse': { 400 | 'ErrMsg': 'No chatroom found', 401 | 'Ret': -1001, }}) 402 | if 'EncryChatRoomId' in chatroom: 403 | params['chatroomid'] = chatroom['EncryChatRoomId'] 404 | params['chatroomid'] = params.get('chatroomid') or chatroom['UserName'] 405 | headers = { 'User-Agent' : config.USER_AGENT} 406 | r = core.s.get(url, params=params, stream=True, headers=headers) 407 | tempStorage = io.BytesIO() 408 | for block in r.iter_content(1024): 409 | tempStorage.write(block) 410 | if picDir is None: 411 | return tempStorage.getvalue() 412 | with open(picDir, 'wb') as f: 413 | f.write(tempStorage.getvalue()) 414 | tempStorage.seek(0) 415 | return ReturnValue({'BaseResponse': { 416 | 'ErrMsg': 'Successfully downloaded', 417 | 'Ret': 0, }, 418 | 'PostFix': utils.get_image_postfix(tempStorage.read(20)), }) 419 | 420 | def create_chatroom(core, memberList, topic=''): 421 | url = '%s/webwxcreatechatroom?pass_ticket=%s&r=%s' % ( 422 | core.loginInfo['url'], core.loginInfo['pass_ticket'], int(time.time())) 423 | data = { 424 | 'BaseRequest': core.loginInfo['BaseRequest'], 425 | 'MemberCount': len(memberList.split(',')), 426 | 'MemberList': [{'UserName': member} for member in memberList.split(',')], 427 | 'Topic': topic, } 428 | headers = { 429 | 'content-type': 'application/json; charset=UTF-8', 430 | 'User-Agent' : config.USER_AGENT } 431 | r = core.s.post(url, headers=headers, 432 | data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore')) 433 | return ReturnValue(rawResponse=r) 434 | 435 | def set_chatroom_name(core, chatroomUserName, name): 436 | url = '%s/webwxupdatechatroom?fun=modtopic&pass_ticket=%s' % ( 437 | core.loginInfo['url'], core.loginInfo['pass_ticket']) 438 | data = { 439 | 'BaseRequest': core.loginInfo['BaseRequest'], 440 | 'ChatRoomName': chatroomUserName, 441 | 'NewTopic': name, } 442 | headers = { 443 | 'content-type': 'application/json; charset=UTF-8', 444 | 'User-Agent' : config.USER_AGENT } 445 | r = core.s.post(url, headers=headers, 446 | data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore')) 447 | return ReturnValue(rawResponse=r) 448 | 449 | def delete_member_from_chatroom(core, chatroomUserName, memberList): 450 | url = '%s/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % ( 451 | core.loginInfo['url'], core.loginInfo['pass_ticket']) 452 | data = { 453 | 'BaseRequest': core.loginInfo['BaseRequest'], 454 | 'ChatRoomName': chatroomUserName, 455 | 'DelMemberList': ','.join([member['UserName'] for member in memberList]), } 456 | headers = { 457 | 'content-type': 'application/json; charset=UTF-8', 458 | 'User-Agent' : config.USER_AGENT} 459 | r = core.s.post(url, data=json.dumps(data),headers=headers) 460 | return ReturnValue(rawResponse=r) 461 | 462 | def add_member_into_chatroom(core, chatroomUserName, memberList, 463 | useInvitation=False): 464 | ''' add or invite member into chatroom 465 | * there are two ways to get members into chatroom: invite or directly add 466 | * but for chatrooms with more than 40 users, you can only use invite 467 | * but don't worry we will auto-force userInvitation for you when necessary 468 | ''' 469 | if not useInvitation: 470 | chatroom = core.storageClass.search_chatrooms(userName=chatroomUserName) 471 | if not chatroom: chatroom = core.update_chatroom(chatroomUserName) 472 | if len(chatroom['MemberList']) > core.loginInfo['InviteStartCount']: 473 | useInvitation = True 474 | if useInvitation: 475 | fun, memberKeyName = 'invitemember', 'InviteMemberList' 476 | else: 477 | fun, memberKeyName = 'addmember', 'AddMemberList' 478 | url = '%s/webwxupdatechatroom?fun=%s&pass_ticket=%s' % ( 479 | core.loginInfo['url'], fun, core.loginInfo['pass_ticket']) 480 | params = { 481 | 'BaseRequest' : core.loginInfo['BaseRequest'], 482 | 'ChatRoomName' : chatroomUserName, 483 | memberKeyName : memberList, } 484 | headers = { 485 | 'content-type': 'application/json; charset=UTF-8', 486 | 'User-Agent' : config.USER_AGENT} 487 | r = core.s.post(url, data=json.dumps(params),headers=headers) 488 | return ReturnValue(rawResponse=r) 489 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/async_components/hotreload.py: -------------------------------------------------------------------------------- 1 | import pickle, os 2 | import logging 3 | 4 | import requests # type: ignore 5 | 6 | from ..config import VERSION 7 | from ..returnvalues import ReturnValue 8 | from ..storage import templates 9 | from .contact import update_local_chatrooms, update_local_friends 10 | from .messages import produce_msg 11 | 12 | logger = logging.getLogger('itchat') 13 | 14 | def load_hotreload(core): 15 | core.dump_login_status = dump_login_status 16 | core.load_login_status = load_login_status 17 | 18 | async def dump_login_status(self, fileDir=None): 19 | fileDir = fileDir or self.hotReloadDir 20 | try: 21 | with open(fileDir, 'w') as f: 22 | f.write('itchat - DELETE THIS') 23 | os.remove(fileDir) 24 | except: 25 | raise Exception('Incorrect fileDir') 26 | status = { 27 | 'version' : VERSION, 28 | 'loginInfo' : self.loginInfo, 29 | 'cookies' : self.s.cookies.get_dict(), 30 | 'storage' : self.storageClass.dumps()} 31 | with open(fileDir, 'wb') as f: 32 | pickle.dump(status, f) 33 | logger.debug('Dump login status for hot reload successfully.') 34 | 35 | async def load_login_status(self, fileDir, 36 | loginCallback=None, exitCallback=None): 37 | try: 38 | with open(fileDir, 'rb') as f: 39 | j = pickle.load(f) 40 | except Exception as e: 41 | logger.debug('No such file, loading login status failed.') 42 | return ReturnValue({'BaseResponse': { 43 | 'ErrMsg': 'No such file, loading login status failed.', 44 | 'Ret': -1002, }}) 45 | 46 | if j.get('version', '') != VERSION: 47 | logger.debug(('you have updated itchat from %s to %s, ' + 48 | 'so cached status is ignored') % ( 49 | j.get('version', 'old version'), VERSION)) 50 | return ReturnValue({'BaseResponse': { 51 | 'ErrMsg': 'cached status ignored because of version', 52 | 'Ret': -1005, }}) 53 | self.loginInfo = j['loginInfo'] 54 | self.loginInfo['User'] = templates.User(self.loginInfo['User']) 55 | self.loginInfo['User'].core = self 56 | self.s.cookies = requests.utils.cookiejar_from_dict(j['cookies']) 57 | self.storageClass.loads(j['storage']) 58 | try: 59 | msgList, contactList = self.get_msg() 60 | except: 61 | msgList = contactList = None 62 | if (msgList or contactList) is None: 63 | self.logout() 64 | await load_last_login_status(self.s, j['cookies']) 65 | logger.debug('server refused, loading login status failed.') 66 | return ReturnValue({'BaseResponse': { 67 | 'ErrMsg': 'server refused, loading login status failed.', 68 | 'Ret': -1003, }}) 69 | else: 70 | if contactList: 71 | for contact in contactList: 72 | if '@@' in contact['UserName']: 73 | update_local_chatrooms(self, [contact]) 74 | else: 75 | update_local_friends(self, [contact]) 76 | if msgList: 77 | msgList = await produce_msg(self, msgList) 78 | for msg in msgList: self.msgList.put(msg) 79 | await self.start_receiving(exitCallback) 80 | logger.debug('loading login status succeeded.') 81 | if hasattr(loginCallback, '__call__'): 82 | await loginCallback(self.storageClass.userName) 83 | return ReturnValue({'BaseResponse': { 84 | 'ErrMsg': 'loading login status succeeded.', 85 | 'Ret': 0, }}) 86 | 87 | async def load_last_login_status(session, cookiesDict): 88 | try: 89 | session.cookies = requests.utils.cookiejar_from_dict({ 90 | 'webwxuvid': cookiesDict['webwxuvid'], 91 | 'webwx_auth_ticket': cookiesDict['webwx_auth_ticket'], 92 | 'login_frequency': '2', 93 | 'last_wxuin': cookiesDict['wxuin'], 94 | 'wxloadtime': cookiesDict['wxloadtime'] + '_expired', 95 | 'wxpluginkey': cookiesDict['wxloadtime'], 96 | 'wxuin': cookiesDict['wxuin'], 97 | 'mm_lang': 'zh_CN', 98 | 'MM_WX_NOTIFY_STATE': '1', 99 | 'MM_WX_SOUND_STATE': '1', }) 100 | except: 101 | logger.info('Load status for push login failed, we may have experienced a cookies change.') 102 | logger.info('If you are using the newest version of itchat, you may report a bug.') 103 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/async_components/login.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os, time, re, io 3 | import threading 4 | import json 5 | import random 6 | import traceback 7 | import logging 8 | from turtle import update 9 | try: 10 | from httplib import BadStatusLine 11 | except ImportError: 12 | from http.client import BadStatusLine 13 | 14 | import requests # type: ignore 15 | from pyqrcode import QRCode 16 | 17 | from .. import config, utils 18 | from ..returnvalues import ReturnValue 19 | from ..storage.templates import wrap_user_dict 20 | from .contact import update_local_chatrooms, update_local_friends 21 | from .messages import produce_msg 22 | 23 | logger = logging.getLogger('itchat') 24 | 25 | 26 | def load_login(core): 27 | core.login = login 28 | core.get_QRuuid = get_QRuuid 29 | core.get_QR = get_QR 30 | core.check_login = check_login 31 | core.web_init = web_init 32 | core.show_mobile_login = show_mobile_login 33 | core.start_receiving = start_receiving 34 | core.get_msg = get_msg 35 | core.logout = logout 36 | 37 | async def login(self, enableCmdQR=False, picDir=None, qrCallback=None, EventScanPayload=None,ScanStatus=None,event_stream=None, 38 | loginCallback=None, exitCallback=None): 39 | if self.alive or self.isLogging: 40 | logger.warning('itchat has already logged in.') 41 | return 42 | self.isLogging = True 43 | 44 | while self.isLogging: 45 | uuid = await push_login(self) 46 | if uuid: 47 | payload = EventScanPayload( 48 | status=ScanStatus.Waiting, 49 | qrcode=f"qrcode/https://login.weixin.qq.com/l/{uuid}" 50 | ) 51 | event_stream.emit('scan', payload) 52 | await asyncio.sleep(0.1) 53 | else: 54 | logger.info('Getting uuid of QR code.') 55 | self.get_QRuuid() 56 | payload = EventScanPayload( 57 | status=ScanStatus.Waiting, 58 | qrcode=f"https://login.weixin.qq.com/l/{self.uuid}" 59 | ) 60 | print(f"https://wechaty.js.org/qrcode/https://login.weixin.qq.com/l/{self.uuid}") 61 | event_stream.emit('scan', payload) 62 | await asyncio.sleep(0.1) 63 | # logger.info('Please scan the QR code to log in.') 64 | isLoggedIn = False 65 | while not isLoggedIn: 66 | status = await self.check_login() 67 | # if hasattr(qrCallback, '__call__'): 68 | # await qrCallback(uuid=self.uuid, status=status, qrcode=self.qrStorage.getvalue()) 69 | if status == '200': 70 | isLoggedIn = True 71 | payload = EventScanPayload( 72 | status=ScanStatus.Scanned, 73 | qrcode=f"https://login.weixin.qq.com/l/{self.uuid}" 74 | ) 75 | event_stream.emit('scan', payload) 76 | await asyncio.sleep(0.1) 77 | elif status == '201': 78 | if isLoggedIn is not None: 79 | logger.info('Please press confirm on your phone.') 80 | isLoggedIn = None 81 | # payload = EventScanPayload( 82 | # status=ScanStatus.Waiting, 83 | # qrcode=f"https://login.weixin.qq.com/l/{self.uuid}" 84 | # ) 85 | # event_stream.emit('scan', payload) 86 | await asyncio.sleep(15) 87 | 88 | elif status != '408': 89 | payload = EventScanPayload( 90 | status=ScanStatus.Cancel, 91 | qrcode=f"https://login.weixin.qq.com/l/{self.uuid}" 92 | ) 93 | event_stream.emit('scan', payload) 94 | await asyncio.sleep(0.1) 95 | break 96 | if isLoggedIn: 97 | payload = EventScanPayload( 98 | status=ScanStatus.Confirmed, 99 | qrcode=f"https://login.weixin.qq.com/l/{self.uuid}" 100 | ) 101 | event_stream.emit('scan', payload) 102 | await asyncio.sleep(0.1) 103 | break 104 | elif self.isLogging: 105 | logger.info('Log in time out, reloading QR code.') 106 | payload = EventScanPayload( 107 | status=ScanStatus.Timeout, 108 | qrcode=f"https://login.weixin.qq.com/l/{self.uuid}" 109 | ) 110 | event_stream.emit('scan', payload) 111 | await asyncio.sleep(0.1) 112 | else: 113 | return 114 | logger.info('Loading the contact, this may take a little while.') 115 | await self.web_init() 116 | await self.show_mobile_login() 117 | self.get_friends(update=True) 118 | self.get_chatrooms(update=True) 119 | if hasattr(loginCallback, '__call__'): 120 | r = await loginCallback(self.storageClass.userName) 121 | else: 122 | utils.clear_screen() 123 | if os.path.exists(picDir or config.DEFAULT_QR): 124 | os.remove(picDir or config.DEFAULT_QR) 125 | logger.info('Login successfully as %s' % self.storageClass.nickName) 126 | await self.start_receiving(exitCallback) 127 | self.isLogging = False 128 | 129 | async def push_login(core): 130 | cookiesDict = core.s.cookies.get_dict() 131 | if 'wxuin' in cookiesDict: 132 | url = '%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s' % ( 133 | config.BASE_URL, cookiesDict['wxuin']) 134 | headers = { 'User-Agent' : config.USER_AGENT} 135 | r = core.s.get(url, headers=headers).json() 136 | if 'uuid' in r and r.get('ret') in (0, '0'): 137 | core.uuid = r['uuid'] 138 | return r['uuid'] 139 | return False 140 | 141 | def get_QRuuid(self): 142 | url = '%s/jslogin' % config.BASE_URL 143 | params = { 144 | 'appid' : 'wx782c26e4c19acffb', 145 | 'fun' : 'new', 146 | 'redirect_uri' : 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop', 147 | 'lang' : 'zh_CN' } 148 | headers = { 'User-Agent' : config.USER_AGENT} 149 | r = self.s.get(url, params=params, headers=headers) 150 | regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";' 151 | data = re.search(regx, r.text) 152 | if data and data.group(1) == '200': 153 | self.uuid = data.group(2) 154 | return self.uuid 155 | 156 | async def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None): 157 | uuid = uuid or self.uuid 158 | picDir = picDir or config.DEFAULT_QR 159 | qrStorage = io.BytesIO() 160 | qrCode = QRCode('https://login.weixin.qq.com/l/' + uuid) 161 | qrCode.png(qrStorage, scale=10) 162 | if hasattr(qrCallback, '__call__'): 163 | await qrCallback(uuid=uuid, status='0', qrcode=qrStorage.getvalue()) 164 | else: 165 | with open(picDir, 'wb') as f: 166 | f.write(qrStorage.getvalue()) 167 | if enableCmdQR: 168 | utils.print_cmd_qr(qrCode.text(1), enableCmdQR=enableCmdQR) 169 | else: 170 | utils.print_qr(picDir) 171 | return qrStorage 172 | 173 | async def check_login(self, uuid=None): 174 | uuid = uuid or self.uuid 175 | url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL 176 | localTime = int(time.time()) 177 | params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % ( 178 | uuid, int(-localTime / 1579), localTime) 179 | headers = { 'User-Agent' : config.USER_AGENT} 180 | r = self.s.get(url, params=params, headers=headers) 181 | regx = r'window.code=(\d+)' 182 | data = re.search(regx, r.text) 183 | if data and data.group(1) == '200': 184 | if await process_login_info(self, r.text): 185 | return '200' 186 | else: 187 | return '400' 188 | elif data: 189 | return data.group(1) 190 | else: 191 | return '400' 192 | 193 | async def process_login_info(core, loginContent): 194 | ''' when finish login (scanning qrcode) 195 | * syncUrl and fileUploadingUrl will be fetched 196 | * deviceid and msgid will be generated 197 | * skey, wxsid, wxuin, pass_ticket will be fetched 198 | ''' 199 | regx = r'window.redirect_uri="(\S+)";' 200 | core.loginInfo['url'] = re.search(regx, loginContent).group(1) 201 | headers = { 'User-Agent' : config.USER_AGENT, 202 | 'client-version' : config.UOS_PATCH_CLIENT_VERSION, 203 | 'extspam' : config.UOS_PATCH_EXTSPAM, 204 | 'referer' : 'https://wx.qq.com/?&lang=zh_CN&target=t' 205 | } 206 | r = core.s.get(core.loginInfo['url'], headers=headers, allow_redirects=False) 207 | core.loginInfo['url'] = core.loginInfo['url'][:core.loginInfo['url'].rfind('/')] 208 | for indexUrl, detailedUrl in ( 209 | ("wx2.qq.com" , ("file.wx2.qq.com", "webpush.wx2.qq.com")), 210 | ("wx8.qq.com" , ("file.wx8.qq.com", "webpush.wx8.qq.com")), 211 | ("qq.com" , ("file.wx.qq.com", "webpush.wx.qq.com")), 212 | ("web2.wechat.com" , ("file.web2.wechat.com", "webpush.web2.wechat.com")), 213 | ("wechat.com" , ("file.web.wechat.com", "webpush.web.wechat.com"))): 214 | fileUrl, syncUrl = ['https://%s/cgi-bin/mmwebwx-bin' % url for url in detailedUrl] 215 | if indexUrl in core.loginInfo['url']: 216 | core.loginInfo['fileUrl'], core.loginInfo['syncUrl'] = \ 217 | fileUrl, syncUrl 218 | break 219 | else: 220 | core.loginInfo['fileUrl'] = core.loginInfo['syncUrl'] = core.loginInfo['url'] 221 | core.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17] 222 | core.loginInfo['logintime'] = int(time.time() * 1e3) 223 | core.loginInfo['BaseRequest'] = {} 224 | cookies = core.s.cookies.get_dict() 225 | skey = re.findall('(.*?)', r.text, re.S)[0] 226 | pass_ticket = re.findall('(.*?)', r.text, re.S)[0] 227 | core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = skey 228 | core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = cookies["wxsid"] 229 | core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = cookies["wxuin"] 230 | core.loginInfo['pass_ticket'] = pass_ticket 231 | 232 | # A question : why pass_ticket == DeviceID ? 233 | # deviceID is only a randomly generated number 234 | 235 | # UOS PATCH By luvletter2333, Sun Feb 28 10:00 PM 236 | # for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes: 237 | # if node.nodeName == 'skey': 238 | # core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data 239 | # elif node.nodeName == 'wxsid': 240 | # core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data 241 | # elif node.nodeName == 'wxuin': 242 | # core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data 243 | # elif node.nodeName == 'pass_ticket': 244 | # core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data 245 | if not all([key in core.loginInfo for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]): 246 | logger.error('Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % r.text) 247 | core.isLogging = False 248 | return False 249 | return True 250 | 251 | async def web_init(self): 252 | url = '%s/webwxinit' % self.loginInfo['url'] 253 | params = { 254 | 'r': int(-time.time() / 1579), 255 | 'pass_ticket': self.loginInfo['pass_ticket'], } 256 | data = { 'BaseRequest': self.loginInfo['BaseRequest'], } 257 | headers = { 258 | 'ContentType': 'application/json; charset=UTF-8', 259 | 'User-Agent' : config.USER_AGENT, } 260 | r = self.s.post(url, params=params, data=json.dumps(data), headers=headers) 261 | dic = json.loads(r.content.decode('utf-8', 'replace')) 262 | # deal with login info 263 | utils.emoji_formatter(dic['User'], 'NickName') 264 | self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount']) 265 | self.loginInfo['User'] = wrap_user_dict(utils.struct_friend_info(dic['User'])) 266 | self.memberList.append(self.loginInfo['User']) 267 | self.loginInfo['SyncKey'] = dic['SyncKey'] 268 | self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) 269 | for item in dic['SyncKey']['List']]) 270 | self.storageClass.userName = dic['User']['UserName'] 271 | self.storageClass.nickName = dic['User']['NickName'] 272 | # deal with contact list returned when init 273 | contactList = dic.get('ContactList', []) 274 | chatroomList, otherList = [], [] 275 | for m in contactList: 276 | if m['Sex'] != 0: 277 | otherList.append(m) 278 | elif '@@' in m['UserName']: 279 | m['MemberList'] = [] # don't let dirty info pollute the list 280 | chatroomList.append(m) 281 | elif '@' in m['UserName']: 282 | # mp will be dealt in update_local_friends as well 283 | otherList.append(m) 284 | if chatroomList: 285 | update_local_chatrooms(self, chatroomList) 286 | if otherList: 287 | update_local_friends(self, otherList) 288 | return dic 289 | 290 | async def show_mobile_login(self): 291 | url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % ( 292 | self.loginInfo['url'], self.loginInfo['pass_ticket']) 293 | data = { 294 | 'BaseRequest' : self.loginInfo['BaseRequest'], 295 | 'Code' : 3, 296 | 'FromUserName' : self.storageClass.userName, 297 | 'ToUserName' : self.storageClass.userName, 298 | 'ClientMsgId' : int(time.time()), } 299 | headers = { 300 | 'ContentType': 'application/json; charset=UTF-8', 301 | 'User-Agent' : config.USER_AGENT, } 302 | r = self.s.post(url, data=json.dumps(data), headers=headers) 303 | return ReturnValue(rawResponse=r) 304 | 305 | async def start_receiving(self, exitCallback=None, getReceivingFnOnly=False): 306 | self.alive = True 307 | async def maintain_loop(): 308 | retryCount = 0 309 | while self.alive: 310 | try: 311 | i = sync_check(self) 312 | if i is None: 313 | self.alive = False 314 | elif i == '0': 315 | pass 316 | else: 317 | msgList, contactList = self.get_msg() 318 | if msgList: 319 | msgList = await produce_msg(self, msgList) 320 | for msg in msgList: 321 | self.msgList.put(msg) 322 | if contactList: 323 | chatroomList, otherList = [], [] 324 | for contact in contactList: 325 | if '@@' in contact['UserName']: 326 | chatroomList.append(contact) 327 | else: 328 | otherList.append(contact) 329 | chatroomMsg = update_local_chatrooms(self, chatroomList) 330 | chatroomMsg['User'] = self.loginInfo['User'] 331 | self.msgList.put(chatroomMsg) 332 | update_local_friends(self, otherList) 333 | retryCount = 0 334 | except requests.exceptions.ReadTimeout: 335 | pass 336 | except: 337 | retryCount += 1 338 | logger.error(traceback.format_exc()) 339 | if self.receivingRetryCount < retryCount: 340 | self.alive = False 341 | else: 342 | time.sleep(1) 343 | self.logout() 344 | if hasattr(exitCallback, '__call__'): 345 | exitCallback(self.storageClass.userName) 346 | else: 347 | logger.info('LOG OUT!') 348 | if getReceivingFnOnly: 349 | return await maintain_loop() 350 | else: 351 | def new_thread(): 352 | async def main_ok(): 353 | await asyncio.sleep(0.1) 354 | await maintain_loop() 355 | asyncio.run(main_ok()) 356 | maintainThread = threading.Thread(target=new_thread) 357 | maintainThread.setDaemon(True) 358 | maintainThread.start() 359 | 360 | def sync_check(self): 361 | url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url']) 362 | params = { 363 | 'r' : int(time.time() * 1000), 364 | 'skey' : self.loginInfo['skey'], 365 | 'sid' : self.loginInfo['wxsid'], 366 | 'uin' : self.loginInfo['wxuin'], 367 | 'deviceid' : self.loginInfo['deviceid'], 368 | 'synckey' : self.loginInfo['synckey'], 369 | '_' : self.loginInfo['logintime'], } 370 | headers = { 'User-Agent' : config.USER_AGENT} 371 | self.loginInfo['logintime'] += 1 372 | try: 373 | r = self.s.get(url, params=params, headers=headers, timeout=config.TIMEOUT) 374 | except requests.exceptions.ConnectionError as e: 375 | try: 376 | if not isinstance(e.args[0].args[1], BadStatusLine): 377 | raise 378 | # will return a package with status '0 -' 379 | # and value like: 380 | # 6f:00:8a:9c:09:74:e4:d8:e0:14:bf:96:3a:56:a0:64:1b:a4:25:5d:12:f4:31:a5:30:f1:c6:48:5f:c3:75:6a:99:93 381 | # seems like status of typing, but before I make further achievement code will remain like this 382 | return '2' 383 | except: 384 | raise 385 | r.raise_for_status() 386 | regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}' 387 | pm = re.search(regx, r.text) 388 | if pm is None or pm.group(1) != '0': 389 | logger.debug('Unexpected sync check result: %s' % r.text) 390 | return None 391 | return pm.group(2) 392 | 393 | def get_msg(self): 394 | self.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17] 395 | url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % ( 396 | self.loginInfo['url'], self.loginInfo['wxsid'], 397 | self.loginInfo['skey'],self.loginInfo['pass_ticket']) 398 | data = { 399 | 'BaseRequest' : self.loginInfo['BaseRequest'], 400 | 'SyncKey' : self.loginInfo['SyncKey'], 401 | 'rr' : ~int(time.time()), } 402 | headers = { 403 | 'ContentType': 'application/json; charset=UTF-8', 404 | 'User-Agent' : config.USER_AGENT } 405 | r = self.s.post(url, data=json.dumps(data), headers=headers, timeout=config.TIMEOUT) 406 | dic = json.loads(r.content.decode('utf-8', 'replace')) 407 | if dic['BaseResponse']['Ret'] != 0: return None, None 408 | self.loginInfo['SyncKey'] = dic['SyncKey'] 409 | self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) 410 | for item in dic['SyncCheckKey']['List']]) 411 | return dic['AddMsgList'], dic['ModContactList'] 412 | 413 | def logout(self): 414 | if self.alive: 415 | url = '%s/webwxlogout' % self.loginInfo['url'] 416 | params = { 417 | 'redirect' : 1, 418 | 'type' : 1, 419 | 'skey' : self.loginInfo['skey'], } 420 | headers = { 'User-Agent' : config.USER_AGENT} 421 | self.s.get(url, params=params, headers=headers) 422 | self.alive = False 423 | self.isLogging = False 424 | self.s.cookies.clear() 425 | del self.chatroomList[:] 426 | del self.memberList[:] 427 | del self.mpList[:] 428 | return ReturnValue({'BaseResponse': { 429 | 'ErrMsg': 'logout successfully.', 430 | 'Ret': 0, }}) 431 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/async_components/register.py: -------------------------------------------------------------------------------- 1 | import logging, traceback, sys, threading 2 | try: 3 | import Queue 4 | except ImportError: 5 | import queue as Queue # type: ignore 6 | 7 | from ..log import set_logging 8 | from ..utils import test_connect 9 | from ..storage import templates 10 | 11 | logger = logging.getLogger('itchat') 12 | 13 | def load_register(core): 14 | core.auto_login = auto_login 15 | core.configured_reply = configured_reply 16 | core.msg_register = msg_register 17 | core.run = run 18 | 19 | async def auto_login(self, hotReload=True, statusStorageDir='itchat.pkl', 20 | EventScanPayload=None,ScanStatus=None,event_stream=None, 21 | enableCmdQR=False, picDir=None, qrCallback=None, 22 | loginCallback=None, exitCallback=None): 23 | if not test_connect(): 24 | logger.info("You can't get access to internet or wechat domain, so exit.") 25 | sys.exit() 26 | self.useHotReload = hotReload 27 | self.hotReloadDir = statusStorageDir 28 | if hotReload: 29 | if await self.load_login_status(statusStorageDir, 30 | loginCallback=loginCallback, exitCallback=exitCallback): 31 | return 32 | await self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, EventScanPayload=EventScanPayload, ScanStatus=ScanStatus, event_stream=event_stream, 33 | loginCallback=loginCallback, exitCallback=exitCallback) 34 | await self.dump_login_status(statusStorageDir) 35 | else: 36 | await self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, EventScanPayload=EventScanPayload, ScanStatus=ScanStatus, event_stream=event_stream, 37 | loginCallback=loginCallback, exitCallback=exitCallback) 38 | 39 | async def configured_reply(self, event_stream, payload, message_container): 40 | ''' determine the type of message and reply if its method is defined 41 | however, I use a strange way to determine whether a msg is from massive platform 42 | I haven't found a better solution here 43 | The main problem I'm worrying about is the mismatching of new friends added on phone 44 | If you have any good idea, pleeeease report an issue. I will be more than grateful. 45 | ''' 46 | try: 47 | msg = self.msgList.get(timeout=1) 48 | if 'MsgId' in msg.keys(): 49 | message_container[msg['MsgId']] = msg 50 | except Queue.Empty: 51 | pass 52 | else: 53 | if isinstance(msg['User'], templates.User): 54 | replyFn = self.functionDict['FriendChat'].get(msg['Type']) 55 | elif isinstance(msg['User'], templates.MassivePlatform): 56 | replyFn = self.functionDict['MpChat'].get(msg['Type']) 57 | elif isinstance(msg['User'], templates.Chatroom): 58 | replyFn = self.functionDict['GroupChat'].get(msg['Type']) 59 | if replyFn is None: 60 | r = None 61 | else: 62 | try: 63 | r = await replyFn(msg) 64 | if r is not None: 65 | await self.send(r, msg.get('FromUserName')) 66 | except: 67 | logger.warning(traceback.format_exc()) 68 | 69 | def msg_register(self, msgType, isFriendChat=False, isGroupChat=False, isMpChat=False): 70 | ''' a decorator constructor 71 | return a specific decorator based on information given ''' 72 | if not (isinstance(msgType, list) or isinstance(msgType, tuple)): 73 | msgType = [msgType] 74 | def _msg_register(fn): 75 | for _msgType in msgType: 76 | if isFriendChat: 77 | self.functionDict['FriendChat'][_msgType] = fn 78 | if isGroupChat: 79 | self.functionDict['GroupChat'][_msgType] = fn 80 | if isMpChat: 81 | self.functionDict['MpChat'][_msgType] = fn 82 | if not any((isFriendChat, isGroupChat, isMpChat)): 83 | self.functionDict['FriendChat'][_msgType] = fn 84 | return fn 85 | return _msg_register 86 | 87 | async def run(self, debug=False, blockThread=True): 88 | logger.info('Start auto replying.') 89 | if debug: 90 | set_logging(loggingLevel=logging.DEBUG) 91 | async def reply_fn(): 92 | try: 93 | while self.alive: 94 | await self.configured_reply() 95 | except KeyboardInterrupt: 96 | if self.useHotReload: 97 | await self.dump_login_status() 98 | self.alive = False 99 | logger.debug('itchat received an ^C and exit.') 100 | logger.info('Bye~') 101 | if blockThread: 102 | await reply_fn() 103 | else: 104 | replyThread = threading.Thread(target=reply_fn) 105 | replyThread.setDaemon(True) 106 | replyThread.start() 107 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/components/__init__.py: -------------------------------------------------------------------------------- 1 | from .contact import load_contact 2 | from .hotreload import load_hotreload 3 | from .login import load_login 4 | from .messages import load_messages 5 | from .register import load_register 6 | 7 | def load_components(core): 8 | load_contact(core) 9 | load_hotreload(core) 10 | load_login(core) 11 | load_messages(core) 12 | load_register(core) 13 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/components/hotreload.py: -------------------------------------------------------------------------------- 1 | import pickle, os 2 | import logging 3 | 4 | import requests 5 | 6 | from ..config import VERSION 7 | from ..returnvalues import ReturnValue 8 | from ..storage import templates 9 | from .contact import update_local_chatrooms, update_local_friends 10 | from .messages import produce_msg 11 | 12 | logger = logging.getLogger('itchat') 13 | 14 | def load_hotreload(core): 15 | core.dump_login_status = dump_login_status 16 | core.load_login_status = load_login_status 17 | 18 | def dump_login_status(self, fileDir=None): 19 | fileDir = fileDir or self.hotReloadDir 20 | try: 21 | with open(fileDir, 'w') as f: 22 | f.write('itchat - DELETE THIS') 23 | os.remove(fileDir) 24 | except: 25 | raise Exception('Incorrect fileDir') 26 | status = { 27 | 'version' : VERSION, 28 | 'loginInfo' : self.loginInfo, 29 | 'cookies' : self.s.cookies.get_dict(), 30 | 'storage' : self.storageClass.dumps()} 31 | with open(fileDir, 'wb') as f: 32 | pickle.dump(status, f) 33 | logger.debug('Dump login status for hot reload successfully.') 34 | 35 | def load_login_status(self, fileDir, 36 | loginCallback=None, exitCallback=None): 37 | try: 38 | with open(fileDir, 'rb') as f: 39 | j = pickle.load(f) 40 | except Exception as e: 41 | logger.debug('No such file, loading login status failed.') 42 | return ReturnValue({'BaseResponse': { 43 | 'ErrMsg': 'No such file, loading login status failed.', 44 | 'Ret': -1002, }}) 45 | 46 | if j.get('version', '') != VERSION: 47 | logger.debug(('you have updated itchat from %s to %s, ' + 48 | 'so cached status is ignored') % ( 49 | j.get('version', 'old version'), VERSION)) 50 | return ReturnValue({'BaseResponse': { 51 | 'ErrMsg': 'cached status ignored because of version', 52 | 'Ret': -1005, }}) 53 | self.loginInfo = j['loginInfo'] 54 | self.loginInfo['User'] = templates.User(self.loginInfo['User']) 55 | self.loginInfo['User'].core = self 56 | self.s.cookies = requests.utils.cookiejar_from_dict(j['cookies']) 57 | self.storageClass.loads(j['storage']) 58 | try: 59 | msgList, contactList = self.get_msg() 60 | except: 61 | msgList = contactList = None 62 | if (msgList or contactList) is None: 63 | self.logout() 64 | load_last_login_status(self.s, j['cookies']) 65 | logger.debug('server refused, loading login status failed.') 66 | return ReturnValue({'BaseResponse': { 67 | 'ErrMsg': 'server refused, loading login status failed.', 68 | 'Ret': -1003, }}) 69 | else: 70 | if contactList: 71 | for contact in contactList: 72 | if '@@' in contact['UserName']: 73 | update_local_chatrooms(self, [contact]) 74 | else: 75 | update_local_friends(self, [contact]) 76 | if msgList: 77 | msgList = produce_msg(self, msgList) 78 | for msg in msgList: self.msgList.put(msg) 79 | self.start_receiving(exitCallback) 80 | logger.debug('loading login status succeeded.') 81 | if hasattr(loginCallback, '__call__'): 82 | loginCallback() 83 | return ReturnValue({'BaseResponse': { 84 | 'ErrMsg': 'loading login status succeeded.', 85 | 'Ret': 0, }}) 86 | 87 | def load_last_login_status(session, cookiesDict): 88 | try: 89 | session.cookies = requests.utils.cookiejar_from_dict({ 90 | 'webwxuvid': cookiesDict['webwxuvid'], 91 | 'webwx_auth_ticket': cookiesDict['webwx_auth_ticket'], 92 | 'login_frequency': '2', 93 | 'last_wxuin': cookiesDict['wxuin'], 94 | 'wxloadtime': cookiesDict['wxloadtime'] + '_expired', 95 | 'wxpluginkey': cookiesDict['wxloadtime'], 96 | 'wxuin': cookiesDict['wxuin'], 97 | 'mm_lang': 'zh_CN', 98 | 'MM_WX_NOTIFY_STATE': '1', 99 | 'MM_WX_SOUND_STATE': '1', }) 100 | except: 101 | logger.info('Load status for push login failed, we may have experienced a cookies change.') 102 | logger.info('If you are using the newest version of itchat, you may report a bug.') 103 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/components/login.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import re 4 | import io 5 | import threading 6 | import json 7 | import xml.dom.minidom 8 | import random 9 | import traceback 10 | import logging 11 | try: 12 | from httplib import BadStatusLine 13 | except ImportError: 14 | from http.client import BadStatusLine 15 | 16 | import requests 17 | from pyqrcode import QRCode 18 | 19 | from .. import config, utils 20 | from ..returnvalues import ReturnValue 21 | from ..storage.templates import wrap_user_dict 22 | from .contact import update_local_chatrooms, update_local_friends 23 | from .messages import produce_msg 24 | 25 | logger = logging.getLogger('itchat') 26 | 27 | 28 | def load_login(core): 29 | core.login = login 30 | core.get_QRuuid = get_QRuuid 31 | core.get_QR = get_QR 32 | core.check_login = check_login 33 | core.web_init = web_init 34 | core.show_mobile_login = show_mobile_login 35 | core.start_receiving = start_receiving 36 | core.get_msg = get_msg 37 | core.logout = logout 38 | 39 | 40 | def login(self, enableCmdQR=False, picDir=None, qrCallback=None, 41 | loginCallback=None, exitCallback=None): 42 | if self.alive or self.isLogging: 43 | logger.warning('itchat has already logged in.') 44 | return 45 | self.isLogging = True 46 | while self.isLogging: 47 | uuid = push_login(self) 48 | if uuid: 49 | qrStorage = io.BytesIO() 50 | else: 51 | logger.info('Getting uuid of QR code.') 52 | while not self.get_QRuuid(): 53 | time.sleep(1) 54 | logger.info('Downloading QR code.') 55 | qrStorage = self.get_QR(enableCmdQR=enableCmdQR, 56 | picDir=picDir, qrCallback=qrCallback) 57 | logger.info('Please scan the QR code to log in.') 58 | isLoggedIn = False 59 | while not isLoggedIn: 60 | status = self.check_login() 61 | if hasattr(qrCallback, '__call__'): 62 | qrCallback(uuid=self.uuid, status=status, 63 | qrcode=qrStorage.getvalue()) 64 | if status == '200': 65 | isLoggedIn = True 66 | elif status == '201': 67 | if isLoggedIn is not None: 68 | logger.info('Please press confirm on your phone.') 69 | isLoggedIn = None 70 | time.sleep(6) 71 | elif status != '408': 72 | break 73 | if isLoggedIn: 74 | break 75 | elif self.isLogging: 76 | logger.info('Log in time out, reloading QR code.') 77 | else: 78 | return # log in process is stopped by user 79 | logger.info('Loading the contact, this may take a little while.') 80 | self.web_init() 81 | self.show_mobile_login() 82 | self.get_contact(True) 83 | if hasattr(loginCallback, '__call__'): 84 | r = loginCallback() 85 | else: 86 | utils.clear_screen() 87 | if os.path.exists(picDir or config.DEFAULT_QR): 88 | os.remove(picDir or config.DEFAULT_QR) 89 | logger.info('Login successfully as %s' % self.storageClass.nickName) 90 | self.start_receiving(exitCallback) 91 | self.isLogging = False 92 | 93 | 94 | def push_login(core): 95 | cookiesDict = core.s.cookies.get_dict() 96 | if 'wxuin' in cookiesDict: 97 | url = '%s/cgi-bin/mmwebwx-bin/webwxpushloginurl?uin=%s' % ( 98 | config.BASE_URL, cookiesDict['wxuin']) 99 | headers = {'User-Agent': config.USER_AGENT} 100 | r = core.s.get(url, headers=headers).json() 101 | if 'uuid' in r and r.get('ret') in (0, '0'): 102 | core.uuid = r['uuid'] 103 | return r['uuid'] 104 | return False 105 | 106 | 107 | def get_QRuuid(self): 108 | url = '%s/jslogin' % config.BASE_URL 109 | params = { 110 | 'appid': 'wx782c26e4c19acffb', 111 | 'fun': 'new', 112 | 'redirect_uri': 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop', 113 | 'lang': 'zh_CN'} 114 | headers = {'User-Agent': config.USER_AGENT} 115 | r = self.s.get(url, params=params, headers=headers) 116 | regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";' 117 | data = re.search(regx, r.text) 118 | if data and data.group(1) == '200': 119 | self.uuid = data.group(2) 120 | return self.uuid 121 | 122 | 123 | def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None): 124 | uuid = uuid or self.uuid 125 | picDir = picDir or config.DEFAULT_QR 126 | qrStorage = io.BytesIO() 127 | qrCode = QRCode('https://login.weixin.qq.com/l/' + uuid) 128 | qrCode.png(qrStorage, scale=10) 129 | if hasattr(qrCallback, '__call__'): 130 | qrCallback(uuid=uuid, status='0', qrcode=qrStorage.getvalue()) 131 | else: 132 | with open(picDir, 'wb') as f: 133 | f.write(qrStorage.getvalue()) 134 | if enableCmdQR: 135 | utils.print_cmd_qr(qrCode.text(1), enableCmdQR=enableCmdQR) 136 | else: 137 | utils.print_qr(picDir) 138 | return qrStorage 139 | 140 | 141 | def check_login(self, uuid=None): 142 | uuid = uuid or self.uuid 143 | url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL 144 | localTime = int(time.time()) 145 | params = 'loginicon=true&uuid=%s&tip=1&r=%s&_=%s' % ( 146 | uuid, int(-localTime / 1579), localTime) 147 | headers = {'User-Agent': config.USER_AGENT} 148 | r = self.s.get(url, params=params, headers=headers) 149 | regx = r'window.code=(\d+)' 150 | data = re.search(regx, r.text) 151 | if data and data.group(1) == '200': 152 | if process_login_info(self, r.text): 153 | return '200' 154 | else: 155 | return '400' 156 | elif data: 157 | return data.group(1) 158 | else: 159 | return '400' 160 | 161 | 162 | def process_login_info(core, loginContent): 163 | ''' when finish login (scanning qrcode) 164 | * syncUrl and fileUploadingUrl will be fetched 165 | * deviceid and msgid will be generated 166 | * skey, wxsid, wxuin, pass_ticket will be fetched 167 | ''' 168 | regx = r'window.redirect_uri="(\S+)";' 169 | core.loginInfo['url'] = re.search(regx, loginContent).group(1) 170 | headers = {'User-Agent': config.USER_AGENT, 171 | 'client-version': config.UOS_PATCH_CLIENT_VERSION, 172 | 'extspam': config.UOS_PATCH_EXTSPAM, 173 | 'referer': 'https://wx.qq.com/?&lang=zh_CN&target=t' 174 | } 175 | r = core.s.get(core.loginInfo['url'], 176 | headers=headers, allow_redirects=False) 177 | core.loginInfo['url'] = core.loginInfo['url'][:core.loginInfo['url'].rfind( 178 | '/')] 179 | for indexUrl, detailedUrl in ( 180 | ("wx2.qq.com", ("file.wx2.qq.com", "webpush.wx2.qq.com")), 181 | ("wx8.qq.com", ("file.wx8.qq.com", "webpush.wx8.qq.com")), 182 | ("qq.com", ("file.wx.qq.com", "webpush.wx.qq.com")), 183 | ("web2.wechat.com", ("file.web2.wechat.com", "webpush.web2.wechat.com")), 184 | ("wechat.com", ("file.web.wechat.com", "webpush.web.wechat.com"))): 185 | fileUrl, syncUrl = ['https://%s/cgi-bin/mmwebwx-bin' % 186 | url for url in detailedUrl] 187 | if indexUrl in core.loginInfo['url']: 188 | core.loginInfo['fileUrl'], core.loginInfo['syncUrl'] = \ 189 | fileUrl, syncUrl 190 | break 191 | else: 192 | core.loginInfo['fileUrl'] = core.loginInfo['syncUrl'] = core.loginInfo['url'] 193 | core.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17] 194 | core.loginInfo['logintime'] = int(time.time() * 1e3) 195 | core.loginInfo['BaseRequest'] = {} 196 | cookies = core.s.cookies.get_dict() 197 | skey = re.findall('(.*?)', r.text, re.S)[0] 198 | pass_ticket = re.findall( 199 | '(.*?)', r.text, re.S)[0] 200 | core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = skey 201 | core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = cookies["wxsid"] 202 | core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = cookies["wxuin"] 203 | core.loginInfo['pass_ticket'] = pass_ticket 204 | # A question : why pass_ticket == DeviceID ? 205 | # deviceID is only a randomly generated number 206 | 207 | # UOS PATCH By luvletter2333, Sun Feb 28 10:00 PM 208 | # for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes: 209 | # if node.nodeName == 'skey': 210 | # core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data 211 | # elif node.nodeName == 'wxsid': 212 | # core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data 213 | # elif node.nodeName == 'wxuin': 214 | # core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data 215 | # elif node.nodeName == 'pass_ticket': 216 | # core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data 217 | if not all([key in core.loginInfo for key in ('skey', 'wxsid', 'wxuin', 'pass_ticket')]): 218 | logger.error( 219 | 'Your wechat account may be LIMITED to log in WEB wechat, error info:\n%s' % r.text) 220 | core.isLogging = False 221 | return False 222 | return True 223 | 224 | 225 | def web_init(self): 226 | url = '%s/webwxinit' % self.loginInfo['url'] 227 | params = { 228 | 'r': int(-time.time() / 1579), 229 | 'pass_ticket': self.loginInfo['pass_ticket'], } 230 | data = {'BaseRequest': self.loginInfo['BaseRequest'], } 231 | headers = { 232 | 'ContentType': 'application/json; charset=UTF-8', 233 | 'User-Agent': config.USER_AGENT, } 234 | r = self.s.post(url, params=params, data=json.dumps(data), headers=headers) 235 | dic = json.loads(r.content.decode('utf-8', 'replace')) 236 | # deal with login info 237 | utils.emoji_formatter(dic['User'], 'NickName') 238 | self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount']) 239 | self.loginInfo['User'] = wrap_user_dict( 240 | utils.struct_friend_info(dic['User'])) 241 | self.memberList.append(self.loginInfo['User']) 242 | self.loginInfo['SyncKey'] = dic['SyncKey'] 243 | self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) 244 | for item in dic['SyncKey']['List']]) 245 | self.storageClass.userName = dic['User']['UserName'] 246 | self.storageClass.nickName = dic['User']['NickName'] 247 | # deal with contact list returned when init 248 | contactList = dic.get('ContactList', []) 249 | chatroomList, otherList = [], [] 250 | for m in contactList: 251 | if m['Sex'] != 0: 252 | otherList.append(m) 253 | elif '@@' in m['UserName']: 254 | m['MemberList'] = [] # don't let dirty info pollute the list 255 | chatroomList.append(m) 256 | elif '@' in m['UserName']: 257 | # mp will be dealt in update_local_friends as well 258 | otherList.append(m) 259 | if chatroomList: 260 | update_local_chatrooms(self, chatroomList) 261 | if otherList: 262 | update_local_friends(self, otherList) 263 | return dic 264 | 265 | 266 | def show_mobile_login(self): 267 | url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % ( 268 | self.loginInfo['url'], self.loginInfo['pass_ticket']) 269 | data = { 270 | 'BaseRequest': self.loginInfo['BaseRequest'], 271 | 'Code': 3, 272 | 'FromUserName': self.storageClass.userName, 273 | 'ToUserName': self.storageClass.userName, 274 | 'ClientMsgId': int(time.time()), } 275 | headers = { 276 | 'ContentType': 'application/json; charset=UTF-8', 277 | 'User-Agent': config.USER_AGENT, } 278 | r = self.s.post(url, data=json.dumps(data), headers=headers) 279 | return ReturnValue(rawResponse=r) 280 | 281 | 282 | def start_receiving(self, exitCallback=None, getReceivingFnOnly=False): 283 | self.alive = True 284 | 285 | def maintain_loop(): 286 | retryCount = 0 287 | while self.alive: 288 | try: 289 | i = sync_check(self) 290 | if i is None: 291 | self.alive = False 292 | elif i == '0': 293 | pass 294 | else: 295 | msgList, contactList = self.get_msg() 296 | if msgList: 297 | msgList = produce_msg(self, msgList) 298 | for msg in msgList: 299 | self.msgList.put(msg) 300 | if contactList: 301 | chatroomList, otherList = [], [] 302 | for contact in contactList: 303 | if '@@' in contact['UserName']: 304 | chatroomList.append(contact) 305 | else: 306 | otherList.append(contact) 307 | chatroomMsg = update_local_chatrooms( 308 | self, chatroomList) 309 | chatroomMsg['User'] = self.loginInfo['User'] 310 | self.msgList.put(chatroomMsg) 311 | update_local_friends(self, otherList) 312 | retryCount = 0 313 | except requests.exceptions.ReadTimeout: 314 | pass 315 | except: 316 | retryCount += 1 317 | logger.error(traceback.format_exc()) 318 | if self.receivingRetryCount < retryCount: 319 | self.alive = False 320 | else: 321 | time.sleep(1) 322 | self.logout() 323 | if hasattr(exitCallback, '__call__'): 324 | exitCallback() 325 | else: 326 | logger.info('LOG OUT!') 327 | if getReceivingFnOnly: 328 | return maintain_loop 329 | else: 330 | maintainThread = threading.Thread(target=maintain_loop) 331 | maintainThread.setDaemon(True) 332 | maintainThread.start() 333 | 334 | 335 | def sync_check(self): 336 | url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url']) 337 | params = { 338 | 'r': int(time.time() * 1000), 339 | 'skey': self.loginInfo['skey'], 340 | 'sid': self.loginInfo['wxsid'], 341 | 'uin': self.loginInfo['wxuin'], 342 | 'deviceid': self.loginInfo['deviceid'], 343 | 'synckey': self.loginInfo['synckey'], 344 | '_': self.loginInfo['logintime'], } 345 | headers = {'User-Agent': config.USER_AGENT} 346 | self.loginInfo['logintime'] += 1 347 | try: 348 | r = self.s.get(url, params=params, headers=headers, 349 | timeout=config.TIMEOUT) 350 | except requests.exceptions.ConnectionError as e: 351 | try: 352 | if not isinstance(e.args[0].args[1], BadStatusLine): 353 | raise 354 | # will return a package with status '0 -' 355 | # and value like: 356 | # 6f:00:8a:9c:09:74:e4:d8:e0:14:bf:96:3a:56:a0:64:1b:a4:25:5d:12:f4:31:a5:30:f1:c6:48:5f:c3:75:6a:99:93 357 | # seems like status of typing, but before I make further achievement code will remain like this 358 | return '2' 359 | except: 360 | raise 361 | r.raise_for_status() 362 | regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}' 363 | pm = re.search(regx, r.text) 364 | if pm is None or pm.group(1) != '0': 365 | logger.debug('Unexpected sync check result: %s' % r.text) 366 | return None 367 | return pm.group(2) 368 | 369 | 370 | def get_msg(self): 371 | self.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17] 372 | url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % ( 373 | self.loginInfo['url'], self.loginInfo['wxsid'], 374 | self.loginInfo['skey'], self.loginInfo['pass_ticket']) 375 | data = { 376 | 'BaseRequest': self.loginInfo['BaseRequest'], 377 | 'SyncKey': self.loginInfo['SyncKey'], 378 | 'rr': ~int(time.time()), } 379 | headers = { 380 | 'ContentType': 'application/json; charset=UTF-8', 381 | 'User-Agent': config.USER_AGENT} 382 | r = self.s.post(url, data=json.dumps(data), 383 | headers=headers, timeout=config.TIMEOUT) 384 | dic = json.loads(r.content.decode('utf-8', 'replace')) 385 | if dic['BaseResponse']['Ret'] != 0: 386 | return None, None 387 | self.loginInfo['SyncKey'] = dic['SyncKey'] 388 | self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) 389 | for item in dic['SyncCheckKey']['List']]) 390 | return dic['AddMsgList'], dic['ModContactList'] 391 | 392 | 393 | def logout(self): 394 | if self.alive: 395 | url = '%s/webwxlogout' % self.loginInfo['url'] 396 | params = { 397 | 'redirect': 1, 398 | 'type': 1, 399 | 'skey': self.loginInfo['skey'], } 400 | headers = {'User-Agent': config.USER_AGENT} 401 | self.s.get(url, params=params, headers=headers) 402 | self.alive = False 403 | self.isLogging = False 404 | self.s.cookies.clear() 405 | del self.chatroomList[:] 406 | del self.memberList[:] 407 | del self.mpList[:] 408 | return ReturnValue({'BaseResponse': { 409 | 'ErrMsg': 'logout successfully.', 410 | 'Ret': 0, }}) 411 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/components/register.py: -------------------------------------------------------------------------------- 1 | import logging, traceback, sys, threading 2 | try: 3 | import Queue 4 | except ImportError: 5 | import queue as Queue 6 | 7 | from ..log import set_logging 8 | from ..utils import test_connect 9 | from ..storage import templates 10 | 11 | logger = logging.getLogger('itchat') 12 | 13 | def load_register(core): 14 | core.auto_login = auto_login 15 | core.configured_reply = configured_reply 16 | core.msg_register = msg_register 17 | core.run = run 18 | 19 | def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl', 20 | enableCmdQR=False, picDir=None, qrCallback=None, 21 | loginCallback=None, exitCallback=None): 22 | if not test_connect(): 23 | logger.info("You can't get access to internet or wechat domain, so exit.") 24 | sys.exit() 25 | self.useHotReload = hotReload 26 | self.hotReloadDir = statusStorageDir 27 | if hotReload: 28 | if self.load_login_status(statusStorageDir, 29 | loginCallback=loginCallback, exitCallback=exitCallback): 30 | return 31 | self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, 32 | loginCallback=loginCallback, exitCallback=exitCallback) 33 | self.dump_login_status(statusStorageDir) 34 | else: 35 | self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, 36 | loginCallback=loginCallback, exitCallback=exitCallback) 37 | 38 | def configured_reply(self): 39 | ''' determine the type of message and reply if its method is defined 40 | however, I use a strange way to determine whether a msg is from massive platform 41 | I haven't found a better solution here 42 | The main problem I'm worrying about is the mismatching of new friends added on phone 43 | If you have any good idea, pleeeease report an issue. I will be more than grateful. 44 | ''' 45 | try: 46 | msg = self.msgList.get(timeout=1) 47 | except Queue.Empty: 48 | pass 49 | else: 50 | if isinstance(msg['User'], templates.User): 51 | replyFn = self.functionDict['FriendChat'].get(msg['Type']) 52 | elif isinstance(msg['User'], templates.MassivePlatform): 53 | replyFn = self.functionDict['MpChat'].get(msg['Type']) 54 | elif isinstance(msg['User'], templates.Chatroom): 55 | replyFn = self.functionDict['GroupChat'].get(msg['Type']) 56 | if replyFn is None: 57 | r = None 58 | else: 59 | try: 60 | r = replyFn(msg) 61 | if r is not None: 62 | self.send(r, msg.get('FromUserName')) 63 | except: 64 | logger.warning(traceback.format_exc()) 65 | 66 | def msg_register(self, msgType, isFriendChat=False, isGroupChat=False, isMpChat=False): 67 | ''' a decorator constructor 68 | return a specific decorator based on information given ''' 69 | if not (isinstance(msgType, list) or isinstance(msgType, tuple)): 70 | msgType = [msgType] 71 | def _msg_register(fn): 72 | for _msgType in msgType: 73 | if isFriendChat: 74 | self.functionDict['FriendChat'][_msgType] = fn 75 | if isGroupChat: 76 | self.functionDict['GroupChat'][_msgType] = fn 77 | if isMpChat: 78 | self.functionDict['MpChat'][_msgType] = fn 79 | if not any((isFriendChat, isGroupChat, isMpChat)): 80 | self.functionDict['FriendChat'][_msgType] = fn 81 | return fn 82 | return _msg_register 83 | 84 | def run(self, debug=False, blockThread=True): 85 | logger.info('Start auto replying.') 86 | if debug: 87 | set_logging(loggingLevel=logging.DEBUG) 88 | def reply_fn(): 89 | try: 90 | while self.alive: 91 | self.configured_reply() 92 | except KeyboardInterrupt: 93 | if self.useHotReload: 94 | self.dump_login_status() 95 | self.alive = False 96 | logger.debug('itchat received an ^C and exit.') 97 | logger.info('Bye~') 98 | if blockThread: 99 | reply_fn() 100 | else: 101 | replyThread = threading.Thread(target=reply_fn) 102 | replyThread.setDaemon(True) 103 | replyThread.start() 104 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/config.py: -------------------------------------------------------------------------------- 1 | import os, platform 2 | 3 | VERSION = '1.5.0.dev' 4 | 5 | # use this envrionment to initialize the async & sync componment 6 | ASYNC_COMPONENTS = os.environ.get('ITCHAT_UOS_ASYNC', True) 7 | 8 | BASE_URL = 'https://login.weixin.qq.com' 9 | OS = platform.system() # Windows, Linux, Darwin 10 | DIR = os.getcwd() 11 | DEFAULT_QR = 'QR.png' 12 | TIMEOUT = (10, 60) 13 | 14 | USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36' 15 | 16 | UOS_PATCH_CLIENT_VERSION = '2.0.0' 17 | UOS_PATCH_EXTSPAM = 'Go8FCIkFEokFCggwMDAwMDAwMRAGGvAESySibk50w5Wb3uTl2c2h64jVVrV7gNs06GFlWplHQbY/5FfiO++1yH4ykCyNPWKXmco+wfQzK5R98D3so7rJ5LmGFvBLjGceleySrc3SOf2Pc1gVehzJgODeS0lDL3/I/0S2SSE98YgKleq6Uqx6ndTy9yaL9qFxJL7eiA/R3SEfTaW1SBoSITIu+EEkXff+Pv8NHOk7N57rcGk1w0ZzRrQDkXTOXFN2iHYIzAAZPIOY45Lsh+A4slpgnDiaOvRtlQYCt97nmPLuTipOJ8Qc5pM7ZsOsAPPrCQL7nK0I7aPrFDF0q4ziUUKettzW8MrAaiVfmbD1/VkmLNVqqZVvBCtRblXb5FHmtS8FxnqCzYP4WFvz3T0TcrOqwLX1M/DQvcHaGGw0B0y4bZMs7lVScGBFxMj3vbFi2SRKbKhaitxHfYHAOAa0X7/MSS0RNAjdwoyGHeOepXOKY+h3iHeqCvgOH6LOifdHf/1aaZNwSkGotYnYScW8Yx63LnSwba7+hESrtPa/huRmB9KWvMCKbDThL/nne14hnL277EDCSocPu3rOSYjuB9gKSOdVmWsj9Dxb/iZIe+S6AiG29Esm+/eUacSba0k8wn5HhHg9d4tIcixrxveflc8vi2/wNQGVFNsGO6tB5WF0xf/plngOvQ1/ivGV/C1Qpdhzznh0ExAVJ6dwzNg7qIEBaw+BzTJTUuRcPk92Sn6QDn2Pu3mpONaEumacjW4w6ipPnPw+g2TfywJjeEcpSZaP4Q3YV5HG8D6UjWA4GSkBKculWpdCMadx0usMomsSS/74QgpYqcPkmamB4nVv1JxczYITIqItIKjD35IGKAUwAA==' 18 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/content.py: -------------------------------------------------------------------------------- 1 | TEXT = 'Text' 2 | MAP = 'Map' 3 | CARD = 'Card' 4 | NOTE = 'Note' 5 | SHARING = 'Sharing' 6 | PICTURE = 'Picture' 7 | RECORDING = VOICE = 'Recording' 8 | ATTACHMENT = 'Attachment' 9 | VIDEO = 'Video' 10 | FRIENDS = 'Friends' 11 | SYSTEM = 'System' 12 | 13 | INCOME_MSG = [TEXT, MAP, CARD, NOTE, SHARING, PICTURE, 14 | RECORDING, VOICE, ATTACHMENT, VIDEO, FRIENDS, SYSTEM] 15 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/core.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from . import storage 4 | 5 | class Core(object): 6 | def __init__(self): 7 | ''' init is the only method defined in core.py 8 | alive is value showing whether core is running 9 | - you should call logout method to change it 10 | - after logout, a core object can login again 11 | storageClass only uses basic python types 12 | - so for advanced uses, inherit it yourself 13 | receivingRetryCount is for receiving loop retry 14 | - it's 5 now, but actually even 1 is enough 15 | - failing is failing 16 | ''' 17 | self.alive, self.isLogging = False, False 18 | self.storageClass = storage.Storage(self) 19 | self.memberList = self.storageClass.memberList 20 | self.mpList = self.storageClass.mpList 21 | self.chatroomList = self.storageClass.chatroomList 22 | self.msgList = self.storageClass.msgList 23 | self.loginInfo = {} 24 | self.s = requests.Session() 25 | self.uuid = None 26 | self.functionDict = {'FriendChat': {}, 'GroupChat': {}, 'MpChat': {}} 27 | self.useHotReload, self.hotReloadDir = False, 'itchat.pkl' 28 | self.receivingRetryCount = 5 29 | def login(self, enableCmdQR=False, picDir=None, qrCallback=None, 30 | loginCallback=None, exitCallback=None): 31 | ''' log in like web wechat does 32 | for log in 33 | - a QR code will be downloaded and opened 34 | - then scanning status is logged, it paused for you confirm 35 | - finally it logged in and show your nickName 36 | for options 37 | - enableCmdQR: show qrcode in command line 38 | - integers can be used to fit strange char length 39 | - picDir: place for storing qrcode 40 | - qrCallback: method that should accept uuid, status, qrcode 41 | - loginCallback: callback after successfully logged in 42 | - if not set, screen is cleared and qrcode is deleted 43 | - exitCallback: callback after logged out 44 | - it contains calling of logout 45 | for usage 46 | ..code::python 47 | 48 | import itchat 49 | itchat.login() 50 | 51 | it is defined in components/login.py 52 | and of course every single move in login can be called outside 53 | - you may scan source code to see how 54 | - and modified according to your own demand 55 | ''' 56 | raise NotImplementedError() 57 | def get_QRuuid(self): 58 | ''' get uuid for qrcode 59 | uuid is the symbol of qrcode 60 | - for logging in, you need to get a uuid first 61 | - for downloading qrcode, you need to pass uuid to it 62 | - for checking login status, uuid is also required 63 | if uuid has timed out, just get another 64 | it is defined in components/login.py 65 | ''' 66 | raise NotImplementedError() 67 | def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None): 68 | ''' download and show qrcode 69 | for options 70 | - uuid: if uuid is not set, latest uuid you fetched will be used 71 | - enableCmdQR: show qrcode in cmd 72 | - picDir: where to store qrcode 73 | - qrCallback: method that should accept uuid, status, qrcode 74 | it is defined in components/login.py 75 | ''' 76 | raise NotImplementedError() 77 | def check_login(self, uuid=None): 78 | ''' check login status 79 | for options: 80 | - uuid: if uuid is not set, latest uuid you fetched will be used 81 | for return values: 82 | - a string will be returned 83 | - for meaning of return values 84 | - 200: log in successfully 85 | - 201: waiting for press confirm 86 | - 408: uuid timed out 87 | - 0 : unknown error 88 | for processing: 89 | - syncUrl and fileUrl is set 90 | - BaseRequest is set 91 | blocks until reaches any of above status 92 | it is defined in components/login.py 93 | ''' 94 | raise NotImplementedError() 95 | def web_init(self): 96 | ''' get info necessary for initializing 97 | for processing: 98 | - own account info is set 99 | - inviteStartCount is set 100 | - syncKey is set 101 | - part of contact is fetched 102 | it is defined in components/login.py 103 | ''' 104 | raise NotImplementedError() 105 | def show_mobile_login(self): 106 | ''' show web wechat login sign 107 | the sign is on the top of mobile phone wechat 108 | sign will be added after sometime even without calling this function 109 | it is defined in components/login.py 110 | ''' 111 | raise NotImplementedError() 112 | def start_receiving(self, exitCallback=None, getReceivingFnOnly=False): 113 | ''' open a thread for heart loop and receiving messages 114 | for options: 115 | - exitCallback: callback after logged out 116 | - it contains calling of logout 117 | - getReceivingFnOnly: if True thread will not be created and started. Instead, receive fn will be returned. 118 | for processing: 119 | - messages: msgs are formatted and passed on to registered fns 120 | - contact : chatrooms are updated when related info is received 121 | it is defined in components/login.py 122 | ''' 123 | raise NotImplementedError() 124 | def get_msg(self): 125 | ''' fetch messages 126 | for fetching 127 | - method blocks for sometime until 128 | - new messages are to be received 129 | - or anytime they like 130 | - synckey is updated with returned synccheckkey 131 | it is defined in components/login.py 132 | ''' 133 | raise NotImplementedError() 134 | def logout(self): 135 | ''' logout 136 | if core is now alive 137 | logout will tell wechat backstage to logout 138 | and core gets ready for another login 139 | it is defined in components/login.py 140 | ''' 141 | raise NotImplementedError() 142 | def update_chatroom(self, userName, detailedMember=False): 143 | ''' update chatroom 144 | for chatroom contact 145 | - a chatroom contact need updating to be detailed 146 | - detailed means members, encryid, etc 147 | - auto updating of heart loop is a more detailed updating 148 | - member uin will also be filled 149 | - once called, updated info will be stored 150 | for options 151 | - userName: 'UserName' key of chatroom or a list of it 152 | - detailedMember: whether to get members of contact 153 | it is defined in components/contact.py 154 | ''' 155 | raise NotImplementedError() 156 | def update_friend(self, userName): 157 | ''' update chatroom 158 | for friend contact 159 | - once called, updated info will be stored 160 | for options 161 | - userName: 'UserName' key of a friend or a list of it 162 | it is defined in components/contact.py 163 | ''' 164 | raise NotImplementedError() 165 | def get_contact(self, update=False): 166 | ''' fetch part of contact 167 | for part 168 | - all the massive platforms and friends are fetched 169 | - if update, only starred chatrooms are fetched 170 | for options 171 | - update: if not set, local value will be returned 172 | for results 173 | - chatroomList will be returned 174 | it is defined in components/contact.py 175 | ''' 176 | raise NotImplementedError() 177 | def get_friends(self, update=False): 178 | ''' fetch friends list 179 | for options 180 | - update: if not set, local value will be returned 181 | for results 182 | - a list of friends' info dicts will be returned 183 | it is defined in components/contact.py 184 | ''' 185 | raise NotImplementedError() 186 | def get_chatrooms(self, update=False, contactOnly=False): 187 | ''' fetch chatrooms list 188 | for options 189 | - update: if not set, local value will be returned 190 | - contactOnly: if set, only starred chatrooms will be returned 191 | for results 192 | - a list of chatrooms' info dicts will be returned 193 | it is defined in components/contact.py 194 | ''' 195 | raise NotImplementedError() 196 | def get_mps(self, update=False): 197 | ''' fetch massive platforms list 198 | for options 199 | - update: if not set, local value will be returned 200 | for results 201 | - a list of platforms' info dicts will be returned 202 | it is defined in components/contact.py 203 | ''' 204 | raise NotImplementedError() 205 | def set_alias(self, userName, alias): 206 | ''' set alias for a friend 207 | for options 208 | - userName: 'UserName' key of info dict 209 | - alias: new alias 210 | it is defined in components/contact.py 211 | ''' 212 | raise NotImplementedError() 213 | def set_pinned(self, userName, isPinned=True): 214 | ''' set pinned for a friend or a chatroom 215 | for options 216 | - userName: 'UserName' key of info dict 217 | - isPinned: whether to pin 218 | it is defined in components/contact.py 219 | ''' 220 | raise NotImplementedError() 221 | def accept_friend(self, userName, v4,autoUpdate=True): 222 | ''' accept a friend or accept a friend 223 | for options 224 | - userName: 'UserName' for friend's info dict 225 | - status: 226 | - for adding status should be 2 227 | - for accepting status should be 3 228 | - ticket: greeting message 229 | - userInfo: friend's other info for adding into local storage 230 | it is defined in components/contact.py 231 | ''' 232 | raise NotImplementedError() 233 | def get_head_img(self, userName=None, chatroomUserName=None, picDir=None): 234 | ''' place for docs 235 | for options 236 | - if you want to get chatroom header: only set chatroomUserName 237 | - if you want to get friend header: only set userName 238 | - if you want to get chatroom member header: set both 239 | it is defined in components/contact.py 240 | ''' 241 | raise NotImplementedError() 242 | def create_chatroom(self, memberList, topic=''): 243 | ''' create a chatroom 244 | for creating 245 | - its calling frequency is strictly limited 246 | for options 247 | - memberList: list of member info dict 248 | - topic: topic of new chatroom 249 | it is defined in components/contact.py 250 | ''' 251 | raise NotImplementedError() 252 | def set_chatroom_name(self, chatroomUserName, name): 253 | ''' set chatroom name 254 | for setting 255 | - it makes an updating of chatroom 256 | - which means detailed info will be returned in heart loop 257 | for options 258 | - chatroomUserName: 'UserName' key of chatroom info dict 259 | - name: new chatroom name 260 | it is defined in components/contact.py 261 | ''' 262 | raise NotImplementedError() 263 | def delete_member_from_chatroom(self, chatroomUserName, memberList): 264 | ''' deletes members from chatroom 265 | for deleting 266 | - you can't delete yourself 267 | - if so, no one will be deleted 268 | - strict-limited frequency 269 | for options 270 | - chatroomUserName: 'UserName' key of chatroom info dict 271 | - memberList: list of members' info dict 272 | it is defined in components/contact.py 273 | ''' 274 | raise NotImplementedError() 275 | def add_member_into_chatroom(self, chatroomUserName, memberList, 276 | useInvitation=False): 277 | ''' add members into chatroom 278 | for adding 279 | - you can't add yourself or member already in chatroom 280 | - if so, no one will be added 281 | - if member will over 40 after adding, invitation must be used 282 | - strict-limited frequency 283 | for options 284 | - chatroomUserName: 'UserName' key of chatroom info dict 285 | - memberList: list of members' info dict 286 | - useInvitation: if invitation is not required, set this to use 287 | it is defined in components/contact.py 288 | ''' 289 | raise NotImplementedError() 290 | def send_raw_msg(self, msgType, content, toUserName): 291 | ''' many messages are sent in a common way 292 | for demo 293 | .. code:: python 294 | 295 | @itchat.msg_register(itchat.content.CARD) 296 | def reply(msg): 297 | itchat.send_raw_msg(msg['MsgType'], msg['Content'], msg['FromUserName']) 298 | 299 | there are some little tricks here, you may discover them yourself 300 | but remember they are tricks 301 | it is defined in components/messages.py 302 | ''' 303 | raise NotImplementedError() 304 | def send_msg(self, msg='Test Message', toUserName=None): 305 | ''' send plain text message 306 | for options 307 | - msg: should be unicode if there's non-ascii words in msg 308 | - toUserName: 'UserName' key of friend dict 309 | it is defined in components/messages.py 310 | ''' 311 | raise NotImplementedError() 312 | def upload_file(self, fileDir, isPicture=False, isVideo=False, 313 | toUserName='filehelper', file_=None, preparedFile=None): 314 | ''' upload file to server and get mediaId 315 | for options 316 | - fileDir: dir for file ready for upload 317 | - isPicture: whether file is a picture 318 | - isVideo: whether file is a video 319 | for return values 320 | will return a ReturnValue 321 | if succeeded, mediaId is in r['MediaId'] 322 | it is defined in components/messages.py 323 | ''' 324 | raise NotImplementedError() 325 | def send_file(self, fileDir, toUserName=None, mediaId=None, file_=None): 326 | ''' send attachment 327 | for options 328 | - fileDir: dir for file ready for upload 329 | - mediaId: mediaId for file. 330 | - if set, file will not be uploaded twice 331 | - toUserName: 'UserName' key of friend dict 332 | it is defined in components/messages.py 333 | ''' 334 | raise NotImplementedError() 335 | 336 | def send_image(self, fileDir=None, toUserName=None, mediaId=None, file_=None): 337 | ''' send image 338 | for options 339 | - fileDir: dir for file ready for upload 340 | - if it's a gif, name it like 'xx.gif' 341 | - mediaId: mediaId for file. 342 | - if set, file will not be uploaded twice 343 | - toUserName: 'UserName' key of friend dict 344 | it is defined in components/messages.py 345 | ''' 346 | raise NotImplementedError() 347 | def send_video(self, fileDir=None, toUserName=None, mediaId=None, file_=None): 348 | ''' send video 349 | for options 350 | - fileDir: dir for file ready for upload 351 | - if mediaId is set, it's unnecessary to set fileDir 352 | - mediaId: mediaId for file. 353 | - if set, file will not be uploaded twice 354 | - toUserName: 'UserName' key of friend dict 355 | it is defined in components/messages.py 356 | ''' 357 | raise NotImplementedError() 358 | def send(self, msg, toUserName=None, mediaId=None): 359 | ''' wrapped function for all the sending functions 360 | for options 361 | - msg: message starts with different string indicates different type 362 | - list of type string: ['@fil@', '@img@', '@msg@', '@vid@'] 363 | - they are for file, image, plain text, video 364 | - if none of them matches, it will be sent like plain text 365 | - toUserName: 'UserName' key of friend dict 366 | - mediaId: if set, uploading will not be repeated 367 | it is defined in components/messages.py 368 | ''' 369 | raise NotImplementedError() 370 | def revoke(self, msgId, toUserName, localId=None): 371 | ''' revoke message with its and msgId 372 | for options 373 | - msgId: message Id on server 374 | - toUserName: 'UserName' key of friend dict 375 | - localId: message Id at local (optional) 376 | it is defined in components/messages.py 377 | ''' 378 | raise NotImplementedError() 379 | def dump_login_status(self, fileDir=None): 380 | ''' dump login status to a specific file 381 | for option 382 | - fileDir: dir for dumping login status 383 | it is defined in components/hotreload.py 384 | ''' 385 | raise NotImplementedError() 386 | def load_login_status(self, fileDir, 387 | loginCallback=None, exitCallback=None): 388 | ''' load login status from a specific file 389 | for option 390 | - fileDir: file for loading login status 391 | - loginCallback: callback after successfully logged in 392 | - if not set, screen is cleared and qrcode is deleted 393 | - exitCallback: callback after logged out 394 | - it contains calling of logout 395 | it is defined in components/hotreload.py 396 | ''' 397 | raise NotImplementedError() 398 | def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl', 399 | EventScanPayload=None,ScanStatus=None,event_stream=None, 400 | enableCmdQR=False, picDir=None, qrCallback=None, 401 | loginCallback=None, exitCallback=None): 402 | ''' log in like web wechat does 403 | for log in 404 | - a QR code will be downloaded and opened 405 | - then scanning status is logged, it paused for you confirm 406 | - finally it logged in and show your nickName 407 | for options 408 | - hotReload: enable hot reload 409 | - statusStorageDir: dir for storing log in status 410 | - enableCmdQR: show qrcode in command line 411 | - integers can be used to fit strange char length 412 | - picDir: place for storing qrcode 413 | - loginCallback: callback after successfully logged in 414 | - if not set, screen is cleared and qrcode is deleted 415 | - exitCallback: callback after logged out 416 | - it contains calling of logout 417 | - qrCallback: method that should accept uuid, status, qrcode 418 | for usage 419 | ..code::python 420 | 421 | import itchat 422 | itchat.auto_login() 423 | 424 | it is defined in components/register.py 425 | and of course every single move in login can be called outside 426 | - you may scan source code to see how 427 | - and modified according to your own demond 428 | ''' 429 | raise NotImplementedError() 430 | def configured_reply(self): 431 | ''' determine the type of message and reply if its method is defined 432 | however, I use a strange way to determine whether a msg is from massive platform 433 | I haven't found a better solution here 434 | The main problem I'm worrying about is the mismatching of new friends added on phone 435 | If you have any good idea, pleeeease report an issue. I will be more than grateful. 436 | ''' 437 | raise NotImplementedError() 438 | def msg_register(self, msgType, 439 | isFriendChat=False, isGroupChat=False, isMpChat=False): 440 | ''' a decorator constructor 441 | return a specific decorator based on information given 442 | ''' 443 | raise NotImplementedError() 444 | def run(self, debug=True, blockThread=True): 445 | ''' start auto respond 446 | for option 447 | - debug: if set, debug info will be shown on screen 448 | it is defined in components/register.py 449 | ''' 450 | raise NotImplementedError() 451 | def search_friends(self, name=None, userName=None, remarkName=None, nickName=None, 452 | wechatAccount=None): 453 | return self.storageClass.search_friends(name, userName, remarkName, 454 | nickName, wechatAccount) 455 | def search_chatrooms(self, name=None, userName=None): 456 | return self.storageClass.search_chatrooms(name, userName) 457 | def search_mps(self, name=None, userName=None): 458 | return self.storageClass.search_mps(name, userName) 459 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | class LogSystem(object): 4 | handlerList = [] 5 | showOnCmd = True 6 | loggingLevel = logging.INFO 7 | loggingFile = None 8 | def __init__(self): 9 | self.logger = logging.getLogger('itchat') 10 | self.logger.addHandler(logging.NullHandler()) 11 | self.logger.setLevel(self.loggingLevel) 12 | self.cmdHandler = logging.StreamHandler() 13 | self.fileHandler = None 14 | self.logger.addHandler(self.cmdHandler) 15 | def set_logging(self, showOnCmd=True, loggingFile=None, 16 | loggingLevel=logging.INFO): 17 | if showOnCmd != self.showOnCmd: 18 | if showOnCmd: 19 | self.logger.addHandler(self.cmdHandler) 20 | else: 21 | self.logger.removeHandler(self.cmdHandler) 22 | self.showOnCmd = showOnCmd 23 | if loggingFile != self.loggingFile: 24 | if self.loggingFile is not None: # clear old fileHandler 25 | self.logger.removeHandler(self.fileHandler) 26 | self.fileHandler.close() 27 | if loggingFile is not None: # add new fileHandler 28 | self.fileHandler = logging.FileHandler(loggingFile) 29 | self.logger.addHandler(self.fileHandler) 30 | self.loggingFile = loggingFile 31 | if loggingLevel != self.loggingLevel: 32 | self.logger.setLevel(loggingLevel) 33 | self.loggingLevel = loggingLevel 34 | 35 | ls = LogSystem() 36 | set_logging = ls.set_logging 37 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/returnvalues.py: -------------------------------------------------------------------------------- 1 | #coding=utf8 2 | TRANSLATE = 'Chinese' 3 | 4 | class ReturnValue(dict): 5 | ''' turn return value of itchat into a boolean value 6 | for requests: 7 | ..code::python 8 | 9 | import requests 10 | r = requests.get('http://httpbin.org/get') 11 | print(ReturnValue(rawResponse=r) 12 | 13 | for normal dict: 14 | ..code::python 15 | 16 | returnDict = { 17 | 'BaseResponse': { 18 | 'Ret': 0, 19 | 'ErrMsg': 'My error msg', }, } 20 | print(ReturnValue(returnDict)) 21 | ''' 22 | def __init__(self, returnValueDict={}, rawResponse=None): 23 | if rawResponse: 24 | try: 25 | returnValueDict = rawResponse.json() 26 | except ValueError: 27 | returnValueDict = { 28 | 'BaseResponse': { 29 | 'Ret': -1004, 30 | 'ErrMsg': 'Unexpected return value', }, 31 | 'Data': rawResponse.content, } 32 | for k, v in returnValueDict.items(): 33 | self[k] = v 34 | if not 'BaseResponse' in self: 35 | self['BaseResponse'] = { 36 | 'ErrMsg': 'no BaseResponse in raw response', 37 | 'Ret': -1000, } 38 | if TRANSLATE: 39 | self['BaseResponse']['RawMsg'] = self['BaseResponse'].get('ErrMsg', '') 40 | self['BaseResponse']['ErrMsg'] = \ 41 | TRANSLATION[TRANSLATE].get( 42 | self['BaseResponse'].get('Ret', '')) \ 43 | or self['BaseResponse'].get('ErrMsg', u'No ErrMsg') 44 | self['BaseResponse']['RawMsg'] = \ 45 | self['BaseResponse']['RawMsg'] or self['BaseResponse']['ErrMsg'] 46 | def __nonzero__(self): 47 | return self['BaseResponse'].get('Ret') == 0 48 | def __bool__(self): 49 | return self.__nonzero__() 50 | def __str__(self): 51 | return '{%s}' % ', '.join( 52 | ['%s: %s' % (repr(k),repr(v)) for k,v in self.items()]) 53 | def __repr__(self): 54 | return '' % self.__str__() 55 | 56 | TRANSLATION = { 57 | 'Chinese': { 58 | -1000: u'返回值不带BaseResponse', 59 | -1001: u'无法找到对应的成员', 60 | -1002: u'文件位置错误', 61 | -1003: u'服务器拒绝连接', 62 | -1004: u'服务器返回异常值', 63 | -1005: u'参数错误', 64 | -1006: u'无效操作', 65 | 0: u'请求成功', 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/storage/__init__.py: -------------------------------------------------------------------------------- 1 | import os, time, copy 2 | from threading import Lock 3 | 4 | from .messagequeue import Queue 5 | from .templates import ( 6 | ContactList, AbstractUserDict, User, 7 | MassivePlatform, Chatroom, ChatroomMember) 8 | 9 | def contact_change(fn): 10 | def _contact_change(core, *args, **kwargs): 11 | with core.storageClass.updateLock: 12 | return fn(core, *args, **kwargs) 13 | return _contact_change 14 | 15 | class Storage(object): 16 | def __init__(self, core): 17 | self.userName = None 18 | self.nickName = None 19 | self.updateLock = Lock() 20 | self.memberList = ContactList() 21 | self.mpList = ContactList() 22 | self.chatroomList = ContactList() 23 | self.msgList = Queue(-1) 24 | self.lastInputUserName = None 25 | self.memberList.set_default_value(contactClass=User) 26 | self.memberList.core = core 27 | self.mpList.set_default_value(contactClass=MassivePlatform) 28 | self.mpList.core = core 29 | self.chatroomList.set_default_value(contactClass=Chatroom) 30 | self.chatroomList.core = core 31 | def dumps(self): 32 | return { 33 | 'userName' : self.userName, 34 | 'nickName' : self.nickName, 35 | 'memberList' : self.memberList, 36 | 'mpList' : self.mpList, 37 | 'chatroomList' : self.chatroomList, 38 | 'lastInputUserName' : self.lastInputUserName, } 39 | def loads(self, j): 40 | self.userName = j.get('userName', None) 41 | self.nickName = j.get('nickName', None) 42 | del self.memberList[:] 43 | for i in j.get('memberList', []): 44 | self.memberList.append(i) 45 | del self.mpList[:] 46 | for i in j.get('mpList', []): 47 | self.mpList.append(i) 48 | del self.chatroomList[:] 49 | for i in j.get('chatroomList', []): 50 | self.chatroomList.append(i) 51 | # I tried to solve everything in pickle 52 | # but this way is easier and more storage-saving 53 | for chatroom in self.chatroomList: 54 | if 'MemberList' in chatroom: 55 | for member in chatroom['MemberList']: 56 | member.core = chatroom.core 57 | member.chatroom = chatroom 58 | if 'Self' in chatroom: 59 | chatroom['Self'].core = chatroom.core 60 | chatroom['Self'].chatroom = chatroom 61 | self.lastInputUserName = j.get('lastInputUserName', None) 62 | def search_friends(self, name=None, userName=None, remarkName=None, nickName=None, 63 | wechatAccount=None): 64 | with self.updateLock: 65 | if (name or userName or remarkName or nickName or wechatAccount) is None: 66 | return copy.deepcopy(self.memberList[0]) # my own account 67 | elif userName: # return the only userName match 68 | for m in self.memberList: 69 | if m['UserName'] == userName: 70 | return copy.deepcopy(m) 71 | else: 72 | matchDict = { 73 | 'RemarkName' : remarkName, 74 | 'NickName' : nickName, 75 | 'Alias' : wechatAccount, } 76 | for k in ('RemarkName', 'NickName', 'Alias'): 77 | if matchDict[k] is None: 78 | del matchDict[k] 79 | if name: # select based on name 80 | contact = [] 81 | for m in self.memberList: 82 | if any([m.get(k) == name for k in ('RemarkName', 'NickName', 'Alias')]): 83 | contact.append(m) 84 | else: 85 | contact = self.memberList[:] 86 | if matchDict: # select again based on matchDict 87 | friendList = [] 88 | for m in contact: 89 | if all([m.get(k) == v for k, v in matchDict.items()]): 90 | friendList.append(m) 91 | return copy.deepcopy(friendList) 92 | else: 93 | return copy.deepcopy(contact) 94 | def search_chatrooms(self, name=None, userName=None): 95 | with self.updateLock: 96 | if userName is not None: 97 | for m in self.chatroomList: 98 | if m['UserName'] == userName: 99 | return copy.deepcopy(m) 100 | elif name is not None: 101 | matchList = [] 102 | for m in self.chatroomList: 103 | print(m['NickName']) 104 | if name in m['NickName']: 105 | matchList.append(copy.deepcopy(m)) 106 | return matchList 107 | def search_mps(self, name=None, userName=None): 108 | with self.updateLock: 109 | if userName is not None: 110 | for m in self.mpList: 111 | if m['UserName'] == userName: 112 | return copy.deepcopy(m) 113 | elif name is not None: 114 | matchList = [] 115 | for m in self.mpList: 116 | if name in m['NickName']: 117 | matchList.append(copy.deepcopy(m)) 118 | return matchList 119 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/storage/messagequeue.py: -------------------------------------------------------------------------------- 1 | import logging 2 | try: 3 | import Queue as queue 4 | except ImportError: 5 | import queue 6 | 7 | from .templates import AttributeDict 8 | 9 | logger = logging.getLogger('itchat') 10 | 11 | class Queue(queue.Queue): 12 | def put(self, message): 13 | queue.Queue.put(self, Message(message)) 14 | 15 | class Message(AttributeDict): 16 | def download(self, fileName): 17 | if hasattr(self.text, '__call__'): 18 | return self.text(fileName) 19 | else: 20 | return b'' 21 | def __getitem__(self, value): 22 | if value in ('isAdmin', 'isAt'): 23 | v = value[0].upper() + value[1:] # ''[1:] == '' 24 | logger.debug('%s is expired in 1.3.0, use %s instead.' % (value, v)) 25 | value = v 26 | return super(Message, self).__getitem__(value) 27 | def __str__(self): 28 | return '{%s}' % ', '.join( 29 | ['%s: %s' % (repr(k),repr(v)) for k,v in self.items()]) 30 | def __repr__(self): 31 | return '<%s: %s>' % (self.__class__.__name__.split('.')[-1], 32 | self.__str__()) 33 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/storage/templates.py: -------------------------------------------------------------------------------- 1 | import logging, copy, pickle 2 | from weakref import ref 3 | 4 | from ..returnvalues import ReturnValue 5 | from ..utils import update_info_dict 6 | 7 | logger = logging.getLogger('itchat') 8 | 9 | class AttributeDict(dict): 10 | def __getattr__(self, value): 11 | keyName = value[0].upper() + value[1:] 12 | try: 13 | return self[keyName] 14 | except KeyError: 15 | raise AttributeError("'%s' object has no attribute '%s'" % ( 16 | self.__class__.__name__.split('.')[-1], keyName)) 17 | def get(self, v, d=None): 18 | try: 19 | return self[v] 20 | except KeyError: 21 | return d 22 | 23 | class UnInitializedItchat(object): 24 | def _raise_error(self, *args, **kwargs): 25 | logger.warning('An itchat instance is called before initialized') 26 | def __getattr__(self, value): 27 | return self._raise_error 28 | 29 | class ContactList(list): 30 | ''' when a dict is append, init function will be called to format that dict ''' 31 | def __init__(self, *args, **kwargs): 32 | super(ContactList, self).__init__(*args, **kwargs) 33 | self.__setstate__(None) 34 | @property 35 | def core(self): 36 | return getattr(self, '_core', lambda: fakeItchat)() or fakeItchat 37 | @core.setter 38 | def core(self, value): 39 | self._core = ref(value) 40 | def set_default_value(self, initFunction=None, contactClass=None): 41 | if hasattr(initFunction, '__call__'): 42 | self.contactInitFn = initFunction 43 | if hasattr(contactClass, '__call__'): 44 | self.contactClass = contactClass 45 | def append(self, value): 46 | contact = self.contactClass(value) 47 | contact.core = self.core 48 | if self.contactInitFn is not None: 49 | contact = self.contactInitFn(self, contact) or contact 50 | super(ContactList, self).append(contact) 51 | def __deepcopy__(self, memo): 52 | r = self.__class__([copy.deepcopy(v) for v in self]) 53 | r.contactInitFn = self.contactInitFn 54 | r.contactClass = self.contactClass 55 | r.core = self.core 56 | return r 57 | def __getstate__(self): 58 | return 1 59 | def __setstate__(self, state): 60 | self.contactInitFn = None 61 | self.contactClass = User 62 | def __str__(self): 63 | return '[%s]' % ', '.join([repr(v) for v in self]) 64 | def __repr__(self): 65 | return '<%s: %s>' % (self.__class__.__name__.split('.')[-1], 66 | self.__str__()) 67 | 68 | class AbstractUserDict(AttributeDict): 69 | def __init__(self, *args, **kwargs): 70 | super(AbstractUserDict, self).__init__(*args, **kwargs) 71 | @property 72 | def core(self): 73 | return getattr(self, '_core', lambda: fakeItchat)() or fakeItchat 74 | @core.setter 75 | def core(self, value): 76 | self._core = ref(value) 77 | def update(self): 78 | return ReturnValue({'BaseResponse': { 79 | 'Ret': -1006, 80 | 'ErrMsg': '%s can not be updated' % \ 81 | self.__class__.__name__, }, }) 82 | def set_alias(self, alias): 83 | return ReturnValue({'BaseResponse': { 84 | 'Ret': -1006, 85 | 'ErrMsg': '%s can not set alias' % \ 86 | self.__class__.__name__, }, }) 87 | def set_pinned(self, isPinned=True): 88 | return ReturnValue({'BaseResponse': { 89 | 'Ret': -1006, 90 | 'ErrMsg': '%s can not be pinned' % \ 91 | self.__class__.__name__, }, }) 92 | def verify(self): 93 | return ReturnValue({'BaseResponse': { 94 | 'Ret': -1006, 95 | 'ErrMsg': '%s do not need verify' % \ 96 | self.__class__.__name__, }, }) 97 | def get_head_image(self, imageDir=None): 98 | return self.core.get_head_img(self.userName, picDir=imageDir) 99 | def delete_member(self, userName): 100 | return ReturnValue({'BaseResponse': { 101 | 'Ret': -1006, 102 | 'ErrMsg': '%s can not delete member' % \ 103 | self.__class__.__name__, }, }) 104 | def add_member(self, userName): 105 | return ReturnValue({'BaseResponse': { 106 | 'Ret': -1006, 107 | 'ErrMsg': '%s can not add member' % \ 108 | self.__class__.__name__, }, }) 109 | def send_raw_msg(self, msgType, content): 110 | return self.core.send_raw_msg(msgType, content, self.userName) 111 | def send_msg(self, msg='Test Message'): 112 | return self.core.send_msg(msg, self.userName) 113 | def send_file(self, fileDir, mediaId=None): 114 | return self.core.send_file(fileDir, self.userName, mediaId) 115 | def send_image(self, fileDir, mediaId=None): 116 | return self.core.send_image(fileDir, self.userName, mediaId) 117 | def send_video(self, fileDir=None, mediaId=None): 118 | return self.core.send_video(fileDir, self.userName, mediaId) 119 | def send(self, msg, mediaId=None): 120 | return self.core.send(msg, self.userName, mediaId) 121 | def search_member(self, name=None, userName=None, remarkName=None, nickName=None, 122 | wechatAccount=None): 123 | return ReturnValue({'BaseResponse': { 124 | 'Ret': -1006, 125 | 'ErrMsg': '%s do not have members' % \ 126 | self.__class__.__name__, }, }) 127 | def __deepcopy__(self, memo): 128 | r = self.__class__() 129 | for k, v in self.items(): 130 | r[copy.deepcopy(k)] = copy.deepcopy(v) 131 | r.core = self.core 132 | return r 133 | def __str__(self): 134 | return '{%s}' % ', '.join( 135 | ['%s: %s' % (repr(k),repr(v)) for k,v in self.items()]) 136 | def __repr__(self): 137 | return '<%s: %s>' % (self.__class__.__name__.split('.')[-1], 138 | self.__str__()) 139 | def __getstate__(self): 140 | return 1 141 | def __setstate__(self, state): 142 | pass 143 | 144 | class User(AbstractUserDict): 145 | def __init__(self, *args, **kwargs): 146 | super(User, self).__init__(*args, **kwargs) 147 | self.__setstate__(None) 148 | def update(self): 149 | r = self.core.update_friend(self.userName) 150 | if r: 151 | update_info_dict(self, r) 152 | return r 153 | def set_alias(self, alias): 154 | return self.core.set_alias(self.userName, alias) 155 | def set_pinned(self, isPinned=True): 156 | return self.core.set_pinned(self.userName, isPinned) 157 | def verify(self): 158 | return self.core.add_friend(**self.verifyDict) 159 | def __deepcopy__(self, memo): 160 | r = super(User, self).__deepcopy__(memo) 161 | r.verifyDict = copy.deepcopy(self.verifyDict) 162 | return r 163 | def __setstate__(self, state): 164 | super(User, self).__setstate__(state) 165 | self.verifyDict = {} 166 | self['MemberList'] = fakeContactList 167 | 168 | class MassivePlatform(AbstractUserDict): 169 | def __init__(self, *args, **kwargs): 170 | super(MassivePlatform, self).__init__(*args, **kwargs) 171 | self.__setstate__(None) 172 | def __setstate__(self, state): 173 | super(MassivePlatform, self).__setstate__(state) 174 | self['MemberList'] = fakeContactList 175 | 176 | class Chatroom(AbstractUserDict): 177 | def __init__(self, *args, **kwargs): 178 | super(Chatroom, self).__init__(*args, **kwargs) 179 | memberList = ContactList() 180 | userName = self.get('UserName', '') 181 | refSelf = ref(self) 182 | def init_fn(parentList, d): 183 | d.chatroom = refSelf() or \ 184 | parentList.core.search_chatrooms(userName=userName) 185 | memberList.set_default_value(init_fn, ChatroomMember) 186 | if 'MemberList' in self: 187 | for member in self.memberList: 188 | memberList.append(member) 189 | self['MemberList'] = memberList 190 | @property 191 | def core(self): 192 | return getattr(self, '_core', lambda: fakeItchat)() or fakeItchat 193 | @core.setter 194 | def core(self, value): 195 | self._core = ref(value) 196 | self.memberList.core = value 197 | for member in self.memberList: 198 | member.core = value 199 | def update(self, detailedMember=False): 200 | r = self.core.update_chatroom(self.userName, detailedMember) 201 | if r: 202 | update_info_dict(self, r) 203 | self['MemberList'] = r['MemberList'] 204 | return r 205 | def set_alias(self, alias): 206 | return self.core.set_chatroom_name(self.userName, alias) 207 | def set_pinned(self, isPinned=True): 208 | return self.core.set_pinned(self.userName, isPinned) 209 | def delete_member(self, userName): 210 | return self.core.delete_member_from_chatroom(self.userName, userName) 211 | def add_member(self, userName): 212 | return self.core.add_member_into_chatroom(self.userName, userName) 213 | def search_member(self, name=None, userName=None, remarkName=None, nickName=None, 214 | wechatAccount=None): 215 | with self.core.storageClass.updateLock: 216 | if (name or userName or remarkName or nickName or wechatAccount) is None: 217 | return None 218 | elif userName: # return the only userName match 219 | for m in self.memberList: 220 | if m.userName == userName: 221 | return copy.deepcopy(m) 222 | else: 223 | matchDict = { 224 | 'RemarkName' : remarkName, 225 | 'NickName' : nickName, 226 | 'Alias' : wechatAccount, } 227 | for k in ('RemarkName', 'NickName', 'Alias'): 228 | if matchDict[k] is None: 229 | del matchDict[k] 230 | if name: # select based on name 231 | contact = [] 232 | for m in self.memberList: 233 | if any([m.get(k) == name for k in ('RemarkName', 'NickName', 'Alias')]): 234 | contact.append(m) 235 | else: 236 | contact = self.memberList[:] 237 | if matchDict: # select again based on matchDict 238 | friendList = [] 239 | for m in contact: 240 | if all([m.get(k) == v for k, v in matchDict.items()]): 241 | friendList.append(m) 242 | return copy.deepcopy(friendList) 243 | else: 244 | return copy.deepcopy(contact) 245 | def __setstate__(self, state): 246 | super(Chatroom, self).__setstate__(state) 247 | if not 'MemberList' in self: 248 | self['MemberList'] = fakeContactList 249 | 250 | class ChatroomMember(AbstractUserDict): 251 | def __init__(self, *args, **kwargs): 252 | super(AbstractUserDict, self).__init__(*args, **kwargs) 253 | self.__setstate__(None) 254 | @property 255 | def chatroom(self): 256 | r = getattr(self, '_chatroom', lambda: fakeChatroom)() 257 | if r is None: 258 | userName = getattr(self, '_chatroomUserName', '') 259 | r = self.core.search_chatrooms(userName=userName) 260 | if isinstance(r, dict): 261 | self.chatroom = r 262 | return r or fakeChatroom 263 | @chatroom.setter 264 | def chatroom(self, value): 265 | if isinstance(value, dict) and 'UserName' in value: 266 | self._chatroom = ref(value) 267 | self._chatroomUserName = value['UserName'] 268 | def get_head_image(self, imageDir=None): 269 | return self.core.get_head_img(self.userName, self.chatroom.userName, picDir=imageDir) 270 | def delete_member(self, userName): 271 | return self.core.delete_member_from_chatroom(self.chatroom.userName, self.userName) 272 | def send_raw_msg(self, msgType, content): 273 | return ReturnValue({'BaseResponse': { 274 | 'Ret': -1006, 275 | 'ErrMsg': '%s can not send message directly' % \ 276 | self.__class__.__name__, }, }) 277 | def send_msg(self, msg='Test Message'): 278 | return ReturnValue({'BaseResponse': { 279 | 'Ret': -1006, 280 | 'ErrMsg': '%s can not send message directly' % \ 281 | self.__class__.__name__, }, }) 282 | def send_file(self, fileDir, mediaId=None): 283 | return ReturnValue({'BaseResponse': { 284 | 'Ret': -1006, 285 | 'ErrMsg': '%s can not send message directly' % \ 286 | self.__class__.__name__, }, }) 287 | def send_image(self, fileDir, mediaId=None): 288 | return ReturnValue({'BaseResponse': { 289 | 'Ret': -1006, 290 | 'ErrMsg': '%s can not send message directly' % \ 291 | self.__class__.__name__, }, }) 292 | def send_video(self, fileDir=None, mediaId=None): 293 | return ReturnValue({'BaseResponse': { 294 | 'Ret': -1006, 295 | 'ErrMsg': '%s can not send message directly' % \ 296 | self.__class__.__name__, }, }) 297 | def send(self, msg, mediaId=None): 298 | return ReturnValue({'BaseResponse': { 299 | 'Ret': -1006, 300 | 'ErrMsg': '%s can not send message directly' % \ 301 | self.__class__.__name__, }, }) 302 | def __setstate__(self, state): 303 | super(ChatroomMember, self).__setstate__(state) 304 | self['MemberList'] = fakeContactList 305 | 306 | def wrap_user_dict(d): 307 | userName = d.get('UserName') 308 | if '@@' in userName: 309 | r = Chatroom(d) 310 | elif d.get('VerifyFlag', 8) & 8 == 0: 311 | r = User(d) 312 | else: 313 | r = MassivePlatform(d) 314 | return r 315 | 316 | fakeItchat = UnInitializedItchat() 317 | fakeContactList = ContactList() 318 | fakeChatroom = Chatroom() 319 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/itchat/utils.py: -------------------------------------------------------------------------------- 1 | import re, os, sys, subprocess, copy, traceback, logging 2 | 3 | try: 4 | from HTMLParser import HTMLParser 5 | except ImportError: 6 | from html.parser import HTMLParser 7 | try: 8 | from urllib import quote as _quote 9 | quote = lambda n: _quote(n.encode('utf8', 'replace')) 10 | except ImportError: 11 | from urllib.parse import quote 12 | 13 | import requests 14 | 15 | from . import config 16 | 17 | logger = logging.getLogger('itchat') 18 | 19 | emojiRegex = re.compile(r'') 20 | htmlParser = HTMLParser() 21 | if not hasattr(htmlParser, 'unescape'): 22 | import html 23 | htmlParser.unescape = html.unescape 24 | # FIX Python 3.9 HTMLParser.unescape is removed. See https://docs.python.org/3.9/whatsnew/3.9.html 25 | try: 26 | b = u'\u2588' 27 | sys.stdout.write(b + '\r') 28 | sys.stdout.flush() 29 | except UnicodeEncodeError: 30 | BLOCK = 'MM' 31 | else: 32 | BLOCK = b 33 | friendInfoTemplate = {} 34 | for k in ('UserName', 'City', 'DisplayName', 'PYQuanPin', 'RemarkPYInitial', 'Province', 35 | 'KeyWord', 'RemarkName', 'PYInitial', 'EncryChatRoomId', 'Alias', 'Signature', 36 | 'NickName', 'RemarkPYQuanPin', 'HeadImgUrl'): 37 | friendInfoTemplate[k] = '' 38 | for k in ('UniFriend', 'Sex', 'AppAccountFlag', 'VerifyFlag', 'ChatRoomId', 'HideInputBarFlag', 39 | 'AttrStatus', 'SnsFlag', 'MemberCount', 'OwnerUin', 'ContactFlag', 'Uin', 40 | 'StarFriend', 'Statues'): 41 | friendInfoTemplate[k] = 0 42 | friendInfoTemplate['MemberList'] = [] 43 | 44 | def clear_screen(): 45 | os.system('cls' if config.OS == 'Windows' else 'clear') 46 | 47 | def emoji_formatter(d, k): 48 | ''' _emoji_deebugger is for bugs about emoji match caused by wechat backstage 49 | like :face with tears of joy: will be replaced with :cat face with tears of joy: 50 | ''' 51 | def _emoji_debugger(d, k): 52 | s = d[k].replace('') # fix missing bug 54 | def __fix_miss_match(m): 55 | return '' % ({ 56 | '1f63c': '1f601', '1f639': '1f602', '1f63a': '1f603', 57 | '1f4ab': '1f616', '1f64d': '1f614', '1f63b': '1f60d', 58 | '1f63d': '1f618', '1f64e': '1f621', '1f63f': '1f622', 59 | }.get(m.group(1), m.group(1))) 60 | return emojiRegex.sub(__fix_miss_match, s) 61 | def _emoji_formatter(m): 62 | s = m.group(1) 63 | if len(s) == 6: 64 | return ('\\U%s\\U%s'%(s[:2].rjust(8, '0'), s[2:].rjust(8, '0')) 65 | ).encode('utf8').decode('unicode-escape', 'replace') 66 | elif len(s) == 10: 67 | return ('\\U%s\\U%s'%(s[:5].rjust(8, '0'), s[5:].rjust(8, '0')) 68 | ).encode('utf8').decode('unicode-escape', 'replace') 69 | else: 70 | return ('\\U%s'%m.group(1).rjust(8, '0') 71 | ).encode('utf8').decode('unicode-escape', 'replace') 72 | d[k] = _emoji_debugger(d, k) 73 | d[k] = emojiRegex.sub(_emoji_formatter, d[k]) 74 | 75 | def msg_formatter(d, k): 76 | emoji_formatter(d, k) 77 | d[k] = d[k].replace('
', '\n') 78 | d[k] = htmlParser.unescape(d[k]) 79 | 80 | def check_file(fileDir): 81 | try: 82 | with open(fileDir): 83 | pass 84 | return True 85 | except: 86 | return False 87 | 88 | def print_qr(fileDir): 89 | if config.OS == 'Darwin': 90 | subprocess.call(['open', fileDir]) 91 | elif config.OS == 'Linux': 92 | subprocess.call(['xdg-open', fileDir]) 93 | else: 94 | os.startfile(fileDir) 95 | 96 | def print_cmd_qr(qrText, white=BLOCK, black=' ', enableCmdQR=True): 97 | blockCount = int(enableCmdQR) 98 | if abs(blockCount) == 0: 99 | blockCount = 1 100 | white *= abs(blockCount) 101 | if blockCount < 0: 102 | white, black = black, white 103 | sys.stdout.write(' '*50 + '\r') 104 | sys.stdout.flush() 105 | qr = qrText.replace('0', white).replace('1', black) 106 | sys.stdout.write(qr) 107 | sys.stdout.flush() 108 | 109 | def struct_friend_info(knownInfo): 110 | member = copy.deepcopy(friendInfoTemplate) 111 | for k, v in copy.deepcopy(knownInfo).items(): member[k] = v 112 | return member 113 | 114 | def search_dict_list(l, key, value): 115 | ''' Search a list of dict 116 | * return dict with specific value & key ''' 117 | for i in l: 118 | if i.get(key) == value: 119 | return i 120 | 121 | def print_line(msg, oneLine = False): 122 | if oneLine: 123 | sys.stdout.write(' '*40 + '\r') 124 | sys.stdout.flush() 125 | else: 126 | sys.stdout.write('\n') 127 | sys.stdout.write(msg.encode(sys.stdin.encoding or 'utf8', 'replace' 128 | ).decode(sys.stdin.encoding or 'utf8', 'replace')) 129 | sys.stdout.flush() 130 | 131 | def test_connect(retryTime=5): 132 | for i in range(retryTime): 133 | try: 134 | r = requests.get(config.BASE_URL) 135 | return True 136 | except: 137 | if i == retryTime - 1: 138 | logger.error(traceback.format_exc()) 139 | return False 140 | 141 | def contact_deep_copy(core, contact): 142 | with core.storageClass.updateLock: 143 | return copy.deepcopy(contact) 144 | 145 | def get_image_postfix(data): 146 | data = data[:20] 147 | if b'GIF' in data: 148 | return 'gif' 149 | elif b'PNG' in data: 150 | return 'png' 151 | elif b'JFIF' in data: 152 | return 'jpg' 153 | return '' 154 | 155 | def update_info_dict(oldInfoDict, newInfoDict): 156 | ''' only normal values will be updated here 157 | because newInfoDict is normal dict, so it's not necessary to consider templates 158 | ''' 159 | for k, v in newInfoDict.items(): 160 | if any((isinstance(v, t) for t in (tuple, list, dict))): 161 | pass # these values will be updated somewhere else 162 | elif oldInfoDict.get(k) is None or v not in (None, '', '0', 0): 163 | oldInfoDict[k] = v -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/user/contact.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Jingjing WU (吴京京) 5 | 6 | 2021-now @ Copyright Wechaty 7 | 8 | Licensed under the Apache License, Version 2.0 (the 'License'); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an 'AS IS' BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | """ 20 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/user/login.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Jingjing WU (吴京京) 5 | 6 | 2021-now @ Copyright Wechaty 7 | 8 | Licensed under the Apache License, Version 2.0 (the 'License'); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an 'AS IS' BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | """ 20 | from __future__ import annotations 21 | 22 | import asyncio 23 | from typing import Optional 24 | 25 | from pyee import AsyncIOEventEmitter 26 | from requests import Session 27 | from wechaty_puppet import EventScanPayload, ScanStatus 28 | from wechaty_puppet import get_logger 29 | from wechaty_puppet.exceptions import WechatyPuppetError 30 | 31 | from wechaty_puppet_itchat.config import LOGIN_TIMEOUT, USER_AGENT 32 | from ..browser import Browser 33 | 34 | try: 35 | from httplib import BadStatusLine 36 | except ImportError: 37 | from http.client import BadStatusLine 38 | 39 | logger = get_logger('Login') 40 | 41 | 42 | class Login: 43 | def __init__(self, event_emitter: AsyncIOEventEmitter, browser: Browser): 44 | self.event_emitter: AsyncIOEventEmitter = event_emitter 45 | self.browser: Browser = browser 46 | 47 | async def login(self): 48 | logger.info('start login ...') 49 | 50 | if self.browser.is_alive: 51 | logger.warning('wechaty has already logged in. please don"t login again ...') 52 | return 53 | 54 | while not self.browser.is_alive: 55 | uuid: Optional[str] = self.browser.get_qr_uuid() 56 | if not uuid: 57 | raise WechatyPuppetError(f'can"t get qrcode from server') 58 | self.event_emitter.emit( 59 | 'login', 60 | EventScanPayload( 61 | status=ScanStatus.Waiting, 62 | qrcode=uuid, 63 | data=None 64 | ) 65 | ) 66 | await asyncio.sleep(LOGIN_TIMEOUT) 67 | 68 | def logout(self): 69 | if self.browser.is_alive: 70 | url = '%s/webwxlogout' % self.browser.login_info['url'] 71 | params = { 72 | 'redirect': 1, 73 | 'type': 1, 74 | 'skey': self.browser.login_info['skey'], } 75 | headers = {'User-Agent': USER_AGENT} 76 | self.browser.session.get(url, params=params, headers=headers) 77 | self.browser.is_alive = False 78 | self.browser.session.cookies.clear() 79 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/user/message.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Jingjing WU (吴京京) 5 | 6 | 2021-now @ Copyright Wechaty 7 | 8 | Licensed under the Apache License, Version 2.0 (the 'License'); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an 'AS IS' BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | """ 20 | from __future__ import annotations 21 | 22 | from wechaty_puppet_itchat.browser import Browser 23 | 24 | 25 | class MessageHandler: 26 | def __init__(self, browser: Browser): 27 | self.browser = browser 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/utils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechaty/python-wechaty-puppet-itchat/5335ffa6bd9a8ed69686b57af43ae14c0a4934fe/src/wechaty_puppet_itchat/utils.py -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Do not edit this file. 3 | This file will be auto-generated before deploy. 4 | """ 5 | VERSION = '0.0.4' 6 | -------------------------------------------------------------------------------- /src/wechaty_puppet_itchat/version_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | version unit test, this file will be updated in deploy stage. 3 | """ 4 | # import pytest # type: ignore 5 | 6 | from .version import VERSION 7 | 8 | 9 | def test_version() -> None: 10 | """ 11 | Unit Test for version file 12 | """ 13 | 14 | assert VERSION == '0.0.4', 'version should be 0.0.4' 15 | -------------------------------------------------------------------------------- /tests/smoke_testing_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit Test 3 | """ 4 | # pylint: disable=W0621 5 | 6 | # from typing import ( 7 | # # Any, 8 | # Iterable, 9 | # ) 10 | 11 | import pytest # type: ignore 12 | 13 | # from agent import Agent 14 | 15 | 16 | def test_smoke_testing() -> None: 17 | """ wechaty """ 18 | assert pytest, 'should True' 19 | --------------------------------------------------------------------------------