Преглед на файлове

Initial commit

The mauigpapi library can connect to Instagram's MQTT and send/receive messages.
Logging in and other HTTP API things are not yet implemented.

The bridge itself doesn't exist yet.
Tulir Asokan преди 4 години
ревизия
3cc0c8b582
променени са 43 файла, в които са добавени 5541 реда и са изтрити 0 реда
  1. 8 0
      .dockerignore
  2. 21 0
      .editorconfig
  3. 18 0
      .gitignore
  4. 48 0
      .gitlab-ci.yml
  5. 46 0
      Dockerfile
  6. 661 0
      LICENSE
  7. 4 0
      MANIFEST.in
  8. 14 0
      README.md
  9. 43 0
      ROADMAP.md
  10. 21 0
      docker-run.sh
  11. 3 0
      mauigpapi/__init__.py
  12. 41 0
      mauigpapi/errors.py
  13. 1 0
      mauigpapi/http/__init__.py
  14. 5 0
      mauigpapi/http/api.py
  15. 102 0
      mauigpapi/http/base.py
  16. 50 0
      mauigpapi/http/direct_inbox_feed.py
  17. 4 0
      mauigpapi/mqtt/__init__.py
  18. 575 0
      mauigpapi/mqtt/conn.py
  19. 26 0
      mauigpapi/mqtt/events.py
  20. 49 0
      mauigpapi/mqtt/otclient.py
  21. 211 0
      mauigpapi/mqtt/subscription.py
  22. 5 0
      mauigpapi/mqtt/thrift/__init__.py
  23. 99 0
      mauigpapi/mqtt/thrift/autospec.py
  24. 112 0
      mauigpapi/mqtt/thrift/ig_objects.py
  25. 69 0
      mauigpapi/mqtt/thrift/read.py
  26. 36 0
      mauigpapi/mqtt/thrift/type.py
  27. 171 0
      mauigpapi/mqtt/thrift/write.py
  28. 555 0
      mauigpapi/mqtt/types.py
  29. 1 0
      mauigpapi/state/__init__.py
  30. 290 0
      mauigpapi/state/application.py
  31. 92 0
      mauigpapi/state/cookies.py
  32. 77 0
      mauigpapi/state/device.py
  33. 18 0
      mauigpapi/state/samples/builds.json
  34. 1795 0
      mauigpapi/state/samples/devices.json
  35. 26 0
      mauigpapi/state/samples/supported-capabilities.json
  36. 31 0
      mauigpapi/state/session.py
  37. 72 0
      mauigpapi/state/state.py
  38. 2 0
      mautrix_instagram/__init__.py
  39. 50 0
      mautrix_instagram/get_version.py
  40. 1 0
      mautrix_instagram/version.py
  41. 10 0
      optional-requirements.txt
  42. 8 0
      requirements.txt
  43. 70 0
      setup.py

+ 8 - 0
.dockerignore

@@ -0,0 +1,8 @@
+.editorconfig
+logs
+.venv
+start
+config.yaml
+registration.yaml
+*.db
+*.pickle

+ 21 - 0
.editorconfig

@@ -0,0 +1,21 @@
+root = true
+
+[*]
+indent_style = tab
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.py]
+max_line_length = 99
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{yaml,yml,py,md}]
+indent_style = space
+
+[{.gitlab-ci.yml,*.md}]
+indent_size = 2

+ 18 - 0
.gitignore

@@ -0,0 +1,18 @@
+/.idea/
+
+/.venv
+/env/
+pip-selfcheck.json
+*.pyc
+__pycache__
+/build
+/dist
+/*.egg-info
+/.eggs
+
+/config.yaml
+/registration.yaml
+*.log*
+*.db
+*.pickle
+*.bak

+ 48 - 0
.gitlab-ci.yml

@@ -0,0 +1,48 @@
+image: docker:stable
+
+stages:
+- build
+- manifest
+
+default:
+  before_script:
+  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+
+build amd64:
+  stage: build
+  tags:
+  - amd64
+  script:
+  - docker pull $CI_REGISTRY_IMAGE:latest || true
+  - docker build --pull --cache-from $CI_REGISTRY_IMAGE:latest --build-arg TARGETARCH=amd64 --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 .
+  - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64
+  - docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64
+  after_script:
+  - |
+    if [ "$CI_COMMIT_BRANCH" = "master" ] && [ ! -z "$NOVA_BRIDGE_TYPE" ]; then
+      apk add --update curl
+      rm -rf /var/cache/apk/*
+      curl "$NOVA_ADMIN_API_URL" -H "Content-Type: application/json" -d '{"password":"'"$NOVA_ADMIN_NIGHTLY_PASS"'","bridge":"'$NOVA_BRIDGE_TYPE'","image":"'$CI_REGISTRY_IMAGE':'$CI_COMMIT_SHA'-amd64"}'
+    fi
+
+build arm64:
+  stage: build
+  tags:
+  - arm64
+  script:
+  - docker pull $CI_REGISTRY_IMAGE:latest || true
+  - docker build --pull --cache-from $CI_REGISTRY_IMAGE:latest --build-arg TARGETARCH=arm64 --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 .
+  - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
+  - docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
+
+manifest:
+  stage: manifest
+  before_script:
+  - "mkdir -p $HOME/.docker && echo '{\"experimental\": \"enabled\"}' > $HOME/.docker/config.json"
+  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+  script:
+  - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64
+  - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
+  - if [ "$CI_COMMIT_BRANCH" = "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:latest; fi
+  - if [ "$CI_COMMIT_BRANCH" != "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME; fi
+  - docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64

+ 46 - 0
Dockerfile

@@ -0,0 +1,46 @@
+FROM alpine:3.12
+
+ARG TARGETARCH=amd64
+
+RUN echo $'\
+@edge http://dl-cdn.alpinelinux.org/alpine/edge/main\n\
+@edge http://dl-cdn.alpinelinux.org/alpine/edge/testing\n\
+@edge http://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories
+
+RUN apk add --no-cache \
+      python3 py3-pip py3-setuptools py3-wheel \
+      py3-virtualenv \
+      py3-pillow \
+      py3-aiohttp \
+      py3-magic \
+      py3-ruamel.yaml \
+      py3-commonmark@edge \
+      # Other dependencies
+      ca-certificates \
+      su-exec \
+      # encryption
+      olm-dev \
+      py3-cffi \
+	  py3-pycryptodome \
+      py3-unpaddedbase64 \
+      py3-future \
+      bash \
+      curl \
+      jq \
+      yq@edge
+
+COPY requirements.txt /opt/mautrix-instagram/requirements.txt
+COPY optional-requirements.txt /opt/mautrix-instagram/optional-requirements.txt
+WORKDIR /opt/mautrix-instagram
+RUN apk add --virtual .build-deps python3-dev libffi-dev build-base \
+ && pip3 install -r requirements.txt -r optional-requirements.txt \
+ && apk del .build-deps
+
+COPY . /opt/mautrix-instagram
+RUN apk add git && pip3 install .[all] && apk del git \
+  # This doesn't make the image smaller, but it's needed so that the `version` command works properly
+  && cp mautrix_instagram/example-config.yaml . && rm -rf mautrix_instagram
+
+VOLUME /data
+
+CMD ["/opt/mautrix-instagram/docker-run.sh"]

+ 661 - 0
LICENSE

@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<https://www.gnu.org/licenses/>.

+ 4 - 0
MANIFEST.in

@@ -0,0 +1,4 @@
+include README.md
+include LICENSE
+include requirements.txt
+include optional-requirements.txt

+ 14 - 0
README.md

@@ -0,0 +1,14 @@
+# mautrix-instagram
+![Languages](https://img.shields.io/github/languages/top/tulir/mautrix-instagram.svg)
+[![License](https://img.shields.io/github/license/tulir/mautrix-instagram.svg)](LICENSE)
+[![Release](https://img.shields.io/github/release/tulir/mautrix-instagram/all.svg)](https://github.com/tulir/mautrix-instagram/releases)
+[![GitLab CI](https://mau.dev/tulir/mautrix-instagram/badges/master/pipeline.svg)](https://mau.dev/tulir/mautrix-instagram/container_registry)
+
+A Matrix-Instagram puppeting bridge.
+
+### [Wiki](https://github.com/tulir/mautrix-instagram/wiki)
+
+### [Features & Roadmap](https://github.com/tulir/mautrix-instagram/blob/master/ROADMAP.md)
+
+## Discussion
+Matrix room: [`#instagram:maunium.net`](https://matrix.to/#/#instagram:maunium.net)

+ 43 - 0
ROADMAP.md

@@ -0,0 +1,43 @@
+# Features & roadmap
+
+* Matrix → Instagram
+  * [ ] Message content
+    * [ ] Text
+    * [ ] Media
+      * [ ] Images
+      * [ ] Videos
+      * [ ] Voice messages
+      * [ ] Locations
+      * [ ] †Files
+  * [ ] Message redactions
+  * [ ] Message reactions
+  * [ ] Presence
+  * [ ] Typing notifications
+  * [ ] Read receipts
+* Instagram → Matrix
+  * [ ] Message content
+    * [ ] Text
+    * [ ] Media
+      * [ ] Images
+      * [ ] Videos
+      * [ ] Voice messages
+      * [ ] Locations
+  * [ ] Message unsend
+  * [ ] Message reactions
+  * [ ] Message history
+  * [ ] Presence
+  * [ ] Typing notifications
+  * [ ] Read receipts
+  * [ ] User metadata
+    * [ ] Name
+    * [ ] Avatar
+* Misc
+  * [ ] Multi-user support
+  * [ ] Shared group chat portals
+  * [ ] Automatic portal creation
+    * [ ] At startup
+    * [ ] When receiving message
+  * [ ] Private chat creation by inviting Matrix puppet of Instagram user to new room
+  * [ ] Option to use own Matrix account for messages sent from other Instagram clients
+
+† Not supported on Instagram

+ 21 - 0
docker-run.sh

@@ -0,0 +1,21 @@
+#!/bin/sh
+cd /opt/mautrix-instagram
+
+if [ ! -f /data/config.yaml ]; then
+	cp example-config.yaml /data/config.yaml
+	echo "Didn't find a config file."
+	echo "Copied default config file to /data/config.yaml"
+	echo "Modify that config file to your liking."
+	echo "Start the container again after that to generate the registration file."
+	exit
+fi
+
+if [ ! -f /data/registration.yaml ]; then
+	python3 -m mautrix_instagram -g -c /data/config.yaml -r /data/registration.yaml
+	echo "Didn't find a registration file."
+	echo "Generated one for you."
+	echo "Copy that over to synapses app service directory."
+	exit
+fi
+
+exec python3 -m mautrix_instagram -c /data/config.yaml

+ 3 - 0
mauigpapi/__init__.py

@@ -0,0 +1,3 @@
+from .state import AndroidState
+from .mqtt import AndroidMQTT, SkywalkerSubscription, GraphQLSubscription
+from .http import AndroidAPI

+ 41 - 0
mauigpapi/errors.py

@@ -0,0 +1,41 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+class IGError(Exception):
+    pass
+
+
+class NotLoggedIn(IGError):
+    pass
+
+
+class NotConnected(IGError):
+    pass
+
+
+class IGUserIDNotFoundError(IGError):
+    def __init__(self, message: str = "Could not extract userid (pk)"):
+        super().__init__(message)
+
+
+class IGCookieNotFoundError(IGError):
+    def __init__(self, key: str) -> None:
+        super().__init__(f"Cookie '{key}' not found")
+
+
+class IGNoCheckpointError(IGError):
+    def __init__(self, message: str = "No checkpoint data available"):
+        super().__init__(message)

+ 1 - 0
mauigpapi/http/__init__.py

@@ -0,0 +1 @@
+from .api import AndroidAPI

+ 5 - 0
mauigpapi/http/api.py

@@ -0,0 +1,5 @@
+from .direct_inbox_feed import DirectInboxAPI
+
+
+class AndroidAPI(DirectInboxAPI):
+    pass

+ 102 - 0
mauigpapi/http/base.py

@@ -0,0 +1,102 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import Optional, Dict
+import random
+import time
+
+from aiohttp import ClientSession, ClientResponse
+from yarl import URL
+from mautrix.types import JSON
+
+from ..state import AndroidState
+
+
+class BaseAndroidAPI:
+    url = URL("https://i.instagram.com")
+    http: ClientSession
+    state: AndroidState
+
+    def __init__(self, state: AndroidState) -> None:
+        self.http = ClientSession(cookie_jar=state.cookies.jar)
+        self.state = state
+
+    @property
+    def headers(self) -> Dict[str, str]:
+        headers = {
+            "User-Agent": self.state.user_agent,
+            "X-Ads-Opt-Out": str(int(self.state.session.ads_opt_out)),
+            #"X-DEVICE--ID": self.state.device.uuid,
+            "X-CM-Bandwidth-KBPS": "-1.000",
+            "X-CM-Latency": "-1.000",
+            "X-IG-App-Locale": self.state.device.language,
+            "X-IG-Device-Locale": self.state.device.language,
+            "X-Pigeon-Session-Id": self.state.pigeon_session_id,
+            "X-Pigeon-Rawclienttime": str(round(time.time(), 3)),
+            "X-IG-Connection-Speed": f"{random.randint(1000, 3700)}kbps",
+            "X-IG-Bandwidth-Speed-KBPS": "-1.000",
+            "X-IG-Bandwidth-TotalBytes-B": "0",
+            "X-IG-Bandwidth-TotalTime-MS": "0",
+            "X-IG-EU-DC-ENABLED": (str(self.state.session.eu_dc_enabled).lower()
+                                   if self.state.session.eu_dc_enabled else None),
+            "X-IG-Extended-CDN-Thumbnail-Cache-Busting-Value":
+                str(self.state.session.thumbnail_cache_busting_value),
+            "X-Bloks-Version-Id": self.state.application.BLOKS_VERSION_ID,
+            "X-MID": self.state.cookies.get_value("mid"),
+            "X-IG-WWW-Claim": self.state.session.ig_www_claim or "0",
+            "X-Bloks-Is-Layout-RTL": str(self.state.device.is_layout_rtl).lower(),
+            "X-IG-Connection-Type": self.state.device.connection_type,
+            "X-Ig-Capabilities": self.state.application.CAPABILITIES,
+            "X-IG-App-Id": self.state.application.FACEBOOK_ANALYTICS_APPLICATION_ID,
+            "X-IG-Device-ID": self.state.device.uuid,
+            "X-IG-Android-ID": self.state.device.id,
+            "Accept-Language": self.state.device.language.replace("_", "-"),
+            "X-FB-HTTP-Engine": "Liger",
+            "Authorization": self.state.session.authorization,
+            "Accept-Encoding": "gzip",
+            "Connection": "close",
+        }
+        return {k: v for k, v in headers.items() if v is not None}
+
+    async def handle_response(self, resp: ClientResponse) -> JSON:
+        self._handle_response_headers(resp)
+        body = await resp.json()
+        if body["status"] == "ok":
+            return body
+        else:
+            await self._raise_response_error(resp)
+
+    async def _raise_response_error(self, resp: ClientResponse) -> None:
+        # TODO handle all errors
+        print("Error:", resp.status)
+        print(await resp.json())
+        raise Exception("oh noes")
+
+    def _handle_response_headers(self, resp: ClientResponse) -> None:
+        fields = {
+            "X-IG-Set-WWW-Claim": "ig_www_claim",
+            "IG-Set-Authorization": "authorization",
+            "IG-Set-Password-Encryption-Key-ID": "password_encryption_key_id",
+            "IG-Set-Password-Encryption-Pub-Key": "password_encryption_pubkey",
+            "IG-Set-IG-U-IG-Direct-Region-Hint": "region_hint"
+        }
+        for header, field in fields.items():
+            try:
+                value = resp.headers[header]
+            except KeyError:
+                pass
+            else:
+                if value:
+                    setattr(self.state.session, field, value)

+ 50 - 0
mauigpapi/http/direct_inbox_feed.py

@@ -0,0 +1,50 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import Optional, Any
+
+from attr import dataclass
+from mautrix.types import SerializableAttrs
+
+from .base import BaseAndroidAPI
+
+
+@dataclass
+class DirectInboxResponse(SerializableAttrs['DirectInboxFeedResponse']):
+    status: str
+    seq_id: int
+    snapshot_at_ms: int
+    pending_requests_total: int
+    # TODO
+    inbox: Any
+    most_recent_inviter: Any = None
+
+
+class DirectInboxAPI(BaseAndroidAPI):
+    async def direct_inbox(self, cursor: Optional[str] = None, seq_id: Optional[str] = None,
+                           thread_message_limit: int = 10, limit: int = 20) -> DirectInboxResponse:
+        query = {
+            "visual_message_return_type": "unseen",
+            "cursor": cursor,
+            "direction": "older" if cursor else None,
+            "seq_id": seq_id,
+            "thread_message_limit": thread_message_limit,
+            "persistentBadging": "true",
+            "limit": limit,
+        }
+        query = {k: v for k, v in query.items() if v is not None}
+        url = (self.url / "api/v1/direct_v2/inbox/").with_query(query)
+        resp = await self.http.get(url, headers=self.headers)
+        return DirectInboxResponse.deserialize(await self.handle_response(resp))

+ 4 - 0
mauigpapi/mqtt/__init__.py

@@ -0,0 +1,4 @@
+from .subscription import SkywalkerSubscription, GraphQLSubscription
+from .types import (RealtimeTopic, ThreadItemType, ThreadAction, ReactionStatus, TypingStatus,
+                    CommandResponse, CommandResponsePayload)
+from .conn import AndroidMQTT

+ 575 - 0
mauigpapi/mqtt/conn.py

@@ -0,0 +1,575 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import Union, Set, Optional, Any, Dict, Awaitable, Type, List, TypeVar, Callable
+from collections import defaultdict
+from socket import socket, error as SocketError
+from uuid import uuid4
+import logging
+import urllib.request
+import asyncio
+import zlib
+import time
+import json
+import re
+
+import paho.mqtt.client
+from paho.mqtt.client import MQTTMessage, WebsocketConnectionError
+from yarl import URL
+from mautrix.util.logging import TraceLogger
+
+from ..errors import NotLoggedIn, NotConnected
+from ..state import AndroidState
+from .thrift import RealtimeConfig, RealtimeClientInfo, ForegroundStateConfig, IncomingMessage
+from .otclient import MQTToTClient
+from .subscription import everclear_subscriptions
+from .types import (RealtimeTopic, CommandResponse, ThreadItemType, ThreadAction, ReactionStatus,
+                    TypingStatus, IrisPayload, PubsubPayload, AppPresenceEventPayload,
+                    RealtimeDirectEvent, RealtimeZeroProvisionPayload, ClientConfigUpdatePayload,
+                    LiveVideoCommentPayload, PubsubEvent, MessageSyncEvent, MessageSyncMessage)
+from .subscription import GraphQLQueryID
+from .events import Connect, Disconnect
+
+try:
+    import socks
+except ImportError:
+    socks = None
+
+T = TypeVar('T')
+
+ACTIVITY_INDICATOR_REGEX = re.compile(
+    r"/direct_v2/threads/([\w_]+)/activity_indicator_id/([\w_]+)")
+
+
+class AndroidMQTT:
+    _loop: asyncio.AbstractEventLoop
+    _client: MQTToTClient
+    log: TraceLogger
+    state: AndroidState
+    _graphql_subs: Set[str]
+    _skywalker_subs: Set[str]
+    _iris_seq_id: Optional[int]
+    _iris_snapshot_at_ms: Optional[int]
+    _publish_waiters: Dict[int, asyncio.Future]
+    _response_waiters: Dict[RealtimeTopic, asyncio.Future]
+    _response_waiter_locks: Dict[RealtimeTopic, asyncio.Lock]
+    _disconnect_error: Optional[Exception]
+    _event_handlers: Dict[Type[T], List[Callable[[T], Awaitable[None]]]]
+
+    # region Initialization
+
+    def __init__(self, state: AndroidState, loop: Optional[asyncio.AbstractEventLoop] = None,
+                 log: Optional[TraceLogger] = None) -> None:
+        self._graphql_subs = set()
+        self._skywalker_subs = set()
+        self._iris_seq_id = None
+        self._iris_snapshot_at_ms = None
+        self._publish_waiters = {}
+        self._response_waiters = {}
+        self._disconnect_error = None
+        self._response_waiter_locks = defaultdict(lambda: asyncio.Lock())
+        self._event_handlers = defaultdict(lambda: [])
+        self.log = log or logging.getLogger("mauigpapi")
+        self._loop = loop or asyncio.get_event_loop()
+        self.state = state
+        self._client = MQTToTClient(
+            client_id=self._form_client_id(),
+            clean_session=True,
+            protocol=paho.mqtt.client.MQTTv31,
+            transport="tcp",
+        )
+        try:
+            http_proxy = urllib.request.getproxies()["http"]
+        except KeyError:
+            http_proxy = None
+        if http_proxy and socks and URL:
+            proxy_url = URL(http_proxy)
+            proxy_type = {
+                "http": socks.HTTP,
+                "https": socks.HTTP,
+                "socks": socks.SOCKS5,
+                "socks5": socks.SOCKS5,
+                "socks4": socks.SOCKS4,
+            }[proxy_url.scheme]
+            self._client.proxy_set(proxy_type=proxy_type, proxy_addr=proxy_url.host,
+                                   proxy_port=proxy_url.port, proxy_username=proxy_url.user,
+                                   proxy_password=proxy_url.password)
+        self._client.enable_logger()
+        self._client.tls_set()
+        # mqtt.max_inflight_messages_set(20)  # The rest will get queued
+        # mqtt.max_queued_messages_set(0)  # Unlimited messages can be queued
+        # mqtt.message_retry_set(20)  # Retry sending for at least 20 seconds
+        # mqtt.reconnect_delay_set(min_delay=1, max_delay=120)
+        self._client.connect_async("edge-mqtt.facebook.com", 443, keepalive=60)
+        self._client.on_message = self._on_message_handler
+        self._client.on_publish = self._on_publish_handler
+        self._client.on_connect = self._on_connect_handler
+        self._client.on_disconnect = self._on_disconnect_handler
+        self._client.on_socket_open = self._on_socket_open
+        self._client.on_socket_close = self._on_socket_close
+        self._client.on_socket_register_write = self._on_socket_register_write
+        self._client.on_socket_unregister_write = self._on_socket_unregister_write
+
+    def _form_client_id(self) -> bytes:
+        subscribe_topics = [RealtimeTopic.PUBSUB, RealtimeTopic.SUB_IRIS_RESPONSE,
+                            RealtimeTopic.REALTIME_SUB, RealtimeTopic.REGION_HINT,
+                            RealtimeTopic.SEND_MESSAGE_RESPONSE, RealtimeTopic.MESSAGE_SYNC,
+                            RealtimeTopic.UNKNOWN_179, RealtimeTopic.UNKNOWN_PP]
+        subscribe_topic_ids = [int(topic.encoded) for topic in subscribe_topics]
+        password = f"sessionid={self.state.cookies['sessionid']}"
+        cfg = RealtimeConfig(
+            client_identifier=self.state.device.phone_id[:20],
+            client_info=RealtimeClientInfo(
+                user_id=int(self.state.user_id),
+                user_agent=self.state.user_agent,
+                client_capabilities=0b10110111,
+                endpoint_capabilities=0,
+                publish_format=1,
+                no_automatic_foreground=True,
+                make_user_available_in_foreground=False,
+                device_id=self.state.device.phone_id,
+                is_initially_foreground=True,
+                network_type=1,
+                network_subtype=0,
+                client_mqtt_session_id=int(time.time() * 1000) & 0xffffffff,
+                subscribe_topics=subscribe_topic_ids,
+                client_type="cookie_auth",
+                app_id=567067343352427,
+                region_preference=self.state.session.region_hint or "LLA",
+                device_secret="",
+                client_stack=3,
+            ),
+            password=password,
+            app_specific_info={
+                "app_version": self.state.application.APP_VERSION,
+                "X-IG-Capabilities": self.state.application.CAPABILITIES,
+                "everclear_subscriptions": json.dumps(everclear_subscriptions),
+                "User-Agent": self.state.user_agent,
+                "Accept-Language": self.state.device.language.replace("_", "-"),
+                "platform": "android",
+                "ig_mqtt_route": "django",
+                "pubsub_msg_type_blacklist": "direct, typing_type",
+                "auth_cache_enabled": "0",
+            },
+        )
+        return zlib.compress(cfg.to_thrift(), level=9)
+
+    # endregion
+
+    def _on_socket_open(self, client: MQTToTClient, _: Any, sock: socket) -> None:
+        self._loop.add_reader(sock, client.loop_read)
+
+    def _on_socket_close(self, client: MQTToTClient, _: Any, sock: socket) -> None:
+        self._loop.remove_reader(sock)
+
+    def _on_socket_register_write(self, client: MQTToTClient, _: Any, sock: socket) -> None:
+        self._loop.add_writer(sock, client.loop_write)
+
+    def _on_socket_unregister_write(self, client: MQTToTClient, _: Any, sock: socket) -> None:
+        self._loop.remove_writer(sock)
+
+    def _on_disconnect_handler(self, client: MQTToTClient, _: Any, rc: int) -> None:
+        print(f"_on_disconnect_handler({rc})")
+
+    def _on_connect_handler(self, client: MQTToTClient, _: Any, flags: Dict[str, Any], rc: int
+                            ) -> None:
+        print(f"_on_connect_handler({flags}, {rc})")
+        if rc != 0:
+            err = paho.mqtt.client.connack_string(rc)
+            self.log.error("MQTT Connection Error: %s (%d)", err, rc)
+            return
+
+        self._loop.create_task(self._post_connect())
+
+    async def _post_connect(self) -> None:
+        self.log.debug("Re-subscribing to things after connect")
+        if self._graphql_subs:
+            res = await self.graphql_subscribe(self._graphql_subs)
+            self.log.trace("GraphQL subscribe response: %s", res)
+        if self._skywalker_subs:
+            res = await self.skywalker_subscribe(self._skywalker_subs)
+            self.log.trace("Skywalker subscribe response: %s", res)
+        if self._iris_seq_id:
+            await self.iris_subscribe(self._iris_seq_id, self._iris_snapshot_at_ms)
+
+    def _on_publish_handler(self, client: MQTToTClient, _: Any, mid: int) -> None:
+        self.log.trace(f"Received publish confirmation for {mid}")
+        try:
+            waiter = self._publish_waiters[mid]
+        except KeyError:
+            return
+        waiter.set_result(None)
+
+    # region Incoming event parsing
+
+    @staticmethod
+    def _parse_direct_thread_path(path: str) -> dict:
+        blank, direct_v2, threads, thread_id, *rest = path.split("/")
+        assert blank == ""
+        assert direct_v2 == "direct_v2"
+        assert threads == "threads"
+        additional = {
+            "thread_id": thread_id
+        }
+        if rest:
+            if rest[0] == "admin_user_ids":
+                additional["admin_user_ids"] = rest[1]
+            elif rest[0] == "approval_required_for_new_members":
+                additional["approval_required_for_new_members"] = True
+            elif rest[0] == ["participants"]:
+                additional["participants"] = {rest[1]: rest[2]}
+            elif rest[0] == ["items"]:
+                additional["item_id"] = rest[1]
+                # TODO wtf is this?
+                #      it has something to do with reactions
+                if len(rest) > 4:
+                    additional[rest[2]] = {
+                        rest[3]: rest[4],
+                    }
+        return additional
+
+    def _on_message_sync(self, payload: bytes) -> None:
+        parsed = json.loads(payload.decode("utf-8"))
+        self.log.trace("Got message sync event: %s", parsed)
+        for sync_item in parsed:
+            parsed_item = IrisPayload.deserialize(sync_item)
+            for part in parsed_item.data:
+                raw_message = {
+                    "path": part.path,
+                    "op": part.op,
+                    **self._parse_direct_thread_path(part.path),
+                }
+                try:
+                    raw_message = {
+                        **raw_message,
+                        **json.loads(part.value),
+                    }
+                except json.JSONDecodeError:
+                    raw_message["value"] = part.value
+                message = MessageSyncMessage.deserialize(raw_message)
+                evt = MessageSyncEvent(iris=parsed_item, message=message)
+                self._loop.create_task(self._dispatch(evt))
+
+    def _on_pubsub(self, payload: bytes) -> None:
+        parsed_thrift = IncomingMessage.from_thrift(payload)
+        message = PubsubPayload.parse_json(parsed_thrift.payload)
+        self.log.trace(f"Got pubsub event with topic {parsed_thrift.topic}: {message}")
+        for data in message.data:
+            match = ACTIVITY_INDICATOR_REGEX.match(data.path)
+            if match:
+                evt = PubsubEvent(data=data, base=message, thread_id=match.group(1),
+                                  activity_indicator_id=match.group(2))
+                self._loop.create_task(self._dispatch(evt))
+            elif not data.double_publish:
+                self.log.debug("Pubsub: no activity indicator on data: %s", data)
+            else:
+                self.log.debug("Pubsub: double publish: %s", data.path)
+
+    @staticmethod
+    def _parse_realtime_sub_item(topic: str, raw: dict) -> Any:
+        if topic == GraphQLQueryID.appPresence:
+            return AppPresenceEventPayload.deserialize(raw).presence_event
+        elif topic == GraphQLQueryID.zeroProvision:
+            return RealtimeZeroProvisionPayload.deserialize(raw).zero_product_provisioning_event
+        elif topic == GraphQLQueryID.clientConfigUpdate:
+            return ClientConfigUpdatePayload.deserialize(raw).client_config_update_event
+        elif topic == GraphQLQueryID.liveRealtimeComments:
+            return LiveVideoCommentPayload.deserialize(raw).live_video_comment_event
+        elif topic == "direct":
+            return RealtimeDirectEvent.deserialize(raw)
+
+    def _on_realtime_sub(self, payload: bytes) -> None:
+        parsed_thrift = IncomingMessage.from_thrift(payload)
+        topic = parsed_thrift.topic
+        if topic not in ("direct", GraphQLQueryID.appPresence, GraphQLQueryID.zeroProvision,
+                         GraphQLQueryID.clientConfigUpdate, GraphQLQueryID.liveRealtimeComments):
+            self.log.debug(f"Got unknown realtime sub event {topic}: {parsed_thrift.payload}")
+        parsed_json = json.loads(parsed_thrift.payload)
+        event = parsed_json["event"]
+        for item in parsed_json["data"]:
+            evt = self._parse_realtime_sub_item(topic, item)
+            self.log.trace(f"Got realtime sub event with topic {topic}/{event}: {evt}")
+            self._loop.create_task(self._dispatch(evt))
+
+    def _on_message_handler(self, client: MQTToTClient, _: Any, message: MQTTMessage) -> None:
+        try:
+            topic = RealtimeTopic.decode(message.topic)
+            # Instagram Android MQTT messages are always compressed
+            message.payload = zlib.decompress(message.payload)
+            if topic == RealtimeTopic.MESSAGE_SYNC:
+                self._on_message_sync(message.payload)
+            elif topic == RealtimeTopic.PUBSUB:
+                self._on_pubsub(message.payload)
+            elif topic == RealtimeTopic.REALTIME_SUB:
+                self._on_realtime_sub(message.payload)
+            else:
+                print("other message", message.payload)
+                try:
+                    waiter = self._response_waiters.pop(topic)
+                except KeyError:
+                    self.log.debug("No handler for MQTT message in %s: %s",
+                                   topic.value, message.payload)
+                else:
+                    waiter.set_result(message)
+        except Exception:
+            self.log.exception("Error in incoming MQTT message handler")
+            print(message.payload)
+
+    # endregion
+
+    async def _reconnect(self) -> None:
+        try:
+            print("Reconnecting")
+            self._client.reconnect()
+        except (SocketError, OSError, WebsocketConnectionError) as e:
+            # TODO custom class
+            raise NotLoggedIn("MQTT reconnection failed") from e
+
+    def add_event_handler(self, evt_type: Type[T], handler: Callable[[T], Awaitable[None]]
+                          ) -> None:
+        self._event_handlers[evt_type].append(handler)
+
+    async def _dispatch(self, evt: T) -> None:
+        for handler in self._event_handlers[type(evt)]:
+            try:
+                await handler(evt)
+            except Exception:
+                self.log.exception(f"Error in {type(evt)} handler")
+
+    def disconnect(self) -> None:
+        self._client.disconnect()
+
+    async def listen(self, graphql_subs: Set[str] = None, skywalker_subs: Set[str] = None,
+                     seq_id: int = None, snapshot_at_ms: int = None) -> None:
+        self._graphql_subs = graphql_subs or set()
+        self._skywalker_subs = skywalker_subs or set()
+        self._iris_seq_id = seq_id
+        self._iris_snapshot_at_ms = snapshot_at_ms
+
+        await self._reconnect()
+        await self._dispatch(Connect())
+        exit_if_not_connected = False
+
+        while True:
+            try:
+                await asyncio.sleep(1)
+            except asyncio.CancelledError:
+                self.disconnect()
+                # this might not be necessary
+                self._client.loop_misc()
+                break
+            rc = self._client.loop_misc()
+
+            # If disconnect() has been called
+            # Beware, internal API, may have to change this to something more stable!
+            if self._client._state == paho.mqtt.client.mqtt_cs_disconnecting:
+                break  # Stop listening
+
+            if rc != paho.mqtt.client.MQTT_ERR_SUCCESS:
+                # If known/expected error
+                if rc == paho.mqtt.client.MQTT_ERR_CONN_LOST:
+                    await self._dispatch(Disconnect(reason="Connection lost, retrying"))
+                elif rc == paho.mqtt.client.MQTT_ERR_NOMEM:
+                    # This error is wrongly classified
+                    # See https://github.com/eclipse/paho.mqtt.python/issues/340
+                    await self._dispatch(Disconnect(reason="Connection lost, retrying"))
+                elif rc == paho.mqtt.client.MQTT_ERR_CONN_REFUSED:
+                    raise NotLoggedIn("MQTT connection refused")
+                elif rc == paho.mqtt.client.MQTT_ERR_NO_CONN:
+                    if exit_if_not_connected:
+                        raise NotConnected("MQTT error: no connection")
+                    await self._dispatch(Disconnect(reason="MQTT Error: no connection, retrying"))
+                else:
+                    err = paho.mqtt.client.error_string(rc)
+                    self.log.error("MQTT Error: %s", err)
+                    await self._dispatch(Disconnect(reason=f"MQTT Error: {err}, retrying"))
+
+                await self._reconnect()
+                exit_if_not_connected = True
+                await self._dispatch(Connect())
+            else:
+                exit_if_not_connected = False
+        if self._disconnect_error:
+            self.log.info("disconnect_error is set, raising and clearing variable")
+            err = self._disconnect_error
+            self._disconnect_error = None
+            raise err
+
+    # region Basic outgoing MQTT
+
+    def publish(self, topic: RealtimeTopic, payload: Union[str, bytes, dict]
+                ) -> asyncio.Future:
+        if isinstance(payload, dict):
+            payload = json.dumps(payload)
+        if isinstance(payload, str):
+            payload = payload.encode("utf-8")
+        payload = zlib.compress(payload, level=9)
+        info = self._client.publish(topic.encoded, payload, qos=1)
+        fut = asyncio.Future()
+        self._publish_waiters[info.mid] = fut
+        return fut
+
+    async def request(self, topic: RealtimeTopic, response: RealtimeTopic,
+                      payload: Union[str, bytes, dict]) -> MQTTMessage:
+        async with self._response_waiter_locks[response]:
+            fut = asyncio.Future()
+            self._response_waiters[response] = fut
+            await self.publish(topic, payload)
+            return await fut
+
+    async def iris_subscribe(self, seq_id: int, snapshot_at_ms: int) -> None:
+        resp = await self.request(RealtimeTopic.SUB_IRIS, RealtimeTopic.SUB_IRIS_RESPONSE,
+                                  {"seq_id": seq_id, "snapshot_at_ms": snapshot_at_ms,
+                                   "snapshot_app_version": "message"})
+        self.log.debug("Iris subscribe response: %s", resp.payload.decode("utf-8"))
+
+    def graphql_subscribe(self, subs: Set[str]) -> asyncio.Future:
+        self._graphql_subs |= subs
+        return self.publish(RealtimeTopic.REALTIME_SUB, {"sub": list(subs)})
+
+    def graphql_unsubscribe(self, subs: Set[str]) -> asyncio.Future:
+        self._graphql_subs -= subs
+        return self.publish(RealtimeTopic.REALTIME_SUB, {"unsub": list(subs)})
+
+    def skywalker_subscribe(self, subs: Set[str]) -> asyncio.Future:
+        self._skywalker_subs |= subs
+        return self.publish(RealtimeTopic.PUBSUB, {"sub": list(subs)})
+
+    def skywalker_unsubscribe(self, subs: Set[str]) -> asyncio.Future:
+        self._skywalker_subs -= subs
+        return self.publish(RealtimeTopic.PUBSUB, {"unsub": list(subs)})
+
+    # endregion
+    # region Actually sending messages and stuff
+
+    async def send_foreground_state(self, state: ForegroundStateConfig) -> None:
+        self.log.debug("Updating foreground state: %s", state)
+        await self.publish(RealtimeTopic.FOREGROUND_STATE,
+                           zlib.compress(state.to_thrift(), level=9))
+        if state.keep_alive_timeout:
+            self._client._keepalive = state.keep_alive_timeout
+
+    async def send_command(self, thread_id: str, action: ThreadAction,
+                           client_context: Optional[str] = None,
+                           offline_threading_id: Optional[str] = None, **kwargs: Any
+                           ) -> CommandResponse:
+        client_context = client_context or str(uuid4())
+        req = {
+            "thread_id": thread_id,
+            "client_context": client_context,
+            "offline_threading_id": offline_threading_id or client_context,
+            "action": action.value,
+            # "device_id": self.state.cookies["ig_did"],
+            **kwargs,
+        }
+        resp = await self.request(RealtimeTopic.SEND_MESSAGE, RealtimeTopic.SEND_MESSAGE_RESPONSE,
+                                  payload=req)
+        return CommandResponse.parse_json(resp.payload.decode("utf-8"))
+
+    def send_item(self, thread_id: str, item_type: ThreadItemType, shh_mode: bool = False,
+                  client_context: Optional[str] = None, offline_threading_id: Optional[str] = None,
+                  **kwargs: Any) -> Awaitable[CommandResponse]:
+        return self.send_command(thread_id, item_type=item_type.value,
+                                 is_shh_mode=str(int(shh_mode)), action=ThreadAction.SEND_ITEM,
+                                 client_context=client_context,
+                                 offline_threading_id=offline_threading_id, **kwargs)
+
+    def send_hashtag(self, thread_id: str, hashtag: str, text: str = "", shh_mode: bool = False,
+                     client_context: Optional[str] = None,
+                     offline_threading_id: Optional[str] = None) -> Awaitable[CommandResponse]:
+        return self.send_item(thread_id, text=text, item_id=hashtag, shh_mode=shh_mode,
+                              item_type=ThreadItemType.HASHTAG, client_context=client_context,
+                              offline_threading_id=offline_threading_id)
+
+    def send_like(self, thread_id: str, shh_mode: bool = False,
+                  client_context: Optional[str] = None, offline_threading_id: Optional[str] = None,
+                  ) -> Awaitable[CommandResponse]:
+        return self.send_item(thread_id, shh_mode=shh_mode, item_type=ThreadItemType.LIKE,
+                              client_context=client_context,
+                              offline_threading_id=offline_threading_id)
+
+    def send_location(self, thread_id: str, venue_id: str, text: str = "",
+                      shh_mode: bool = False, client_context: Optional[str] = None,
+                      offline_threading_id: Optional[str] = None,
+                      ) -> Awaitable[CommandResponse]:
+        return self.send_item(thread_id, text=text, item_id=venue_id, shh_mode=shh_mode,
+                              item_type=ThreadItemType.LOCATION, client_context=client_context,
+                              offline_threading_id=offline_threading_id)
+
+    def send_media(self, thread_id: str, media_id: str, text: str = "", shh_mode: bool = False,
+                   client_context: Optional[str] = None,
+                   offline_threading_id: Optional[str] = None) -> Awaitable[CommandResponse]:
+        return self.send_item(thread_id, text=text, item_id=media_id, shh_mode=shh_mode,
+                              item_type=ThreadItemType.MEDIA_SHARE, client_context=client_context,
+                              offline_threading_id=offline_threading_id)
+
+    def send_profile(self, thread_id: str, user_id: str, text: str = "", shh_mode: bool = False,
+                     client_context: Optional[str] = None,
+                     offline_threading_id: Optional[str] = None) -> Awaitable[CommandResponse]:
+        return self.send_item(thread_id, text=text, item_id=user_id, shh_mode=shh_mode,
+                              item_type=ThreadItemType.PROFILE, client_context=client_context,
+                              offline_threading_id=offline_threading_id)
+
+    def send_reaction(self, thread_id: str, emoji: str, item_id: str,
+                      reaction_status: ReactionStatus = ReactionStatus.CREATED,
+                      target_item_type: ThreadItemType = ThreadItemType.TEXT,
+                      shh_mode: bool = False, client_context: Optional[str] = None,
+                      offline_threading_id: Optional[str] = None) -> Awaitable[CommandResponse]:
+        return self.send_item(thread_id, reaction_status=reaction_status.value, node_type="item",
+                              reaction_type="like", target_item_type=target_item_type.value,
+                              emoji=emoji, item_id=item_id, reaction_action_source="double_tap",
+                              shh_mode=shh_mode, item_type=ThreadItemType.REACTION,
+                              client_context=client_context,
+                              offline_threading_id=offline_threading_id)
+
+    def send_user_story(self, thread_id: str, media_id: str, text: str = "",
+                        shh_mode: bool = False, client_context: Optional[str] = None,
+                        offline_threading_id: Optional[str] = None) -> Awaitable[CommandResponse]:
+        return self.send_item(thread_id, text=text, item_id=media_id, shh_mode=shh_mode,
+                              item_type=ThreadItemType.REEL_SHARE, client_context=client_context,
+                              offline_threading_id=offline_threading_id)
+
+    def send_text(self, thread_id: str, text: str = "", shh_mode: bool = False,
+                  client_context: Optional[str] = None, offline_threading_id: Optional[str] = None
+                  ) -> Awaitable[CommandResponse]:
+        return self.send_item(thread_id, text=text, shh_mode=shh_mode,
+                              item_type=ThreadItemType.TEXT, client_context=client_context,
+                              offline_threading_id=offline_threading_id)
+
+    def mark_seen(self, thread_id: str, item_id: str, client_context: Optional[str] = None,
+                  offline_threading_id: Optional[str] = None) -> Awaitable[CommandResponse]:
+        return self.send_command(thread_id, item_id=item_id, action=ThreadAction.MARK_SEEN,
+                                 client_context=client_context,
+                                 offline_threading_id=offline_threading_id)
+
+    def mark_visual_item_seen(self, thread_id: str, item_id: str,
+                              client_context: Optional[str] = None,
+                              offline_threading_id: Optional[str] = None
+                              ) -> Awaitable[CommandResponse]:
+        return self.send_command(thread_id, item_id=item_id,
+                                 action=ThreadAction.MARK_VISUAL_ITEM_SEEN,
+                                 client_context=client_context,
+                                 offline_threading_id=offline_threading_id)
+
+    def indicate_activity(self, thread_id: str, activity_status: TypingStatus = TypingStatus.TEXT,
+                          client_context: Optional[str] = None,
+                          offline_threading_id: Optional[str] = None
+                          ) -> Awaitable[CommandResponse]:
+        return self.send_command(thread_id, activity_status=activity_status.value,
+                                 action=ThreadAction.INDICATE_ACTIVITY,
+                                 client_context=client_context,
+                                 offline_threading_id=offline_threading_id)
+
+    # endregion

+ 26 - 0
mauigpapi/mqtt/events.py

@@ -0,0 +1,26 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from attr import dataclass
+
+
+@dataclass
+class Connect:
+    pass
+
+
+@dataclass
+class Disconnect:
+    reason: str

+ 49 - 0
mauigpapi/mqtt/otclient.py

@@ -0,0 +1,49 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+import paho.mqtt.client
+import struct
+
+
+class MQTToTClient(paho.mqtt.client.Client):
+    # This is equivalent to the original _send_connect, except:
+    # * the protocol ID is MQTToT.
+    # * the client ID is sent without a length.
+    # * all extra stuff like wills, usernames, passwords and MQTTv5 is removed.
+    def _send_connect(self, keepalive):
+        proto_ver = self._protocol
+        protocol = b"MQTToT"
+
+        remaining_length = (2 + len(protocol) + 1 +
+                            1 + 2 + len(self._client_id))
+
+        # Username, password, clean session
+        connect_flags = 0x80 + 0x40 + 0x02
+
+        command = paho.mqtt.client.CONNECT
+        packet = bytearray()
+        packet.append(command)
+
+        self._pack_remaining_length(packet, remaining_length)
+        packet.extend(struct.pack(f"!H{len(protocol)}sBBH",
+                                  len(protocol), protocol, proto_ver, connect_flags, keepalive))
+        packet.extend(self._client_id)
+
+        self._keepalive = keepalive
+        self._easy_log(
+            paho.mqtt.client.MQTT_LOG_DEBUG,
+            "Sending CONNECT",
+        )
+        return self._packet_queue(command, packet, 0, 0)

+ 211 - 0
mauigpapi/mqtt/subscription.py

@@ -0,0 +1,211 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import Any, Optional, Union
+from enum import Enum
+from uuid import uuid4
+import json
+
+
+class SkywalkerSubscription:
+    @staticmethod
+    def direct_sub(user_id: Union[str, int]) -> str:
+        return f"ig/u/v1/{user_id}"
+
+    @staticmethod
+    def live_sub(user_id: Union[str, int]) -> str:
+        return f"ig/live_notification_subscribe/{user_id}"
+
+
+class GraphQLQueryID(Enum):
+    appPresence = '17846944882223835'
+    asyncAdSub = '17911191835112000'
+    clientConfigUpdate = '17849856529644700'
+    directStatus = '17854499065530643'
+    directTyping = '17867973967082385'
+    liveWave = '17882305414154951'
+    interactivityActivateQuestion = '18005526940184517'
+    interactivityRealtimeQuestionSubmissionsStatus = '18027779584026952'
+    interactivitySub = '17907616480241689'
+    liveRealtimeComments = '17855344750227125'
+    liveTypingIndicator = '17926314067024917'
+    mediaFeedback = '17877917527113814'
+    reactNativeOTA = '17861494672288167'
+    videoCallCoWatchControl = '17878679623388956'
+    videoCallInAlert = '17878679623388956'
+    videoCallPrototypePublish = '18031704190010162'
+    videoCallParticipantDelivery = '17977239895057311'
+    zeroProvision = '17913953740109069'
+    inappNotification = '17899377895239777'
+    businessDelivery = '17940467278199720'
+
+
+everclear_subscriptions = {
+    "async_ads_subscribe": GraphQLQueryID.asyncAdSub.value,
+    "inapp_notification_subscribe_default": GraphQLQueryID.inappNotification.value,
+    "inapp_notification_subscribe_comment": GraphQLQueryID.inappNotification.value,
+    "inapp_notification_subscribe_comment_mention_and_reply": GraphQLQueryID.inappNotification.value,
+    "business_import_page_media_delivery_subscribe": GraphQLQueryID.businessDelivery.value,
+    "video_call_participant_state_delivery": GraphQLQueryID.videoCallParticipantDelivery.value,
+}
+
+
+class GraphQLSubscription:
+    @staticmethod
+    def _fmt(query_id: GraphQLQueryID, input_params: Any,
+             client_logged: Optional[bool] = None) -> str:
+        params = {
+            "input_data": input_params,
+            **({"%options": {"client_logged": client_logged}}
+               if client_logged is not None else {}),
+        }
+        return f"1/graphqlsubscriptions/{query_id.value}/{json.dumps(params)}"
+
+    @classmethod
+    def app_presence(cls, subscription_id: Optional[str] = None,
+                     client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.appPresence,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4())},
+                        client_logged=client_logged)
+
+    @classmethod
+    def async_ad(cls, user_id: str, subscription_id: Optional[str] = None,
+                 client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.asyncAdSub,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "user_id": user_id},
+                        client_logged=client_logged)
+
+    @classmethod
+    def client_config_update(cls, subscription_id: Optional[str] = None,
+                             client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.clientConfigUpdate,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4())},
+                        client_logged=client_logged)
+
+    @classmethod
+    def direct_status(cls, subscription_id: Optional[str] = None,
+                      client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.directStatus,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4())},
+                        client_logged=client_logged)
+
+    @classmethod
+    def direct_typing(cls, user_id: str, client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.directTyping,
+                        input_params={"user_id": user_id},
+                        client_logged=client_logged)
+
+    @classmethod
+    def ig_live_wave(cls, broadcast_id: str, receiver_id: str,
+                     subscription_id: Optional[str] = None, client_logged: Optional[bool] = None
+                     ) -> str:
+        return cls._fmt(GraphQLQueryID.liveWave,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "broadcast_id": broadcast_id, "receiver_id": receiver_id},
+                        client_logged=client_logged)
+
+    @classmethod
+    def interactivity_activate_question(cls, broadcast_id: str,
+                                        subscription_id: Optional[str] = None,
+                                        client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.interactivityActivateQuestion,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "broadcast_id": broadcast_id},
+                        client_logged=client_logged)
+
+    @classmethod
+    def interactivity_realtime_question_submissions_status(
+        cls, broadcast_id: str, subscription_id: Optional[str] = None,
+        client_logged: Optional[bool] = None
+    ) -> str:
+        return cls._fmt(GraphQLQueryID.interactivityRealtimeQuestionSubmissionsStatus,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "broadcast_id": broadcast_id},
+                        client_logged=client_logged)
+
+    @classmethod
+    def interactivity(cls, broadcast_id: str, subscription_id: Optional[str] = None,
+                      client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.interactivitySub,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "broadcast_id": broadcast_id},
+                        client_logged=client_logged)
+
+    @classmethod
+    def live_realtime_comments(cls, broadcast_id: str, subscription_id: Optional[str] = None,
+                               client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.liveRealtimeComments,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "broadcast_id": broadcast_id},
+                        client_logged=client_logged)
+
+    @classmethod
+    def live_realtime_typing_indicator(cls, broadcast_id: str,
+                                       subscription_id: Optional[str] = None,
+                                       client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.liveTypingIndicator,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "broadcast_id": broadcast_id},
+                        client_logged=client_logged)
+
+    @classmethod
+    def media_feedback(cls, feedback_id: str, subscription_id: Optional[str] = None,
+                       client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.mediaFeedback,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "feedback_id": feedback_id},
+                        client_logged=client_logged)
+
+    @classmethod
+    def react_native_ota_update(cls, build_number: str, subscription_id: Optional[str] = None,
+                                client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.reactNativeOTA,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "build_number": build_number},
+                        client_logged=client_logged)
+
+    @classmethod
+    def video_call_co_watch_control(cls, video_call_id: str, subscription_id: Optional[str] = None,
+                                    client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.videoCallCoWatchControl,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "video_call_id": video_call_id},
+                        client_logged=client_logged)
+
+    @classmethod
+    def video_call_in_call_alert(cls, video_call_id: str, subscription_id: Optional[str] = None,
+                                 client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.videoCallInAlert,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "video_call_id": video_call_id},
+                        client_logged=client_logged)
+
+    @classmethod
+    def video_call_prototype_publish(cls, video_call_id: str,
+                                     subscription_id: Optional[str] = None,
+                                     client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.videoCallPrototypePublish,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "video_call_id": video_call_id},
+                        client_logged=client_logged)
+
+    @classmethod
+    def zero_provision(cls, device_id: str, subscription_id: Optional[str] = None,
+                       client_logged: Optional[bool] = None) -> str:
+        return cls._fmt(GraphQLQueryID.zeroProvision,
+                        input_params={"client_subscription_id": subscription_id or str(uuid4()),
+                                      "device_id": device_id},
+                        client_logged=client_logged)

+ 5 - 0
mauigpapi/mqtt/thrift/__init__.py

@@ -0,0 +1,5 @@
+from .read import ThriftReader
+from .write import ThriftWriter
+from .type import TType
+from .autospec import autospec, field
+from .ig_objects import RealtimeConfig, RealtimeClientInfo, ForegroundStateConfig, IncomingMessage

+ 99 - 0
mauigpapi/mqtt/thrift/autospec.py

@@ -0,0 +1,99 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import Tuple, Union
+import sys
+
+import attr
+
+from .type import TType
+
+TYPE_META = "net.maunium.instagram.thrift.type"
+
+if sys.version_info >= (3, 7):
+    def _get_type_class(typ):
+        try:
+            return typ.__origin__
+        except AttributeError:
+            return None
+else:
+    def _get_type_class(typ):
+        try:
+            return typ.__extra__
+        except AttributeError:
+            return None
+
+
+Subtype = Union[None, TType, Tuple['Subtype', 'Subtype']]
+
+
+def _guess_type(python_type, name: str) -> Tuple[TType, Subtype]:
+    if python_type == str or python_type == bytes:
+        return TType.BINARY, None
+    elif python_type == bool:
+        return TType.BOOL, None
+    elif python_type == int:
+        raise ValueError(f"Ambiguous integer field {name}")
+    elif python_type == float:
+        return TType.DOUBLE, None
+    elif attr.has(python_type):
+        return TType.STRUCT, None
+
+    type_class = _get_type_class(python_type)
+    args = getattr(python_type, "__args__", None)
+    if type_class == list:
+        return TType.LIST, _guess_type(args[0], f"{name} item")
+    elif type_class == dict:
+        return TType.MAP, (_guess_type(args[0], f"{name} key"),
+                           _guess_type(args[1], f"{name} value"))
+    elif type_class == set:
+        return TType.SET, _guess_type(args[0], f"{name} item")
+
+    raise ValueError(f"Unknown type {python_type} for {name}")
+
+
+def autospec(clazz):
+    """
+    Automatically generate a thrift_spec dict based on attrs metadata.
+
+    Args:
+        clazz: The class to decorate.
+
+    Returns:
+        The class given as a parameter.
+    """
+    clazz.thrift_spec = {}
+    index = 1
+    for field in attr.fields(clazz):
+        field_type, subtype = field.metadata.get(TYPE_META) or _guess_type(field.type, field.name)
+        clazz.thrift_spec[index] = (field_type, field.name, subtype)
+        index += 1
+    return clazz
+
+
+def field(thrift_type: TType, subtype: Subtype = None, **kwargs) -> attr.Attribute:
+    """
+    Specify an explicit type for the :meth:`autospec` decorator.
+
+    Args:
+        thrift_type: The thrift type to use for the field.
+        subtype: The subtype, for multi-part types like lists and maps.
+        **kwargs: Other parameters to pass to :meth:`attr.ib`.
+
+    Returns:
+        The result of :meth:`attr.ib`
+    """
+    kwargs.setdefault("metadata", {})[TYPE_META] = (thrift_type, subtype)
+    return attr.ib(**kwargs)

+ 112 - 0
mauigpapi/mqtt/thrift/ig_objects.py

@@ -0,0 +1,112 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import Dict, List, Union
+
+from attr import dataclass
+
+from .type import TType
+from .autospec import field, autospec
+from .write import ThriftWriter
+from .read import ThriftReader
+
+
+@autospec
+@dataclass(kw_only=True)
+class RealtimeClientInfo:
+    user_id: int = field(TType.I64)
+    user_agent: str
+    client_capabilities: int = field(TType.I64)
+    endpoint_capabilities: int = field(TType.I64)
+    publish_format: int = field(TType.I32)
+    no_automatic_foreground: bool
+    make_user_available_in_foreground: bool
+    device_id: str
+    is_initially_foreground: bool
+    network_type: int = field(TType.I32)
+    network_subtype: int = field(TType.I32)
+    client_mqtt_session_id: int = field(TType.I64)
+    client_ip_address: str = None
+    subscribe_topics: List[int] = field(TType.LIST, TType.I32)
+    client_type: str
+    app_id: int = field(TType.I64)
+    override_nectar_logging: bool = None
+    connect_token_hash: str = None
+    region_preference: str
+    device_secret: str
+    client_stack: int = field(TType.BYTE)
+    fbns_connection_key: int = field(TType.I64, default=None)
+    fbns_connection_secret: str = None
+    fbns_device_id: str = None
+    fbns_device_secret: str = None
+    another_unknown: int = field(TType.I64, default=None)
+
+
+@autospec
+@dataclass(kw_only=True)
+class RealtimeConfig:
+    client_identifier: str
+    will_topic: str = None
+    will_message: str = None
+    client_info: RealtimeClientInfo
+    password: str
+    get_diffs_request: List[str] = None
+    zero_rating_token_hash: str = None
+    app_specific_info: Dict[str, str]
+
+    def to_thrift(self) -> bytes:
+        buf = ThriftWriter()
+        buf.write_struct(self)
+        return buf.getvalue()
+
+
+@autospec
+@dataclass(kw_only=True)
+class ForegroundStateConfig:
+    in_foreground_app: bool
+    in_foreground_device: bool
+    keep_alive_timeout: int = field(TType.I32)
+    subscribe_topics: List[str]
+    subscribe_generic_topics: List[str]
+    unsubscribe_topics: List[str]
+    unsubscribe_generic_topics: List[str]
+    request_id: int = field(TType.I64)
+
+    def to_thrift(self) -> bytes:
+        buf = ThriftWriter()
+        buf.write_struct(self)
+        return buf.getvalue()
+
+
+@dataclass(kw_only=True)
+class IncomingMessage:
+    topic: Union[str, int]
+    payload: str
+
+    @classmethod
+    def from_thrift(cls, data: bytes) -> 'IncomingMessage':
+        buf = ThriftReader(data)
+        topic_type = buf.read_field()
+        if topic_type == TType.BINARY:
+            topic = buf.read(buf.read_varint()).decode("utf-8")
+        elif topic_type == TType.I32:
+            topic = buf.read_small_int()
+        else:
+            raise ValueError(f"Unsupported topic type {topic_type}")
+        payload_type = buf.read_field()
+        if payload_type != TType.BINARY:
+            raise ValueError(f"Unsupported payload type {topic_type}")
+        payload = buf.read(buf.read_varint()).decode("utf-8")
+        return cls(topic=topic, payload=payload)

+ 69 - 0
mauigpapi/mqtt/thrift/read.py

@@ -0,0 +1,69 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import List
+import io
+
+from .type import TType
+
+
+class ThriftReader(io.BytesIO):
+    prev_field_id: int
+    stack: List[int]
+
+    def __init__(self, *args, **kwargs) -> None:
+        super().__init__(*args, **kwargs)
+        self.prev_field_id = 0
+        self.stack = []
+
+    def _push_stack(self) -> None:
+        self.stack.append(self.prev_field_id)
+        self.prev_field_id = 0
+
+    def _pop_stack(self) -> None:
+        if self.stack:
+            self.prev_field_id = self.stack.pop()
+
+    def _read_byte(self, signed: bool = False) -> int:
+        return int.from_bytes(self.read(1), "big", signed=signed)
+
+    @staticmethod
+    def _from_zigzag(val: int) -> int:
+        return (val >> 1) ^ -(val & 1)
+
+    def read_small_int(self) -> int:
+        return self._from_zigzag(self.read_varint())
+
+    def read_varint(self) -> int:
+        shift = 0
+        result = 0
+        while True:
+            byte = self._read_byte()
+            result |= (byte & 0x7f) << shift
+            if (byte & 0x80) == 0:
+                break
+            shift += 7
+        return result
+
+    def read_field(self) -> TType:
+        byte = self._read_byte()
+        if byte == 0:
+            return TType.STOP
+        delta = (byte & 0xf0) >> 4
+        if delta == 0:
+            self.prev_field_id = self._from_zigzag(self.read_varint())
+        else:
+            self.prev_field_id += delta
+        return TType(byte & 0x0f)

+ 36 - 0
mauigpapi/mqtt/thrift/type.py

@@ -0,0 +1,36 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from enum import IntEnum
+
+
+class TType(IntEnum):
+    STOP = 0
+    TRUE = 1
+    FALSE = 2
+    BYTE = 3
+    I16 = 4
+    I32 = 5
+    I64 = 6
+    # DOUBLE = 7
+    BINARY = 8
+    STRING = 8
+    LIST = 9
+    SET = 10
+    MAP = 11
+    STRUCT = 12
+
+    # internal
+    BOOL = 0xa1

+ 171 - 0
mauigpapi/mqtt/thrift/write.py

@@ -0,0 +1,171 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import Any, Union, List, Dict, Optional
+import io
+
+from .type import TType
+
+
+class ThriftWriter(io.BytesIO):
+    prev_field_id: int
+    stack: List[int]
+
+    def __init__(self, *args, **kwargs) -> None:
+        super().__init__(*args, **kwargs)
+        self.prev_field_id = 0
+        self.stack = []
+
+    def _push_stack(self) -> None:
+        self.stack.append(self.prev_field_id)
+        self.prev_field_id = 0
+
+    def _pop_stack(self) -> None:
+        if self.stack:
+            self.prev_field_id = self.stack.pop()
+
+    def _write_byte(self, byte: Union[int, TType]) -> None:
+        self.write(bytes([byte]))
+
+    @staticmethod
+    def _to_zigzag(val: int, bits: int) -> int:
+        return (val << 1) ^ (val >> (bits - 1))
+
+    def _write_varint(self, val: int) -> None:
+        while True:
+            byte = val & ~0x7f
+            if byte == 0:
+                self._write_byte(val)
+                break
+            elif byte == -128:
+                self._write_byte(0)
+                break
+            else:
+                self._write_byte((val & 0xff) | 0x80)
+                val = val >> 7
+
+    def _write_word(self, val: int) -> None:
+        self._write_varint(self._to_zigzag(val, 16))
+
+    def _write_int(self, val: int) -> None:
+        self._write_varint(self._to_zigzag(val, 32))
+
+    def _write_long(self, val: int) -> None:
+        self._write_varint(self._to_zigzag(val, 64))
+
+    def write_field_begin(self, field_id: int, ttype: TType) -> None:
+        ttype_val = ttype.value
+        delta = field_id - self.prev_field_id
+        if 0 < delta < 16:
+            self._write_byte((delta << 4) | ttype_val)
+        else:
+            self._write_byte(ttype_val)
+            self._write_word(field_id)
+        self.prev_field_id = field_id
+
+    def write_map(self, field_id: int, key_type: TType, value_type: TType, val: Dict[Any, Any]
+                  ) -> None:
+        self.write_field_begin(field_id, TType.MAP)
+        if not map:
+            self._write_byte(0)
+            return
+        self._write_varint(len(val))
+        self._write_byte(((key_type.value & 0xf) << 4) | (value_type.value & 0xf))
+        for key, value in val.items():
+            self.write_val(None, key_type, key)
+            self.write_val(None, value_type, value)
+
+    def write_string_direct(self, val: Union[str, bytes]) -> None:
+        if isinstance(val, str):
+            val = val.encode("utf-8")
+        self._write_varint(len(val))
+        self.write(val)
+
+    def write_stop(self) -> None:
+        self._write_byte(TType.STOP.value)
+        self._pop_stack()
+
+    def write_int8(self, field_id: int, val: int) -> None:
+        self.write_field_begin(field_id, TType.BYTE)
+        self._write_byte(val)
+
+    def write_int16(self, field_id: int, val: int) -> None:
+        self.write_field_begin(field_id, TType.I16)
+        self._write_word(val)
+
+    def write_int32(self, field_id: int, val: int) -> None:
+        self.write_field_begin(field_id, TType.I32)
+        self._write_int(val)
+
+    def write_int64(self, field_id: int, val: int) -> None:
+        self.write_field_begin(field_id, TType.I64)
+        self._write_long(val)
+
+    def write_list(self, field_id: int, item_type: TType, val: List[Any]) -> None:
+        self.write_field_begin(field_id, TType.LIST)
+        if len(val) < 0x0f:
+            self._write_byte((len(val) << 4) | item_type.value)
+        else:
+            self._write_byte(0xf0 | item_type.value)
+            self._write_varint(len(val))
+        for item in val:
+            self.write_val(None, item_type, item)
+
+    def write_struct_begin(self, field_id: int) -> None:
+        self.write_field_begin(field_id, TType.STRUCT)
+        self._push_stack()
+
+    def write_val(self, field_id: Optional[int], ttype: TType, val: Any) -> None:
+        if ttype == TType.BOOL:
+            if field_id is None:
+                raise ValueError("booleans can only be in structs")
+            self.write_field_begin(field_id, TType.TRUE if val else TType.FALSE)
+            return
+        if field_id is not None:
+            self.write_field_begin(field_id, ttype)
+        if ttype == TType.BYTE:
+            self._write_byte(val)
+        elif ttype == TType.I16:
+            self._write_word(val)
+        elif ttype == TType.I32:
+            self._write_int(val)
+        elif ttype == TType.I64:
+            self._write_long(val)
+        elif ttype == TType.BINARY:
+            self.write_string_direct(val)
+        else:
+            raise ValueError(f"{ttype} is not supported by write_val()")
+
+    def write_struct(self, obj: Any) -> None:
+        for field_id in iter(obj.thrift_spec):
+            field_type, field_name, inner_type = obj.thrift_spec[field_id]
+
+            val = getattr(obj, field_name, None)
+            if val is None:
+                continue
+
+            start = len(self.getvalue())
+            if field_type in (TType.BOOL, TType.BYTE, TType.I16, TType.I32, TType.I64,
+                              TType.BINARY):
+                self.write_val(field_id, field_type, val)
+            elif field_type in (TType.LIST, TType.SET):
+                self.write_list(field_id, inner_type, val)
+            elif field_type == TType.MAP:
+                (key_type, _), (value_type, _) = inner_type
+                self.write_map(field_id, key_type, value_type, val)
+            elif field_type == TType.STRUCT:
+                self.write_struct_begin(field_id)
+                self.write_struct(val)
+        self.write_stop()

+ 555 - 0
mauigpapi/mqtt/types.py

@@ -0,0 +1,555 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import Dict, Any, List, Optional
+import json
+
+from mautrix.types import SerializableAttrs, SerializableEnum, JSON, dataclass, field
+
+from .subscription import GraphQLQueryID
+
+_topic_map: Dict[str, str] = {
+    "/pp": "34",  # unknown
+    "/ig_sub_iris": "134",
+    "/ig_sub_iris_response": "135",
+    "/ig_message_sync": "146",
+    "/ig_send_message": "132",
+    "/ig_send_message_response": "133",
+    "/ig_realtime_sub": "149",
+    "/pubsub": "88",
+    "/t_fs": "102",  # Foreground state
+    "/graphql": "9",
+    "/t_region_hint": "150",
+    "/mqtt_health_stats": "/mqtt_health_stats",
+    "179": "179",  # also unknown
+}
+
+_reverse_topic_map: Dict[str, str] = {value: key for key, value in _topic_map.items()}
+
+
+class RealtimeTopic(SerializableEnum):
+    SUB_IRIS = "/ig_sub_iris"
+    SUB_IRIS_RESPONSE = "/ig_sub_iris_response"
+    MESSAGE_SYNC = "/ig_message_sync"
+    SEND_MESSAGE = "/ig_send_message"
+    SEND_MESSAGE_RESPONSE = "/ig_send_message_response"
+    REALTIME_SUB = "/ig_realtime_sub"
+    PUBSUB = "/pubsub"
+    FOREGROUND_STATE = "/t_fs"
+    GRAPHQL = "/graphql"
+    REGION_HINT = "/t_region_hint"
+    MQTT_HEALTH_STATS = "/mqtt_health_stats"
+    UNKNOWN_PP = "/pp"
+    UNKNOWN_179 = "179"
+
+    @property
+    def encoded(self) -> str:
+        return _topic_map[self.value]
+
+    @classmethod
+    def decode(cls, val: str) -> 'RealtimeTopic':
+        return cls(_reverse_topic_map[val])
+
+
+class ThreadItemType(SerializableEnum):
+    DELETION = "deletion"
+    MEDIA = "media"
+    TEXT = "text"
+    LIKE = "like"
+    HASHTAG = "hashtag"
+    PROFILE = "profile"
+    MEDIA_SHARE = "media_share"
+    LOCATION = "location"
+    ACTION_LOG = "action_log"
+    TITLE = "title"
+    USER_REACTION = "user_reaction"
+    HISTORY_EDIT = "history_edit"
+    REACTION_LOG = "reaction_log"
+    REEL_SHARE = "reel_share"
+    DEPRECATED_CHANNEL = "deprecated_channel"
+    LINK = "link"
+    RAVEN_MEDIA = "raven_media"
+    LIVE_VIDEO_SHARE = "live_video_share"
+    TEST = "test"
+    STORY_SHARE = "story_share"
+    REEL_REACT = "reel_react"
+    LIVE_INVITE_GUEST = "live_invite_guest"
+    LIVE_VIEWER_INVITE = "live_viewer_invite"
+    TYPE_MAX = "type_max"
+    PLACEHOLDER = "placeholder"
+    PRODUCT = "product"
+    PRODUCT_SHARE = "product_share"
+    VIDEO_CALL_EVENT = "video_call_event"
+    POLL_VOTE = "poll_vote"
+    FELIX_SHARE = "felix_share"
+    ANIMATED_MEDIA = "animated_media"
+    CTA_LINK = "cta_link"
+    VOICE_MEDIA = "voice_media"
+    STATIC_STICKER = "static_sticker"
+    AR_EFFECT = "ar_effect"
+    SELFIE_STICKER = "selfie_sticker"
+    REACTION = "reaction"
+
+
+class Operation(SerializableEnum):
+    ADD = "add"
+    REPLACE = "replace"
+    REMOVE = "remove"
+
+
+class ThreadAction(SerializableEnum):
+    SEND_ITEM = "send_item"
+    PROFILE = "profile"
+    MARK_SEEN = "mark_seen"
+    MARK_VISUAL_ITEM_SEEN = "mark_visual_item_seen"
+    INDICATE_ACTIVITY = "indicate_activity"
+
+
+class ReactionStatus(SerializableEnum):
+    CREATED = "created"
+    DELETED = "deleted"
+
+
+class TypingStatus(SerializableEnum):
+    OFF = 0
+    TEXT = 1
+    VISUAL = 2
+
+
+@dataclass
+class CommandResponsePayload(SerializableAttrs['CommandResponsePayload']):
+    client_context: Optional[str] = None
+    item_id: Optional[str] = None
+    timestamp: Optional[str] = None
+    thread_id: Optional[str] = None
+
+
+@dataclass
+class CommandResponse(SerializableAttrs['CommandResponse']):
+    action: str
+    status: str
+    status_code: str
+    payload: CommandResponsePayload
+
+
+@dataclass
+class IrisPayloadData(SerializableAttrs['IrisPayloadData']):
+    op: Operation
+    path: str
+    value: str
+
+
+@dataclass
+class IrisPayload(SerializableAttrs['IrisPayload']):
+    data: List[IrisPayloadData]
+    message_type: int
+    seq_id: int
+    event: str = "patch"
+    mutation_token: Optional[str] = None
+    realtime: Optional[bool] = None
+    sampled: Optional[bool] = None
+
+
+class ViewMode(SerializableEnum):
+    ONCE = "once"
+    REPLAYABLE = "replayable"
+    PERMANENT = "permanent"
+
+
+@dataclass
+class CreativeConfig(SerializableAttrs['CreativeConfig']):
+    capture_type: str
+    camera_facing: str
+    should_render_try_it_on: bool
+
+
+@dataclass
+class CreateModeAttribution(SerializableAttrs['CreateModeAttribution']):
+    type: str
+    name: str
+
+
+@dataclass
+class ImageVersion(SerializableAttrs['ImageVersion']):
+    width: int
+    height: int
+    url: str
+    estimated_scan_sizes: Optional[List[int]] = None
+
+
+@dataclass
+class ImageVersions(SerializableAttrs['ImageVersions']):
+    candidates: List[ImageVersion]
+
+
+@dataclass
+class VideoVersion(SerializableAttrs['VideoVersion']):
+    type: int
+    width: int
+    height: int
+    url: str
+    id: str
+
+
+class MediaType(SerializableEnum):
+    IMAGE = 1
+    VIDEO = 2
+    AD_MAP = 6
+    LIVE = 7
+    CAROUSEL = 8
+    LIVE_REPLAY = 9
+    COLLECTION = 10
+    AUDIO = 11
+    SHOWREEL_NATIVE = 12
+
+
+@dataclass
+class RegularMediaItem(SerializableAttrs['RegularMediaItem']):
+    id: str
+    image_versions2: Optional[ImageVersions] = None
+    video_versions: Optional[List[VideoVersion]] = None
+    original_width: int
+    original_height: int
+    media_type: MediaType
+    media_id: Optional[int] = None
+    organic_tracking_token: Optional[str] = None
+    creative_config: Optional[CreativeConfig] = None
+    create_mode_attribution: Optional[CreateModeAttribution] = None
+
+
+@dataclass
+class FriendshipStatus(SerializableAttrs['FriendshipStatus']):
+    following: bool
+    outgoing_request: bool
+    is_bestie: bool
+    is_restricted: bool
+
+
+@dataclass
+class MinimalUser(SerializableAttrs['MinimalUser']):
+    pk: int
+    username: str
+
+
+@dataclass
+class User(MinimalUser, SerializableAttrs['User']):
+    full_name: str
+    is_private: bool
+    is_favorite: bool
+    is_unpublished: bool
+    has_anonymous_profile_picture: bool
+    profile_pic_url: str
+    profile_pic_id: str
+    latest_reel_media: int
+    friendship_status: FriendshipStatus
+
+
+@dataclass
+class Caption(SerializableAttrs['Caption']):
+    pk: int
+    user_id: int
+    text: str
+    # TODO enum?
+    type: int
+    created_at: int
+    created_at_utc: int
+    content_type: str
+    # TODO enum?
+    status: str
+    bit_flags: int
+    user: User
+    did_report_as_spam: bool
+    share_enabled: bool
+    media_id: int
+
+
+@dataclass
+class MediaShareItem(SerializableAttrs['MediaShareItem']):
+    taken_at: int
+    pk: int
+    id: str
+    device_timestamp: int
+    media_type: MediaType
+    code: str
+    client_cache_key: str
+    filter_type: int
+    image_versions2: ImageVersions
+    video_versions: VideoVersion
+    original_width: int
+    original_height: int
+    user: User
+    can_viewer_reshare: bool
+    caption_is_edited: bool
+    comment_likes_enabled: bool
+    comment_threading_enabled: bool
+    has_more_comments: bool
+    max_num_visible_preview_comments: int
+    can_view_more_preview_comments: bool
+    comment_count: int
+    like_count: int
+    has_liked: bool
+    photo_of_you: bool
+    caption: Caption
+    can_viewer_save: bool
+    organic_tracking_token: str
+
+
+@dataclass
+class ReplayableMediaItem(SerializableAttrs['ReplayableMediaItem']):
+    view_mode: ViewMode
+    seen_count: int
+    seen_user_ids: List[int]
+    replay_expiring_at_us: Optional[Any] = None
+
+
+@dataclass
+class VisualMedia(ReplayableMediaItem, SerializableAttrs['VisualMedia']):
+    url_expire_at_secs: int
+    playback_duration_secs: int
+    media: RegularMediaItem
+
+
+@dataclass
+class AudioInfo(SerializableAttrs['AudioInfo']):
+    audio_src: str
+    duration: int
+    waveform_data: List[int]
+    waveform_sampling_frequence_hz: int
+
+
+@dataclass
+class VoiceMediaData(SerializableAttrs['VoiceMediaData']):
+    id: str
+    audio: AudioInfo
+    organic_tracking_token: str
+    user: MinimalUser
+    # TODO enum?
+    product_type: str = "direct_audio"
+    media_type: MediaType = MediaType.AUDIO
+
+
+@dataclass
+class VoiceMediaItem(ReplayableMediaItem, SerializableAttrs['VoiceMediaItem']):
+    media: VoiceMediaData
+
+
+@dataclass
+class AnimatedMediaImage(SerializableAttrs['AnimatedMediaImage']):
+    height: str
+    mp4: str
+    mp4_size: str
+    size: str
+    url: str
+    webp: str
+    webp_size: str
+    width: str
+
+
+@dataclass
+class AnimatedMediaImages(SerializableAttrs['AnimatedMediaImages']):
+    fixed_height: Optional[AnimatedMediaImage] = None
+
+
+@dataclass
+class AnimatedMediaItem(SerializableAttrs['AnimatedMediaItem']):
+    id: str
+    is_random: str
+    is_sticker: str
+    images: AnimatedMediaImages
+
+
+@dataclass
+class MessageSyncMessage(SerializableAttrs['MessageSyncMessage']):
+    thread_id: str
+    item_id: Optional[str] = None
+    admin_user_ids: Optional[int] = None
+    approval_required_for_new_members: Optional[bool] = None
+    participants: Optional[Dict[str, str]] = None
+    # TODO enum
+    op: Operation = Operation.ADD
+    path: str
+    user_id: Optional[int] = None
+    timestamp: int
+    item_type: Optional[ThreadItemType] = None
+    text: Optional[str] = None
+    media: Optional[RegularMediaItem] = None
+    voice_media: Optional[VoiceMediaItem] = None
+    animated_media: Optional[AnimatedMediaItem] = None
+    visual_media: Optional[VisualMedia] = None
+    media_share: Optional[MediaShareItem] = None
+    reactions: Optional[dict] = None
+
+
+@dataclass
+class MessageSyncEvent(SerializableAttrs['MessageSyncEvent']):
+    iris: IrisPayload
+    message: MessageSyncMessage
+
+
+@dataclass
+class PubsubPublishMetadata(SerializableAttrs['PubsubPublishMetadata']):
+    publish_time_ms: str
+    topic_publish_id: int
+
+
+@dataclass
+class PubsubBasePayload(SerializableAttrs['PubsubBasePayload']):
+    lazy: bool
+    event: str = "patch"
+    publish_metadata: Optional[PubsubPublishMetadata] = None
+    num_endpoints: Optional[int] = None
+
+
+@dataclass
+class ActivityIndicatorData(SerializableAttrs['ActivityIndicatorData']):
+    timestamp: str
+    sender_id: str
+    ttl: int
+    activity_status: TypingStatus
+
+    @classmethod
+    def deserialize(cls, data: JSON) -> 'ActivityIndicatorData':
+        # The ActivityIndicatorData in PubsubPayloadData is actually a string,
+        # so we need to unmarshal it first.
+        if isinstance(data, str):
+            data = json.loads(data)
+        return super().deserialize(data)
+
+
+@dataclass
+class PubsubPayloadData(SerializableAttrs['PubsubPayloadData']):
+    double_publish: bool = field(json="doublePublish")
+    value: ActivityIndicatorData
+    path: str
+    op: Operation = Operation.ADD
+
+
+@dataclass
+class PubsubPayload(PubsubBasePayload, SerializableAttrs['PubsubPayload']):
+    data: List[PubsubPayloadData]
+
+
+@dataclass
+class PubsubEvent(SerializableAttrs['PubsubEvent']):
+    base: PubsubBasePayload
+    data: PubsubPayloadData
+    thread_id: str
+    activity_indicator_id: str
+
+
+@dataclass
+class AppPresenceEvent(SerializableAttrs['AppPresenceEvent']):
+    user_id: str
+    is_active: bool
+    last_activity_at_ms: str
+    in_threads: List[Any]
+
+
+@dataclass
+class AppPresenceEventPayload(SerializableAttrs['AppPresenceEventPayload']):
+    presence_event: AppPresenceEvent
+
+
+@dataclass
+class ZeroProductProvisioningEvent(SerializableAttrs['ZeroProductProvisioningEvent']):
+    device_id: str
+    product_name: str
+    zero_provisioned_time: str
+
+
+@dataclass
+class RealtimeZeroProvisionPayload(SerializableAttrs['RealtimeZeroProvisionPayload']):
+    zero_product_provisioning_event: ZeroProductProvisioningEvent
+
+
+@dataclass
+class ClientConfigUpdateEvent(SerializableAttrs['ClientConfigUpdateEvent']):
+    publish_id: str
+    client_config_name: str
+    backing: str = "QE"
+    client_subscription_id: str = GraphQLQueryID.clientConfigUpdate
+
+
+@dataclass
+class ClientConfigUpdatePayload(SerializableAttrs['ClientConfigUpdatePayload']):
+    client_config_update_event: ClientConfigUpdateEvent
+
+
+RealtimeDirectData = ActivityIndicatorData
+
+
+@dataclass
+class RealtimeDirectEvent(SerializableAttrs['RealtimeDirectEvent']):
+    op: Operation
+    path: str
+    value: RealtimeDirectData
+
+
+@dataclass
+class LiveVideoCommentUser(SerializableAttrs['LiveVideoCommentUser']):
+    pk: str
+    username: str
+    full_name: str
+    is_private: bool
+    is_verified: bool
+    profile_pic_url: str
+    profile_pic_id: Optional[str] = None
+
+
+@dataclass
+class LiveVideoSystemComment(SerializableAttrs['LiveVideoSystemComment']):
+    pk: str
+    created_at: int
+    text: str
+    user_count: int
+    user: LiveVideoCommentUser
+
+
+@dataclass
+class LiveVideoComment(SerializableAttrs['LiveVideoComment']):
+    pk: str
+    user_id: str
+    text: str
+    type: int
+    created_at: int
+    created_at_utc: int
+    content_type: str
+    status: str = "Active"
+    bit_flags: int
+    did_report_as_spam: bool
+    inline_composer_display_condition: str
+    user: LiveVideoCommentUser
+
+
+@dataclass
+class LiveVideoCommentEvent(SerializableAttrs['LiveVideoCommentEvent']):
+    client_subscription_id: str
+    live_seconds_per_comment: int
+    comment_likes_enabled: bool
+    comment_count: int
+    caption: Optional[str] = None
+    caption_is_edited: bool
+    has_more_comments: bool
+    has_more_headload_comments: bool
+    media_header_display: str
+    comment_muted: int
+    comments: Optional[List[LiveVideoComment]] = None
+    pinned_comment: Optional[LiveVideoComment] = None
+    system_comments: Optional[List[LiveVideoSystemComment]] = None
+
+
+@dataclass
+class LiveVideoCommentPayload(SerializableAttrs['LiveVideoCommentPayload']):
+    live_video_comment_event: LiveVideoCommentEvent

+ 1 - 0
mauigpapi/state/__init__.py

@@ -0,0 +1 @@
+from .state import AndroidState

+ 290 - 0
mauigpapi/state/application.py

@@ -0,0 +1,290 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+import pkgutil
+import json
+
+from mautrix.types import SerializableAttrs, dataclass
+
+default_capabilities = json.loads(pkgutil.get_data("mauigpapi.state",
+                                                   "samples/supported-capabilities.json"))
+
+
+@dataclass
+class AndroidApplication(SerializableAttrs['AndroidApplication']):
+    APP_VERSION: str = "159.0.0.29.122"
+    APP_VERSION_CODE: str = "244390482"
+    BREADCRUMB_KEY: str = "iN4$aGr0m"
+    FACEBOOK_ANALYTICS_APPLICATION_ID: str = "567067343352427"
+    FACEBOOK_OTA_FIELDS: str = (
+        "update%7Bdownload_uri%2Cdownload_uri_delta_base%2Cversion_code_delta_base"
+        "%2Cdownload_uri_delta%2Cfallback_to_full_update%2Cfile_size_delta%2Cversion_code"
+        "%2Cpublished_date%2Cfile_size%2Cota_bundle_type%2Cresources_checksum%2Callowed_networks"
+        "%2Crelease_id%7D"
+    )
+    FACEBOOK_ORCA_APPLICATION_ID: str = "124024574287414"
+
+    BLOKS_VERSION_ID: str = "92ee42225c399547492c358aacb3ac176e6a953a1d828e7d2507c465df4252d1"
+    CAPABILITIES: str = "3brTvx8="
+    SUPPORTED_CAPABILITIES: str = default_capabilities
+
+    EXPERIMENTS: str = (
+        "ig_android_video_raven_streaming_upload_universe,"
+        "ig_android_vc_explicit_intent_for_notification,ig_android_shopping_checkout_signaling,"
+        "ig_stories_ads_delivery_rules,ig_shopping_checkout_improvements_universe,"
+        "ig_business_new_value_prop_universe,ig_android_mqtt_cookie_auth_memcache_universe,"
+        "ig_android_vio_pipeline_universe,ig_android_product_tag_suggestions,"
+        "ig_android_gallery_grid_controller_folder_cache,ig_android_suggested_users_background,"
+        "ig_android_stories_music_search_typeahead,ig_android_delayed_comments,"
+        "ig_android_direct_mutation_manager_media_3,ig_android_gif_preview_quality_universe,"
+        "ig_android_ads_bottom_sheet_report_flow,ig_android_login_onetap_upsell_universe,"
+        "ig_fb_graph_differentiation,ig_android_shopping_bag_null_state_v1,"
+        "ig_android_iris_improvements,ig_camera_android_feed_effect_attribution_universe,"
+        "ig_android_test_not_signing_address_book_unlink_endpoint,"
+        "ig_android_stories_share_extension_video_segmentation,"
+        "ig_android_search_nearby_places_universe,"
+        "ig_graph_management_production_h2_2019_holdout_universe,"
+        "ig_android_vc_migrate_to_bluetooth_v2_universe,ig_ei_option_setting_universe,"
+        "ig_android_direct_reshare_chaining,"
+        "ig_android_test_remove_button_main_cta_self_followers_universe,"
+        "instagram_ns_qp_prefetch_universe,ig_android_camera_leak,"
+        "ig_android_wellbeing_support_frx_live_reporting,"
+        "ig_android_separate_empty_feed_su_universe,ig_stories_rainbow_ring,"
+        "ig_android_zero_rating_carrier_signal,ig_explore_2019_h1_destination_cover,"
+        "ig_android_explore_recyclerview_universe,ig_android_image_pdq_calculation,"
+        "ig_android_vc_egl10_compat,ig_camera_android_subtle_filter_universe,"
+        "ig_android_whats_app_contact_invite_universe,"
+        "ig_android_direct_add_member_dialog_universe,"
+        "ig_android_xposting_reel_memory_share_universe,"
+        "ig_android_viewpoint_stories_public_testing,ig_graph_management_h2_2019_universe,"
+        "ig_android_photo_creation_large_width,ig_android_save_all,"
+        "ig_android_video_upload_hevc_encoding_universe,"
+        "instagram_shopping_hero_carousel_visual_variant_consolidation,"
+        "ig_android_vc_face_effects_universe,ig_android_fbpage_on_profile_side_tray,"
+        "ig_android_ttcp_improvements,ig_android_igtv_refresh_tv_guide_interval,"
+        "ig_shopping_bag_universe,ig_android_recyclerview_binder_group_enabled_universe,"
+        "ig_android_video_exoplayer_2,ig_rn_branded_content_settings_approval_on_select_save,"
+        "ig_android_account_insights_shopping_content_universe,"
+        "ig_branded_content_tagging_approval_request_flow_brand_side_v2,"
+        "ig_android_render_thread_memory_leak_holdout,"
+        "ig_threads_clear_notifications_on_has_seen,"
+        "ig_android_xposting_dual_destination_shortcut_fix,"
+        "ig_android_wellbeing_support_frx_profile_reporting,"
+        "ig_android_show_create_content_pages_universe,ig_android_camera_reduce_file_exif_reads,"
+        "ig_android_disk_usage_logging_universe,ig_android_stories_blacklist,"
+        "ig_payments_billing_address,ig_android_fs_new_gallery_hashtag_prompts,"
+        "ig_android_video_product_specific_abr,ig_android_sidecar_segmented_streaming_universe,"
+        "ig_camera_android_gyro_senser_sampling_period_universe,"
+        "ig_android_xposting_feed_to_stories_reshares_universe,"
+        "ig_android_stories_layout_universe,ig_emoji_render_counter_logging_universe,"
+        "ig_android_wellbeing_support_frx_feed_posts_reporting,"
+        "ig_android_sharedpreferences_qpl_logging,ig_android_vc_cpu_overuse_universe,"
+        "ig_android_image_upload_quality_universe,"
+        "ig_android_invite_list_button_redesign_universe,"
+        "ig_android_react_native_email_sms_settings_universe,ig_android_enable_zero_rating,"
+        "ig_android_direct_leave_from_group_message_requests,"
+        "ig_android_unfollow_reciprocal_universe,ig_android_stories_viewer_modal_activity,"
+        "ig_android_publisher_stories_migration,"
+        "aymt_instagram_promote_flow_abandonment_ig_universe,"
+        "ig_android_whitehat_options_universe,ig_android_stories_context_sheets_universe,"
+        "ig_android_stories_vpvd_container_module_fix,ig_android_delete_ssim_compare_img_soon,"
+        "instagram_android_profile_follow_cta_context_feed,"
+        "ig_android_personal_user_xposting_destination_fix,"
+        "ig_android_stories_boomerang_v2_universe,ig_android_direct_message_follow_button,"
+        "ig_android_video_raven_passthrough,ig_android_vc_cowatch_universe,"
+        "ig_shopping_insights_wc_copy_update_android,"
+        "android_cameracore_fbaudio_integration_ig_universe,"
+        "ig_stories_ads_media_based_insertion,ig_android_analytics_background_uploader_schedule,"
+        "ig_android_explore_reel_loading_state,ig_android_wellbeing_timeinapp_v1_universe,"
+        "ig_end_of_feed_universe,ig_android_mainfeed_generate_prefetch_background,"
+        "ig_android_feed_ads_ppr_universe,ig_android_igtv_browse_long_press,"
+        "ig_xposting_mention_reshare_stories,ig_threads_sanity_check_thread_viewkeys,"
+        "ig_android_vc_shareable_moments_universe,ig_android_igtv_stories_preview,"
+        "ig_android_shopping_product_metadata_on_product_tiles_universe,"
+        "ig_android_stories_quick_react_gif_universe,ig_android_video_qp_logger_universe,"
+        "ig_android_stories_weblink_creation,ig_android_story_bottom_sheet_top_clips_mas,"
+        "ig_android_frx_highlight_cover_reporting_qe,ig_email_sent_list_universe,"
+        "ig_android_vc_capture_universe,ig_android_optic_face_detection,"
+        "ig_android_save_to_collections_flow,ig_android_direct_segmented_video,"
+        "ig_android_stories_video_prefetch_kb,ig_android_direct_mark_as_read_notif_action,"
+        "ig_android_not_interested_secondary_options,ig_android_product_breakdown_post_insights,"
+        "ig_inventory_connections,ig_android_canvas_cookie_universe,"
+        "ig_android_video_streaming_upload_universe,"
+        "ig_android_wellbeing_support_frx_stories_reporting,ig_android_smplt_universe,"
+        "ig_android_vc_missed_call_call_back_action_universe,"
+        "ig_cameracore_android_new_optic_camera2,ig_android_partial_share_sheet,"
+        "ig_android_secondary_inbox_universe,ig_android_fbc_upsell_on_dp_first_load,"
+        "ig_android_stories_sundial_creation_universe,saved_collections_cache_universe,"
+        "ig_android_show_self_followers_after_becoming_private_universe,"
+        "ig_camera_android_release_drawing_view_universe,"
+        "ig_android_music_story_fb_crosspost_universe,"
+        "ig_android_payments_growth_promote_payments_in_payments,"
+        "ig_carousel_bumped_organic_impression_client_universe,"
+        "ig_android_business_attribute_sync,ig_biz_post_approval_nux_universe,"
+        "ig_camera_android_bg_processor,"
+        "ig_android_ig_personal_account_to_fb_page_linkage_backfill,"
+        "ig_android_dropframe_manager,ig_android_ad_stories_scroll_perf_universe,"
+        "ig_android_persistent_nux,ig_android_crash_fix_detach_from_gl_context,"
+        "ig_android_branded_content_upsell_keywords_extension,"
+        "ig_android_tango_cpu_overuse_universe,"
+        "ig_android_direct_wellbeing_message_reachability_settings,"
+        "ig_android_edit_location_page_info,ig_android_unfollow_from_main_feed_v2,"
+        "ig_android_stories_project_eclipse,ig_direct_android_bubble_system,"
+        "ig_android_self_story_setting_option_in_menu,"
+        "ig_android_frx_creation_question_responses_reporting,qe_android_direct_vm_view_modes,"
+        "ig_android_li_session_chaining,ig_android_create_mode_memories_see_all,"
+        "ig_android_feed_post_warning_universe,ig_camera_android_device_capabilities_experiment,"
+        "ig_mprotect_code_universe,ig_android_video_visual_quality_score_based_abr,"
+        "ig_explore_2018_post_chaining_account_recs_dedupe_universe,"
+        "ig_android_view_info_universe,ig_android_camera_upsell_dialog,"
+        "ig_android_business_transaction_in_stories_consumer,ig_android_dead_code_detection,"
+        "ig_android_stories_video_seeking_audio_bug_fix,ig_android_qp_kill_switch,"
+        "ig_android_new_follower_removal_universe,ig_android_feed_post_sticker,"
+        "ig_android_business_cross_post_with_biz_id_infra,"
+        "ig_android_inline_editing_local_prefill,"
+        "ig_android_reel_tray_item_impression_logging_viewpoint,"
+        "ig_android_story_bottom_sheet_music_mas,ig_android_video_abr_universe,"
+        "ig_android_unify_graph_management_actions,ig_android_ads_history_universe,"
+        "ig_android_vc_cowatch_media_share_universe,ig_challenge_general_v2,"
+        "ig_android_place_signature_universe,ig_android_direct_inbox_cache_universe,"
+        "ig_android_business_promote_tooltip,"
+        "ig_android_wellbeing_support_frx_hashtags_reporting,"
+        "ig_android_wait_for_app_initialization_on_push_action_universe,"
+        "ig_android_pending_media_file_registry,ig_android_direct_aggregated_media_and_reshares,"
+        "ig_camera_android_facetracker_v12_universe,"
+        "ig_android_story_bottom_sheet_clips_single_audio_mas,"
+        "ig_android_fb_follow_server_linkage_universe,igqe_pending_tagged_posts,"
+        "ig_sim_api_analytics_reporting,ig_android_self_following_v2_universe,"
+        "ig_android_interest_follows_universe,ig_android_direct_view_more_qe,"
+        "ig_android_iab_holdout_universe,ig_android_audience_control,"
+        "ig_android_memory_use_logging_universe,ig_android_branded_content_tag_redesign_organic,"
+        "ig_camera_android_paris_filter_universe,ig_android_igtv_whitelisted_for_web,"
+        "ig_rti_inapp_notifications_universe,ig_android_vc_join_timeout_universe,"
+        "ig_android_share_publish_page_universe,ig_direct_max_participants,"
+        "ig_android_multi_dex_class_loader_v2,ig_commerce_platform_ptx_bloks_universe,"
+        "ig_android_video_raven_bitrate_ladder_universe,"
+        "ig_android_live_realtime_comments_universe,ig_android_recipient_picker,"
+        "ig_android_graphql_survey_new_proxy_universe,ig_android_music_browser_redesign,"
+        "ig_android_disable_manual_retries,ig_android_qr_code_nametag,"
+        "ig_android_purx_native_checkout_universe,ig_android_fs_creation_flow_tweaks,"
+        "ig_android_apr_lazy_build_request_infra,"
+        "ig_android_business_transaction_in_stories_creator,"
+        "ig_cameracore_android_new_optic_camera2_galaxy,"
+        "ig_android_wellbeing_support_frx_igtv_reporting,"
+        "ig_android_branded_content_appeal_states,ig_android_claim_location_page,"
+        "ig_android_location_integrity_universe,"
+        "ig_video_experimental_encoding_consumption_universe,"
+        "ig_android_biz_story_to_fb_page_improvement,"
+        "ig_shopping_checkout_improvements_v2_universe,"
+        "ig_android_direct_thread_target_queue_universe,"
+        "ig_android_save_to_collections_bottom_sheet_refactor,"
+        "ig_android_branded_content_insights_disclosure,ig_android_create_mode_tap_to_cycle,"
+        "ig_android_fb_profile_integration_universe,"
+        "ig_android_shopping_bag_optimization_universe,ig_android_create_page_on_top_universe,"
+        "android_ig_cameracore_aspect_ratio_fix,ig_android_feed_auto_share_to_facebook_dialog,"
+        "ig_android_skip_button_content_on_connect_fb_universe,"
+        "ig_android_igtv_explore2x2_viewer,ig_android_network_perf_qpl_ppr,"
+        "ig_android_wellbeing_support_frx_comment_reporting,"
+        "ig_android_insights_post_dismiss_button,ig_xposting_biz_feed_to_story_reshare,"
+        "ig_android_user_url_deeplink_fbpage_endpoint,"
+        "ig_android_comment_warning_non_english_universe,"
+        "ig_android_wellbeing_support_frx_cowatch_reporting,"
+        "ig_android_profile_lazy_load_carousel_media,"
+        "ig_android_stories_question_sticker_music_format,"
+        "ig_promote_interactive_poll_sticker_igid_universe,ig_android_feed_cache_update,"
+        "ig_pacing_overriding_universe,ig_explore_reel_ring_universe,"
+        "ig_android_follow_request_button_new_ui,ig_android_explore_peek_redesign_universe,"
+        "ig_android_igtv_pip,ig_graph_evolution_holdout_universe,"
+        "ig_android_wishlist_reconsideration_universe,ig_android_sso_use_trustedapp_universe,"
+        "ig_android_stories_music_lyrics,ig_android_camera_formats_ranking_universe,"
+        "ig_android_direct_multi_upload_universe,ig_android_stories_music_awareness_universe,"
+        "ig_explore_2019_h1_video_autoplay_resume,ig_android_multi_capture_camera,"
+        "ig_android_video_upload_quality_qe1,"
+        "ig_android_expanded_xposting_upsell_directly_after_sharing_story_universe,"
+        "ig_android_country_code_fix_universe,ig_android_stories_music_overlay,"
+        "ig_android_multi_thread_sends,ig_android_low_latency_consumption_universe,"
+        "ig_android_render_output_surface_timeout_universe,ig_android_emoji_util_universe_3,"
+        "ig_android_unified_iab_logging_universe,ig_android_shopping_pdp_post_purchase_sharing,"
+        "ig_branded_content_settings_unsaved_changes_dialog,ig_android_realtime_mqtt_logging,"
+        "ig_android_rainbow_hashtags,ig_android_ad_iab_qpl_kill_switch_universe,"
+        "ig_android_create_mode_templates,ig_android_direct_block_from_group_message_requests,"
+        "ig_android_live_subscribe_user_level_universe,ig_android_video_call_finish_universe,"
+        "ig_android_viewpoint_occlusion,ig_biz_growth_insights_universe,"
+        "ig_android_logged_in_delta_migration,ig_android_push_reliability_universe,"
+        "ig_android_self_story_button_non_fbc_accounts,"
+        "ig_android_stories_gallery_video_segmentation,"
+        "ig_android_explore_discover_people_entry_point_universe,"
+        "ig_android_action_sheet_migration_universe,ig_android_live_webrtc_livewith_params,"
+        "ig_camera_android_effect_metadata_cache_refresh_universe,"
+        "ig_android_xposting_upsell_directly_after_sharing_to_story,"
+        "ig_android_vc_codec_settings,ig_android_appstate_logger,"
+        "ig_android_dual_destination_quality_improvement,ig_prefetch_scheduler_backtest,"
+        "ig_android_ads_data_preferences_universe,ig_android_feed_camera_size_setter,"
+        "ig_payment_checkout_cvv,ig_android_vc_background_call_toast_universe,"
+        "ig_android_fb_link_ui_polish_universe,ig_android_qr_code_scanner,"
+        "ig_disable_fsync_universe,mi_viewpoint_viewability_universe,"
+        "ig_android_live_egl10_compat,ig_android_camera_gyro_universe,"
+        "ig_android_video_upload_transform_matrix_fix_universe,ig_android_fb_url_universe,"
+        "ig_android_reel_raven_video_segmented_upload_universe,"
+        "ig_android_fb_sync_options_universe,ig_android_stories_gallery_sticker_universe,"
+        "ig_android_recommend_accounts_destination_routing_fix,"
+        "ig_android_startupmanager_refactor,ig_android_enable_automated_instruction_text_ar,"
+        "ig_traffic_routing_universe,ig_stories_allow_camera_actions_while_recording,"
+        "ig_shopping_checkout_mvp_experiment,ig_android_video_fit_scale_type_igtv,"
+        "ig_android_wellbeing_support_frx_friction_process_education,"
+        "ig_android_direct_pending_media,ig_android_direct_state_observer,"
+        "ig_android_igtv_player_follow_button,ig_android_arengine_remote_scripting_universe,"
+        "ig_android_page_claim_deeplink_qe,ig_android_logging_metric_universe_v2,"
+        "ig_android_xposting_newly_fbc_people,"
+        "ig_android_recognition_tracking_thread_prority_universe,"
+        "ig_android_contact_point_upload_rate_limit_killswitch,"
+        "ig_android_optic_photo_cropping_fixes,ig_android_qpl_class_marker,"
+        "ig_camera_android_gallery_search_universe,ig_android_sso_kototoro_app_universe,"
+        "ig_android_vc_cowatch_config_universe,ig_android_profile_thumbnail_impression,"
+        "ig_android_fs_new_gallery,ig_android_media_remodel,"
+        "ig_camera_android_share_effect_link_universe,ig_android_igtv_autoplay_on_prepare,"
+        "ig_android_ads_rendering_logging,ig_shopping_size_selector_redesign,"
+        "ig_android_vc_webrtc_params,ig_android_image_exif_metadata_ar_effect_id_universe,"
+        "ig_android_optic_new_architecture,ig_android_external_gallery_import_affordance,"
+        "ig_search_hashtag_content_advisory_remove_snooze,"
+        "ig_android_kitkat_segmented_upload_universe,"
+        "ig_android_on_notification_cleared_async_universe,ig_android_direct_new_gallery,"
+        "ig_payment_checkout_info "
+    )
+    LOGIN_EXPERIMENTS: str = (
+        "ig_android_device_detection_info_upload,ig_android_gmail_oauth_in_reg,"
+        "ig_android_account_linking_upsell_universe,ig_android_direct_main_tab_universe_v2,"
+        "ig_android_sms_retriever_backtest_universe,"
+        "ig_android_vc_interop_use_test_igid_universe,"
+        "ig_android_direct_add_direct_to_android_native_photo_share_sheet,"
+        "ig_growth_android_profile_pic_prefill_with_fb_pic_2,"
+        "ig_account_identity_logged_out_signals_global_holdout_universe,"
+        "ig_android_notification_unpack_universe,ig_android_quickcapture_keep_screen_on,"
+        "ig_android_device_based_country_verification,ig_android_login_identifier_fuzzy_match,"
+        "ig_android_reg_modularization_universe,ig_android_video_render_codec_low_memory_gc,"
+        "ig_android_device_verification_separate_endpoint,"
+        "ig_android_email_fuzzy_matching_universe,ig_android_suma_landing_page,"
+        "ig_android_smartlock_hints_universe,ig_android_video_ffmpegutil_pts_fix,"
+        "ig_android_retry_create_account_universe,"
+        "ig_android_caption_typeahead_fix_on_o_universe,"
+        "ig_android_enable_keyboardlistener_redesign,"
+        "ig_android_reg_nux_headers_cleanup_universe,ig_android_nux_add_email_device,"
+        "ig_android_device_info_foreground_reporting,ig_android_shortcuts_2019,"
+        "ig_android_device_verification_fb_signup,"
+        "ig_android_passwordless_account_password_creation_universe,"
+        "ig_android_security_intent_switchoff,ig_android_sim_info_upload,"
+        "ig_android_mobile_http_flow_device_universe,"
+        "ig_android_fb_account_linking_sampling_freq_universe"
+    )

+ 92 - 0
mauigpapi/state/cookies.py

@@ -0,0 +1,92 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import Optional
+from http.cookies import Morsel, SimpleCookie
+
+from aiohttp import CookieJar
+from yarl import URL
+from mautrix.types import Serializable, JSON
+
+from ..errors import IGCookieNotFoundError
+
+ig_url = URL("https://instagram.com")
+
+
+class Cookies(Serializable):
+    jar: CookieJar
+
+    def __init__(self) -> None:
+        self.jar = CookieJar()
+
+    def serialize(self) -> JSON:
+        return {
+            "version": "tough-cookie@4.0.0",
+            "storeType": "MemoryCookieStore",
+            "rejectPublicSuffixes": True,
+            "cookies": [{
+                **morsel,
+                "key": key,
+                "value": morsel.value,
+            } for key, morsel in self.jar.filter_cookies(ig_url).items()],
+        }
+
+    @classmethod
+    def deserialize(cls, raw: JSON) -> 'Cookies':
+        cookie = SimpleCookie()
+        for item in raw["cookies"]:
+            key = item.pop("key")
+            cookie[key] = item.pop("value")
+            item.pop("hostOnly", None)
+            item.pop("lastAccessed", None)
+            item.pop("creation", None)
+            try:
+                # Morsel.update() is case-insensitive, but not dash-insensitive
+                item["max-age"] = item.pop("maxAge")
+            except KeyError:
+                pass
+            cookie[key].update(item)
+        cookies = cls()
+        cookies.jar.update_cookies(cookie, ig_url)
+        return cookies
+
+    @property
+    def csrf_token(self) -> str:
+        try:
+            return self["csrftoken"]
+        except IGCookieNotFoundError:
+            return "missing"
+
+    @property
+    def user_id(self) -> str:
+        return self["ds_user_id"]
+
+    @property
+    def username(self) -> str:
+        return self["ds_username"]
+
+    def get(self, key: str) -> Morsel:
+        filtered = self.jar.filter_cookies(ig_url)
+        return filtered.get(key)
+
+    def get_value(self, key: str) -> Optional[str]:
+        cookie = self.get(key)
+        return cookie.value if cookie else None
+
+    def __getitem__(self, key: str) -> str:
+        cookie = self.get(key)
+        if not cookie:
+            raise IGCookieNotFoundError(key)
+        return cookie.value

+ 77 - 0
mauigpapi/state/device.py

@@ -0,0 +1,77 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import Optional
+from uuid import UUID
+import pkgutil
+import random
+import string
+import time
+import json
+
+from mautrix.types import SerializableAttrs, field, dataclass
+
+builds = json.loads(pkgutil.get_data("mauigpapi.state", "samples/builds.json"))
+descriptors = json.loads(pkgutil.get_data("mauigpapi.state", "samples/devices.json"))
+
+
+@dataclass
+class AndroidDevice(SerializableAttrs['AndroidDevice']):
+    id: Optional[str] = None
+    descriptor: Optional[str] = None
+    uuid: Optional[str] = None
+    phone_id: Optional[str] = field(default=None, json="phoneId")
+    # Google Play advertising ID
+    adid: Optional[str] = None
+    build: Optional[str] = None
+
+    language: str = "en_US"
+    radio_type: str = "wifi-none"
+    connection_type: str = "WIFI"
+    timezone_offset: str = str(-time.timezone)
+    is_layout_rtl: bool = False
+
+    @property
+    def battery_level(self) -> int:
+        rand = random.Random(self.id)
+        percent_time = rand.randint(200, 600)
+        return 100 - round(time.time() / percent_time) % 100
+
+    @property
+    def is_charging(self) -> bool:
+        rand = random.Random(f"{self.id}{round(time.time() / 10800)}")
+        return rand.choice([True, False])
+
+    @property
+    def payload(self) -> dict:
+        device_parts = self.descriptor.split(";")
+        android_version, android_release, *_ = device_parts[0].split("/")
+        manufacturer, *_ = device_parts[3].split("/")
+        model = device_parts[4]
+        return {
+            "android_version": android_version,
+            "android_release": android_release,
+            "manufacturer": manufacturer,
+            "model": model,
+        }
+
+    def generate(self, seed: str) -> None:
+        rand = random.Random(seed)
+        self.id = f"android-{''.join(rand.choices(string.hexdigits, k=16))}"
+        self.descriptor = rand.choice(descriptors)
+        self.uuid = str(UUID(int=rand.getrandbits(128), version=4))
+        self.phone_id = str(UUID(int=rand.getrandbits(128), version=4))
+        self.adid = str(UUID(int=rand.getrandbits(128), version=4))
+        self.build = rand.choice(builds)

+ 18 - 0
mauigpapi/state/samples/builds.json

@@ -0,0 +1,18 @@
+[
+  "NMF26X",
+  "MMB29M",
+  "MRA58K",
+  "NRD90M",
+  "MRA58K",
+  "OPM1.171019.011",
+  "IMM76L",
+  "JZO54K",
+  "JDQ39",
+  "JLS36I",
+  "KTU84P",
+  "LRX22C",
+  "LMY48M",
+  "MMB29V",
+  "NRD91N",
+  "N2G48C"
+]

+ 1795 - 0
mauigpapi/state/samples/devices.json

@@ -0,0 +1,1795 @@
+[
+  "17/4.2.1; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo K900_ROW; K900_ROW; redhookbay",
+  "19/4.4.2; 480dpi; 1080x1920; GENERAL_MOBILE/GM; Discovery Elite; GENERAL_MOBILE; qcom",
+  "19/4.4.2; 640dpi; 1440x2392; LGE/lge; LG-D855; g3; g3",
+  "21/5.0.1; 480dpi; 1080x1920; samsung; GT-I9500; ja3g; universal5410",
+  "21/5.0.1; 480dpi; 1080x1920; samsung; GT-I9505; jflte; qcom",
+  "21/5.0.1; 480dpi; 1080x1920; samsung; GT-I9515; jfvelte; qcom",
+  "21/5.0.1; 480dpi; 1080x1920; samsung; SAMSUNG-SGH-I537; jactivelteatt; qcom",
+  "21/5.0.1; 640dpi; 1440x2392; LGE/lge; LGLS990; g3; g3",
+  "21/5.0.2; 480dpi; 1080x1920; HTC/htc; HTC One dual sim; m7cdug; m7cdug",
+  "21/5.0.2; 480dpi; 1080x1920; HTC/htc; HTC One; m7; m7",
+  "21/5.0.2; 480dpi; 1080x1920; Xiaomi; Mi 4i; ferrari; qcom",
+  "21/5.0.2; 480dpi; 1080x1920; Xiaomi; Redmi Note 2; hermes; mt6795",
+  "21/5.0.2; 480dpi; 1080x1920; Xiaomi; Redmi Note 3; hennessy; mt6795",
+  "21/5.0; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo K50-T5; K50-T5; mt6752",
+  "21/5.0; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo K50-t5; aio_otfp; mt6752",
+  "21/5.0; 480dpi; 1080x1920; asus; ASUS_Z00AD; Z00A; mofd_v1",
+  "21/5.0; 480dpi; 1080x1920; asus; ASUS_Z00AD; Z00A_3; mofd_v1",
+  "21/5.0; 480dpi; 1080x1920; samsung; SM-G900F; klte; qcom",
+  "21/5.0; 480dpi; 1080x1920; samsung; SM-G900H; k3g; universal5422",
+  "21/5.0; 480dpi; 1080x1920; samsung; SM-N9000Q; ha3g; universal5420",
+  "21/5.0; 480dpi; 1080x1920; samsung; SM-N9005; hlte; qcom",
+  "21/5.0; 480dpi; 1080x1920; samsung; SM-N900; ha3g; universal5420",
+  "21/5.0; 640dpi; 1440x2392; LGE/lge; LG-D855; g3; g3",
+  "22/5.1.1; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo A6020a46; A6020a46; qcom",
+  "22/5.1.1; 480dpi; 1080x1920; Vodafone; Vodafone Smart ultra 6; P839V55; qcom",
+  "22/5.1.1; 480dpi; 1080x1920; Xiaomi; Mi-4c; libra; qcom",
+  "22/5.1.1; 480dpi; 1080x1920; Xiaomi; Redmi Note 3; kenzo; qcom",
+  "22/5.1.1; 480dpi; 1080x1920; samsung; SM-A510F; a5xelte; samsungexynos7580",
+  "22/5.1.1; 480dpi; 1080x1920; vivo; vivo V3Max; V3Max; qcom",
+  "22/5.1.1; 480dpi; 1920x1080; Vodafone; Vodafone Smart ultra 6; P839V55; qcom",
+  "22/5.1.1; 560dpi; 1440x2560; samsung/Verizon; SM-N920V; nobleltevzw; samsungexynos7420",
+  "22/5.1.1; 640dpi; 1440x2560; samsung; SM-G925F; zerolte; samsungexynos7420",
+  "22/5.1; 480dpi; 1080x1920; Elephone; M2; M2; mt6735",
+  "22/5.1; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo K51c78; k5fp; mt6735",
+  "22/5.1; 480dpi; 1080x1920; Meizu; MX5; mx5; mt6795",
+  "22/5.1; 480dpi; 1080x1920; Meizu; m1 note; m1note; mt6752",
+  "22/5.1; 480dpi; 1080x1920; Meizu; m2 note; m2note; mt6735",
+  "22/5.1; 480dpi; 1080x1920; Meizu; m3 note; m3note; mt6755",
+  "22/5.1; 480dpi; 1152x1920; Meizu; MX4; mx4; mt6595",
+  "22/5.1; 640dpi; 1440x2392; LGE/lge; LG-H815; p1; p1",
+  "22/5.1; 640dpi; 1440x2392; LGE/lge; LG-H818N; p1; p1",
+  "23/6.0.1; 420dpi; 1080x1920; Coolpad; C1-U02; C103; qcom",
+  "23/6.0.1; 420dpi; 1080x1920; Coolpad; C103; C103; qcom",
+  "23/6.0.1; 420dpi; 1080x1920; Coolpad; C107-9; cool_c1; qcom",
+  "23/6.0.1; 420dpi; 1080x1920; LeMobile/LeEco; Le X520; le_s2; qcom",
+  "23/6.0.1; 420dpi; 1080x1920; LeMobile/LeEco; Le X522; le_s2_na; qcom",
+  "23/6.0.1; 420dpi; 1080x1920; LeMobile/LeEco; Le X527; le_s2_ww; qcom",
+  "23/6.0.1; 420dpi; 1080x1920; samsung; SM-A520F; a5y17lte; samsungexynos7880",
+  "23/6.0.1; 480dpi; 1080x1920; Coolpad; Coolpad R116; C1-S02; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; Hisense; STARXTREM 6; HS8937QC; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo A6020l36; A6020l36; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo K33b36; K33b36; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo K53a48; K53a48; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo Z90a40; zoom_row; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; OnePlus/oneplus; A0001; A0001; bacon",
+  "23/6.0.1; 480dpi; 1080x1920; OnePlus; ONE A2003; OnePlus2; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; OnePlus; ONE E1003; OnePlus; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; Vodafone; Vodafone Smart ultra 6; P839V55; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; Xiaomi/xiaomi; Redmi Note 4; mido; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; Xiaomi; MI 4W; cancro; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; Xiaomi; MI 5s; capricorn; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; Xiaomi; Redmi 4; markw; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; Xiaomi; Redmi Note 3; kate; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; Xiaomi; Redmi Note 3; kenzo; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; asus; ASUS_X00DD; ASUS_X00DD; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; asus; ASUS_Z00AD; Z00A; mofd_v1",
+  "23/6.0.1; 480dpi; 1080x1920; asus; ASUS_Z00AD; Z00A_1; mofd_v1",
+  "23/6.0.1; 480dpi; 1080x1920; asus; ASUS_Z00UD; ASUS_Z00U_1; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; asus; ASUS_Z011D; ASUS_Z011; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; bq; Aquaris M5; Aquaris_M5; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; samsung/Verizon; SM-G900V; kltevzw; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SAMSUNG-SM-G900A; klteatt; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-A510F; a5xelte; samsungexynos7580",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-A520F; a5y17lte; samsungexynos7880",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-A700FD; a7lte; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-G610M; on7xelte; samsungexynos7870",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-G900F; klte; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-G900H; k3g; universal5422",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-G900L; kltelgt; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-G900M; klte; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-G900P; kltespr; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-G900T3; kltetmo; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-G900T; kltetmo; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-G901F; kccat6; qcom",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-G903F; s5neolte; samsungexynos7580",
+  "23/6.0.1; 480dpi; 1080x1920; samsung; SM-G903M; s5neolte; samsungexynos7580",
+  "23/6.0.1; 480dpi; 1920x1080; Xiaomi/xiaomi; Redmi Note 4; mido; qcom",
+  "23/6.0.1; 480dpi; 1920x1080; Xiaomi; Redmi 4; markw; qcom",
+  "23/6.0.1; 480dpi; 1920x1080; nubia; NX551J; NX551J; qcom",
+  "23/6.0.1; 480dpi; 1920x1080; samsung; SM-A510F; a5xelte; samsungexynos7580",
+  "23/6.0.1; 480dpi; 1920x1080; samsung; SM-G901F; kccat6; qcom",
+  "23/6.0.1; 560dpi; 1440x2560; LeMobile/LeEco; Le X829; le_x2_na; qcom",
+  "23/6.0.1; 560dpi; 1440x2560; samsung; SM-G928F; zenlte; samsungexynos7420",
+  "23/6.0.1; 560dpi; 1440x2560; samsung; SM-G935T; hero2qltetmo; qcom",
+  "23/6.0.1; 640dpi; 1440x2368; TCL; 6070K; idol4s; qcom",
+  "23/6.0.1; 640dpi; 1440x2368; Vodafone; VFD 900; VFD900; qcom",
+  "23/6.0.1; 640dpi; 1440x2560; HTC/htc; HTC 10; htc_pmewhl; htc_pme",
+  "23/6.0.1; 640dpi; 1440x2560; motorola; XT1254; quark; qcom",
+  "23/6.0.1; 640dpi; 1440x2560; samsung/Verizon; SM-G935F; hero2qltevzw; qcom",
+  "23/6.0.1; 640dpi; 1440x2560; samsung/Verizon; SM-N910V; trltevzw; qcom",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SAMSUNG-SM-G890A; marinelteatt; samsungexynos7420",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SAMSUNG-SM-G920A; zeroflteatt; samsungexynos7420",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SAMSUNG-SM-G935A; hero2qlteatt; qcom",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-G920F; zeroflte; samsungexynos7420",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-G925F; zerolte; samsungexynos7420",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-G925I; zerolte; samsungexynos7420",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-G930F; herolte; samsungexynos8890",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-G930T; heroqltetmo; qcom",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-G935F; hero2lte; samsungexynos8890",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-N910C; trelte; universal5433",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-N910F; trlte; qcom",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-N910H; tre3g; universal5433",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-N910K; treltektt; universal5433",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-N910L; treltelgt; universal5433",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-N910U; trhplte; universal5433",
+  "23/6.0.1; 640dpi; 1440x2560; samsung; SM-N910W8; trltecan; qcom",
+  "23/6.0.1; 640dpi; 1532x2560; samsung; SM-N915FY; tblte; qcom",
+  "23/6.0; 420dpi; 1080x1920; LeMobile/LeEco; LEX626; le_x6; mt6797",
+  "23/6.0; 440dpi; 2048x2048; HUAWEI; BLL-L21; HWBLN-H; hi6250",
+  "23/6.0; 480dpi; 1080x1920; Elephone; P8000 6.0; k06te_a; mt6735",
+  "23/6.0; 480dpi; 1080x1920; Elephone; S7; S7_4g; mt6797",
+  "23/6.0; 480dpi; 1080x1920; HTC/htc; HTC One X10; htc_e66_dugl; mt6755",
+  "23/6.0; 480dpi; 1080x1920; HTC/htc; HTC One X9 dual sim; htc_e56ml_dtul; mt6795",
+  "23/6.0; 480dpi; 1080x1920; HTC/htc; HTC U Play; htc_alpine_dugl; mt6755",
+  "23/6.0; 480dpi; 1080x1920; InnJoo; InnJoo 3; InnJoo 3; mt6755",
+  "23/6.0; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo A7010a48; A7010a48; mt6735",
+  "23/6.0; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo K50-t5; K50-t5; mt6752",
+  "23/6.0; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo K52e78; k52_e78; mt6755",
+  "23/6.0; 480dpi; 1080x1920; Meizu; M5 Note; M5Note; mt6755",
+  "23/6.0; 480dpi; 1080x1920; Meizu; M5 Note; m1621; mt6755",
+  "23/6.0; 480dpi; 1080x1920; Meizu; MX6; MX6; mt6797",
+  "23/6.0; 480dpi; 1080x1920; Meizu; PRO 6; PRO6; mt6797",
+  "23/6.0; 480dpi; 1080x1920; Meizu; U20; U20; mt6755",
+  "23/6.0; 480dpi; 1080x1920; OPPO; CPH1609; CPH1609; mt6755",
+  "23/6.0; 480dpi; 1080x1920; OUKITEL; U20 Plus; pri6737t_66_m0; mt6735",
+  "23/6.0; 480dpi; 1080x1920; TCL; 5095K; shine_plus; mt6755",
+  "23/6.0; 480dpi; 1080x1920; UMI TOUCH/UMI; UMI TOUCH X; UMI_TOUCH_X; mt6735",
+  "23/6.0; 480dpi; 1080x1920; Vodafone; VFD 700; VFD700; mt6755",
+  "23/6.0; 480dpi; 1080x1920; Xiaomi; MI 5; gemini; qcom",
+  "23/6.0; 480dpi; 1080x1920; Xiaomi; Redmi Note 4; nikel; mt6797",
+  "23/6.0; 480dpi; 1080x1920; Xiaomi; Redmi Note 4X; nikel; mt6797",
+  "23/6.0; 480dpi; 1080x1920; Xiaomi; Redmi Pro; omega; mt6797",
+  "23/6.0; 480dpi; 1080x1920; nubia; NX541J; NX541J; mt6755",
+  "23/6.0; 480dpi; 1920x1080; Xiaomi; Redmi Pro; omega; mt6797",
+  "23/6.0; 560dpi; 1440x2413; LGE/lge; LG-D855; g3; g3",
+  "23/6.0; 640dpi; 1440x2368; vernee; Apollo; k15ta_a; mt6797",
+  "23/6.0; 640dpi; 1440x2392; LGE/lge; LG-D855; g3; g3",
+  "23/6.0; 640dpi; 1440x2392; LGE/lge; LG-H815; p1; p1",
+  "23/6.0; 640dpi; 1440x2392; LGE/lge; VS986; p1; p1",
+  "23/6.0; 640dpi; 1440x2392; LGE/lge; VS990; pplus; pplus",
+  "23/6.0; 640dpi; 1440x2560; vivo; vivo Xplay5S; PD1516A; qcom",
+  "23/6.0; 640dpi; 2392x1440; LGE/lge; LG-H815; p1; p1",
+  "24/7.0; 420dpi; 1080x1920; samsung/Verizon; SM-G930V; heroqltevzw; qcom",
+  "24/7.0; 420dpi; 1080x1920; samsung/Verizon; SM-N920V; nobleltevzw; samsungexynos7420",
+  "24/7.0; 420dpi; 1080x1920; samsung; SAMSUNG-SM-N920A; noblelteatt; samsungexynos7420",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-A520F; a5y17lte; samsungexynos7880",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-A720F; a7y17lte; samsungexynos7880",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-A910F; a9xproltesea; qcom",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-C7000; c7ltechn; qcom",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-G9287; zenlte; samsungexynos7420",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-G928F; zenlte; samsungexynos7420",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-G928T; zenltetmo; samsungexynos7420",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-G930F; herolte; samsungexynos8890",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-G935F; hero2lte; samsungexynos8890",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-J730F; j7y17lte; samsungexynos7870",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-J730FM; j7y17lte; samsungexynos7870",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-N920C; noblelte; samsungexynos7420",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-N920G; noblelte; samsungexynos7420",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-N920I; noblelte; samsungexynos7420",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-N920P; nobleltespr; samsungexynos7420",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-N920S; noblelteskt; samsungexynos7420",
+  "24/7.0; 420dpi; 1080x1920; samsung; SM-N920T; nobleltetmo; samsungexynos7420",
+  "24/7.0; 420dpi; 1080x2094; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "24/7.0; 420dpi; 1080x2094; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "24/7.0; 420dpi; 1080x2094; samsung; SM-G955U; dream2qltesq; qcom",
+  "24/7.0; 420dpi; 1920x1080; samsung; SM-J730F; j7y17lte; samsungexynos7870",
+  "24/7.0; 420dpi; 1920x1080; samsung; SM-J730FM; j7y17lte; samsungexynos7870",
+  "24/7.0; 440dpi; 1080x1920; Xiaomi; MI MAX; helium; qcom",
+  "24/7.0; 440dpi; 1080x1920; Xiaomi; MI MAX; hydrogen; qcom",
+  "24/7.0; 440dpi; 1080x1920; Xiaomi; Mi Note 2; scorpio; qcom",
+  "24/7.0; 440dpi; 1080x2050; HUAWEI; RNE-L21; HWRNE; hi6250",
+  "24/7.0; 480dpi; 1080x1920; Blackview; BV7000; BV7000; mt6735",
+  "24/7.0; 480dpi; 1080x1920; Blackview; BV8000Pro; BV8000Pro; mt6757",
+  "24/7.0; 480dpi; 1080x1920; GIONEE; A1; GIONEE_SWW1609; mt6755",
+  "24/7.0; 480dpi; 1080x1920; HUAWEI/HONOR; STF-L09; HWSTF; hi3660",
+  "24/7.0; 480dpi; 1080x1920; HUAWEI; VTR-L09; HWVTR; hi3660",
+  "24/7.0; 480dpi; 1080x1920; HUAWEI; VTR-L29; HWVTR; hi3660",
+  "24/7.0; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo K33a42; K33a42; qcom",
+  "24/7.0; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo K33b36; K33b36; qcom",
+  "24/7.0; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo K53a48; K53a48; qcom",
+  "24/7.0; 480dpi; 1080x1920; Meizu; M5 Note; M5Note; mt6755",
+  "24/7.0; 480dpi; 1080x1920; Meizu; PRO 7; PRO7S; mt6757",
+  "24/7.0; 480dpi; 1080x1920; SNCOIA/Qilive; Q8S6IN4G; Q8S6IN4G; mt6735",
+  "24/7.0; 480dpi; 1080x1920; Xiaomi/xiaomi; Redmi Note 4; mido; qcom",
+  "24/7.0; 480dpi; 1080x1920; Xiaomi/xiaomi; Redmi Note 4X; mido; qcom",
+  "24/7.0; 480dpi; 1080x1920; Xiaomi; MI 5; gemini; qcom",
+  "24/7.0; 480dpi; 1080x1920; Xiaomi; MI 5s; capricorn; qcom",
+  "24/7.0; 480dpi; 1080x1920; ZTE; ZTE BLADE V0800; ZTE_BLADE_V0800; qcom",
+  "24/7.0; 480dpi; 1080x1920; asus; ASUS_Z012DE; ASUS_Z012D; qcom",
+  "24/7.0; 480dpi; 1080x1920; asus; ASUS_Z017D; ASUS_Z017D_1; qcom",
+  "24/7.0; 480dpi; 1080x1920; asus; ASUS_Z017DC; ASUS_Z017D_1; qcom",
+  "24/7.0; 480dpi; 1080x1920; motorola; Moto G (5 Plus; potter_n; qcom",
+  "24/7.0; 480dpi; 1080x1920; samsung/Verizon; SM-G930V; heroqltevzw; qcom",
+  "24/7.0; 480dpi; 1080x1920; samsung/Verizon; SM-G935V; hero2qltevzw; qcom",
+  "24/7.0; 480dpi; 1080x1920; samsung/Verizon; SM-N920V; nobleltevzw; samsungexynos7420",
+  "24/7.0; 480dpi; 1080x1920; samsung; SAMSUNG-SM-G891A; poseidonlteatt; qcom",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-A510F; a5xelte; samsungexynos7580",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-A510M; a5xelte; samsungexynos7580",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-A520F; a5y17lte; samsungexynos7880",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-A710F; a7xelte; samsungexynos7580",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-A720F; a7y17lte; samsungexynos7880",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-G610M; on7xelte; samsungexynos7870",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-G930F; hero2qltetmo; qcom",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-G930F; herolte; samsungexynos8890",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-G930P; heroqltespr; qcom",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-G930U; heroqlteue; qcom",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-G930VL; heroqltetfnvzw; qcom",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-G935F; hero2lte; samsungexynos8890",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-J730F; j7y17lte; samsungexynos7870",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-J730FM; j7y17lte; samsungexynos7870",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-N920P; nobleltespr; samsungexynos7420",
+  "24/7.0; 480dpi; 1080x1920; samsung; SM-N920T; nobleltetmo; samsungexynos7420",
+  "24/7.0; 480dpi; 1080x2004; asus; ASUS_X018D; ASUS_X018_4; mt6755",
+  "24/7.0; 480dpi; 1080x2016; vernee; MIX 2; MIX_2; mt6757",
+  "24/7.0; 480dpi; 1080x2016; vkworld; S8; S8; mt6755",
+  "24/7.0; 480dpi; 1080x2040; HUAWEI; RNE-L21; HWRNE; hi6250",
+  "24/7.0; 480dpi; 1080x2076; samsung; SM-G892A; cruiserlteatt; qcom",
+  "24/7.0; 480dpi; 1080x2076; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "24/7.0; 480dpi; 1080x2076; samsung; SM-G950U; dreamqltesq; qcom",
+  "24/7.0; 480dpi; 1600x2560; HUAWEI; BTV-DL09; hwbeethoven; hi3650",
+  "24/7.0; 480dpi; 1920x1080; Xiaomi/xiaomi; Redmi Note 4; mido; qcom",
+  "24/7.0; 480dpi; 1920x1080; samsung; SM-A510F; a5xelte; samsungexynos7580",
+  "24/7.0; 480dpi; 1920x1080; samsung; SM-A520F; a5y17lte; samsungexynos7880",
+  "24/7.0; 480dpi; 1920x1080; samsung; SM-G930F; herolte; samsungexynos8890",
+  "24/7.0; 480dpi; 1920x1080; samsung; SM-G935F; hero2lte; samsungexynos8890",
+  "24/7.0; 540dpi; 1080x1920; asus; ASUS_Z012D; ASUS_Z012D; qcom",
+  "24/7.0; 540dpi; 1080x1920; asus; ASUS_Z017DC; ASUS_Z017D_1; qcom",
+  "24/7.0; 540dpi; 1080x1920; samsung; SM-A510F; a5xelte; samsungexynos7580",
+  "24/7.0; 540dpi; 1080x1920; samsung; SM-G935F; hero2lte; samsungexynos8890",
+  "24/7.0; 540dpi; 1080x2058; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "24/7.0; 544dpi; 1440x2560; HTC/htc; 2PYB2; htc_acawhl; htc_aca",
+  "24/7.0; 544dpi; 1440x2560; Meizu; PRO 7 Plus; PRO7Plus; mt6799",
+  "24/7.0; 560dpi; 1440x2392; motorola; XT1572; clark; qcom",
+  "24/7.0; 560dpi; 1440x2560; samsung/Verizon; SM-N920V; nobleltevzw; samsungexynos7420",
+  "24/7.0; 560dpi; 1440x2560; samsung; SM-G920F; zeroflte; samsungexynos7420",
+  "24/7.0; 560dpi; 1440x2560; samsung; SM-G925F; zerolte; samsungexynos7420",
+  "24/7.0; 560dpi; 1440x2560; samsung; SM-G928F; zenlte; samsungexynos7420",
+  "24/7.0; 560dpi; 1440x2560; samsung; SM-G928G; zenlte; samsungexynos7420",
+  "24/7.0; 560dpi; 1440x2560; samsung; SM-G930F; herolte; samsungexynos8890",
+  "24/7.0; 560dpi; 1440x2560; samsung; SM-G935F; hero2lte; samsungexynos8890",
+  "24/7.0; 560dpi; 1440x2792; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "24/7.0; 640dpi; 1440x2368; motorola; XT1585; kinzie; qcom",
+  "24/7.0; 640dpi; 1440x2392; LGE/lge; LG-H840; alicee; alicee",
+  "24/7.0; 640dpi; 1440x2392; LGE/lge; LG-H850; h1; h1",
+  "24/7.0; 640dpi; 1440x2392; LGE/lge; LG-H901; pplus; pplus",
+  "24/7.0; 640dpi; 1440x2392; LGE/lge; LGLS992; h1; h1",
+  "24/7.0; 640dpi; 1440x2416; HUAWEI; VKY-L29; HWVKY; hi3660",
+  "24/7.0; 640dpi; 1440x2560; HTC/htc; 2PYB2; htc_acawhl; htc_aca",
+  "24/7.0; 640dpi; 1440x2560; HUAWEI; VKY-L09; HWVKY; hi3660",
+  "24/7.0; 640dpi; 1440x2560; HUAWEI; VKY-L29; HWVKY; hi3660",
+  "24/7.0; 640dpi; 1440x2560; Meizu; PRO 7 Plus; PRO7Plus; mt6799",
+  "24/7.0; 640dpi; 1440x2560; samsung/Verizon; SM-G920V; zerofltevzw; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung/Verizon; SM-G925V; zeroltevzw; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung/Verizon; SM-G930V; heroqltevzw; qcom",
+  "24/7.0; 640dpi; 1440x2560; samsung; SAMSUNG-SM-G890A; marinelteatt; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung; SAMSUNG-SM-G920A; zeroflteatt; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung; SAMSUNG-SM-G925A; zerolteatt; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung; SM-G920F; zeroflte; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung; SM-G920K; zerofltektt; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung; SM-G920P; zerofltespr; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung; SM-G920T; zerofltetmo; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung; SM-G920W8; zerofltebmc; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung; SM-G9250; zeroltechn; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung; SM-G925F; zerolte; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung; SM-G925I; zerolte; samsungexynos7420",
+  "24/7.0; 640dpi; 1440x2560; samsung; SM-G930F; herolte; samsungexynos8890",
+  "24/7.0; 640dpi; 1440x2560; samsung; SM-G935F; hero2lte; samsungexynos8890",
+  "24/7.0; 640dpi; 1440x2672; LGE/lge; LG-H870; lucye; lucye",
+  "24/7.0; 640dpi; 1440x2672; LGE/lge; LG-H870DS; lucye; lucye",
+  "24/7.0; 640dpi; 1440x2672; LGE/lge; LG-H871; lucye; lucye",
+  "24/7.0; 640dpi; 1440x2768; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "24/7.0; 640dpi; 2368x1440; motorola; XT1585; kinzie; qcom",
+  "24/7.0; 640dpi; 2392x1440; LGE/lge; LG-H990; elsa; elsa",
+  "24/7.0; 640dpi; 2560x1440; samsung; SM-G920F; zeroflte; samsungexynos7420",
+  "24/7.0; 640dpi; 2560x1440; samsung; SM-G925F; zerolte; samsungexynos7420",
+  "24/7.0; 720dpi; 1440x2560; samsung; SAMSUNG-SM-G920A; zeroflteatt; samsungexynos7420",
+  "24/7.0; 720dpi; 1440x2560; samsung; SM-G920F; zeroflte; samsungexynos7420",
+  "24/7.0; 720dpi; 1440x2560; samsung; SM-G920P; zerofltespr; samsungexynos7420",
+  "25/7.1.1; 420dpi; 1080x1920; OnePlus; ONEPLUS A3003; OnePlus3; qcom",
+  "25/7.1.1; 420dpi; 1080x1920; OnePlus; ONEPLUS A5000; OnePlus5; qcom",
+  "25/7.1.1; 420dpi; 1080x2034; OnePlus; ONEPLUS A5010; OnePlus5T; qcom",
+  "25/7.1.1; 420dpi; 1080x2094; samsung; SM-A530F; jackpotlte; samsungexynos7885",
+  "25/7.1.1; 420dpi; 1080x2094; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "25/7.1.1; 420dpi; 1080x2220; samsung; SM-A730F; jackpot2lte; samsungexynos7885",
+  "25/7.1.1; 420dpi; 1080x2220; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "25/7.1.1; 440dpi; 1080x1920; Xiaomi; MI MAX 2; oxygen; qcom",
+  "25/7.1.1; 440dpi; 1080x1920; Xiaomi; Mi Note 3; jason; qcom",
+  "25/7.1.1; 476dpi; 1440x2560; Elephone; S8; l9708_4g; mt6797",
+  "25/7.1.1; 480dpi; 1080x1920; Coolpad; C106; cool_c1; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; Elephone; S7; s7_4g; mt6797",
+  "25/7.1.1; 480dpi; 1080x1920; Meizu; PRO 6; PRO6; mt6797",
+  "25/7.1.1; 480dpi; 1080x1920; OPPO; OPPO R11t; R11; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; ZTE; N9560; bolton; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; ZTE; Z982; crocus; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; ZTE; Z983; stollen; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; asus; ASUS_X00DD; ASUS_X00DD; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; asus; ASUS_Z01BD; Z01B_1; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; asus; ASUS_Z01MD; ASUS_Z01M_1; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; asus; ASUS_Z01MDA; ASUS_Z01M_1; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; asus; ZD552KL; ASUS_Z01M_1; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; bq; Aquaris X Pro; bardock-pro; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; bq; Aquaris X5 Plus; Aquaris_X5_Plus; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; bq; Aquaris X; bardock; qcom",
+  "25/7.1.1; 480dpi; 1080x1920; samsung; SM-A5100; a5xltechn; qcom",
+  "25/7.1.1; 480dpi; 1080x2004; LGE/lge; LG-M700; mh; mh",
+  "25/7.1.1; 480dpi; 1080x2004; LGE/lge; LG-M700; mhn; mh",
+  "25/7.1.1; 480dpi; 1080x2004; asus; ASUS_X017DA; ASUS_X017D_2; qcom",
+  "25/7.1.1; 480dpi; 1080x2016; OPPO; CPH1723; CPH1723; mt6763",
+  "25/7.1.1; 480dpi; 1080x2076; samsung; SM-A530F; jackpotlte; samsungexynos7885",
+  "25/7.1.1; 480dpi; 1080x2160; OPPO; CPH1723; CPH1723; mt6763",
+  "25/7.1.1; 480dpi; 1080x2220; samsung; SM-A530F; jackpotlte; samsungexynos7885",
+  "25/7.1.1; 540dpi; 1080x1920; Yulong/Coolpad; REVVLPLUS C3701A; alchemy; qcom",
+  "25/7.1.1; 540dpi; 1080x1984; LGE/lge; LG-M700; mh; mh",
+  "25/7.1.1; 540dpi; 1080x2220; samsung; SM-A530F; jackpotlte; samsungexynos7885",
+  "25/7.1.1; 560dpi; 1440x2392; motorola/google; Nexus 6; shamu; shamu",
+  "25/7.1.1; 560dpi; 1440x2560; Elephone; S8; l9708_4g; mt6797",
+  "25/7.1.1; 640dpi; 1440x2368; motorola; Moto Z (2; nash; qcom",
+  "25/7.1.2; 440dpi; 1080x2030; Xiaomi/xiaomi; Redmi 5 Plus; vince; qcom",
+  "25/7.1.2; 480dpi; 1080x1920; KYOCERA; E6810; E6810_3GB; qcom",
+  "25/7.1.2; 480dpi; 1080x1920; Meizu; M6 Note; M6Note; qcom",
+  "25/7.1.2; 480dpi; 1080x1920; OnePlus/oneplus; A0001; A0001; bacon",
+  "25/7.1.2; 480dpi; 1080x1920; Xiaomi/xiaomi; MI 5X; tiffany; qcom",
+  "25/7.1.2; 560dpi; 1440x2733; LGE/lge; VS996; joan; joan",
+  "25/7.1; 640dpi; 1440x2560; Sony; SO-02G; SO-02G; SO-02G",
+  "26/8.0.0; 420dpi; 1080x1920; BullittGroupLimited/Cat; S41; CatS41; mt6757",
+  "26/8.0.0; 420dpi; 1080x1920; OnePlus; ONEPLUS A3003; OnePlus3; qcom",
+  "26/8.0.0; 420dpi; 1080x1920; OnePlus; ONEPLUS A3003; OnePlus3T; qcom",
+  "26/8.0.0; 420dpi; 1080x1920; OnePlus; ONEPLUS A3010; OnePlus3T; qcom",
+  "26/8.0.0; 420dpi; 1080x1920; asus; ASUS_A006; ASUS_A006; qcom",
+  "26/8.0.0; 420dpi; 1080x1920; samsung; SM-A520F; a5y17lte; samsungexynos7880",
+  "26/8.0.0; 420dpi; 1080x1920; samsung; SM-A720F; a7y17lte; samsungexynos7880",
+  "26/8.0.0; 420dpi; 1080x1920; samsung; SM-A910F; a9xproltesea; qcom",
+  "26/8.0.0; 420dpi; 1080x1920; samsung; SM-C5000; c5ltechn; qcom",
+  "26/8.0.0; 420dpi; 1080x1920; samsung; SM-C7000; c7ltechn; qcom",
+  "26/8.0.0; 420dpi; 1080x1920; samsung; SM-C900F; c9lte; qcom",
+  "26/8.0.0; 420dpi; 1080x1920; samsung; SM-G930F; herolte; samsungexynos8890",
+  "26/8.0.0; 420dpi; 1080x1920; samsung; SM-G935F; hero2lte; samsungexynos8890",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-A605FN; a6plte; qcom",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-A605GN; a6plte; qcom",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-A730F; jackpot2lte; samsungexynos7885",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-A750FN; a7y18lte; samsungexynos7885",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-A920F; a9y18qlte; qcom",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-G950U1; dreamqlteue; qcom",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-G955U; dream2qltesq; qcom",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-G965U; star2qltesq; qcom",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "26/8.0.0; 420dpi; 1080x2094; samsung; SM-N950U; greatqlte; qcom",
+  "26/8.0.0; 420dpi; 1080x2220; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "26/8.0.0; 420dpi; 1080x2220; samsung; SM-G965U; star2qltesq; qcom",
+  "26/8.0.0; 420dpi; 1920x1080; OnePlus; ONEPLUS A3010; OnePlus3T; qcom",
+  "26/8.0.0; 420dpi; 2094x1080; samsung; SM-N950U; greatqlte; qcom",
+  "26/8.0.0; 440dpi; 1080x1920; Xiaomi; MI 5s Plus; natrium; qcom",
+  "26/8.0.0; 440dpi; 1080x1920; Xiaomi; Mi Note 2; scorpio; qcom",
+  "26/8.0.0; 440dpi; 1080x2050; HUAWEI; RNE-L21; HWRNE; hi6250",
+  "26/8.0.0; 440dpi; 1080x2050; HUAWEI; RNE-L23; HWRNE; hi6250",
+  "26/8.0.0; 480dpi; 1080x1920; HMD Global/Nokia; TA-1021; PLE; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; HUAWEI/HONOR; STF-L09; HWSTF; hi3660",
+  "26/8.0.0; 480dpi; 1080x1920; HUAWEI; VTR-L09; HWVTR; hi3660",
+  "26/8.0.0; 480dpi; 1080x1920; LENOVO/Lenovo; Lenovo K8 Plus; marino_f; mt6757",
+  "26/8.0.0; 480dpi; 1080x1920; OnePlus; ONEPLUS A3003; OnePlus3; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; OnePlus; ONEPLUS A3003; OnePlus3T; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; OnePlus; ONEPLUS A5000; OnePlus5; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; Xiaomi/xiaomi; Mi A1; tissot_sprout; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; Xiaomi; MI 5; gemini; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; Xiaomi; MI 5s; capricorn; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; Xiaomi; MI 6; sagit; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; asus; ASUS_Z012D; ASUS_Z012D; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; asus; ASUS_Z012DC; ASUS_Z012D; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; asus; ASUS_Z012S; ASUS_Z012D; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; asus; ASUS_Z017D; ASUS_Z017D_1; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; asus; ASUS_Z017DC; ASUS_Z017D_1; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; asus; ASUS_Z01KD; ASUS_Z01KD_2; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; asus; ASUS_Z01KD; ASUS_Z01KD_3; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; asus; ASUS_Z01KDA; ASUS_Z01KD_1; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; asus; ZE520KL; ASUS_Z017D_1; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; asus; ZE553KL; ASUS_Z01H_1; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; lenovo; Lenovo K8 Note; manning; mt6797",
+  "26/8.0.0; 480dpi; 1080x1920; motorola; Moto Z2 Play; albus; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung/Verizon; SM-G930V; heroqltevzw; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung/Verizon; SM-G935V; hero2qltevzw; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SAMSUNG-SM-G891A; poseidonlteatt; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SAMSUNG-SM-G930A; heroqlteatt; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SAMSUNG-SM-G935A; hero2qlteatt; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-A520F; a5y17lte; samsungex",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-A520F; a5y17lte; samsungexynos7880",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-A720F; a7y17lte; samsungexynos7880",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-G930F; herolte; samsungexynos8890",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-G930P; heroqltespr; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-G930R4; heroqlteusc; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-G930T; heroqltetmo; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-G930U; heroqlteue; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-G930VL; heroqltetfnvzw; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-G935F; hero2lte; samsungexynos8890",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-G935P; hero2qltespr; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-G935R4; hero2qlteusc; qcom",
+  "26/8.0.0; 480dpi; 1080x1920; samsung; SM-G935T; hero2qltetmo; qcom",
+  "26/8.0.0; 480dpi; 1080x2016; Lenovo; Lenovo K520; seoul; qcom",
+  "26/8.0.0; 480dpi; 1080x2016; motorola; moto g(6 plus; evert_nt; qcom",
+  "26/8.0.0; 480dpi; 1080x2016; motorola; moto g(6; ali; qcom",
+  "26/8.0.0; 480dpi; 1080x2028; TCL; 5099D_RU; A3A_XL_4G; mt6735",
+  "26/8.0.0; 480dpi; 1080x2028; TCL; 5099Y; A3A_XL_4G; mt6735",
+  "26/8.0.0; 480dpi; 1080x2032; HUAWEI/HONOR; LLD-L31; HWLLD-H; hi6250",
+  "26/8.0.0; 480dpi; 1080x2032; HUAWEI; FIG-LX1; HWFIG-H; hi6250",
+  "26/8.0.0; 480dpi; 1080x2038; HUAWEI/HONOR; BND-L21; HWBND-H; hi6250",
+  "26/8.0.0; 480dpi; 1080x2038; HUAWEI; BND-L34; HWBND-H; hi6250",
+  "26/8.0.0; 480dpi; 1080x2040; HUAWEI; BLA-L29; HWBLA; kirin970",
+  "26/8.0.0; 480dpi; 1080x2040; HUAWEI; RNE-L01; HWRNE; hi6250",
+  "26/8.0.0; 480dpi; 1080x2040; HUAWEI; RNE-L21; HWRNE; hi6250",
+  "26/8.0.0; 480dpi; 1080x2076; samsung; SM-A530F; jackpotlte; samsungexynos7885",
+  "26/8.0.0; 480dpi; 1080x2076; samsung; SM-G892A; cruiserlteatt; qcom",
+  "26/8.0.0; 480dpi; 1080x2076; samsung; SM-G892U; cruiserltesq; qcom",
+  "26/8.0.0; 480dpi; 1080x2076; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "26/8.0.0; 480dpi; 1080x2076; samsung; SM-G950U1; dreamqlteue; qcom",
+  "26/8.0.0; 480dpi; 1080x2076; samsung; SM-G950U; dreamqltesq; qcom",
+  "26/8.0.0; 480dpi; 1080x2076; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "26/8.0.0; 480dpi; 1080x2076; samsung; SM-G960F; starlte; samsungexynos9810",
+  "26/8.0.0; 480dpi; 1080x2076; samsung; SM-G960U; starqltesq; qcom",
+  "26/8.0.0; 480dpi; 1080x2076; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "26/8.0.0; 480dpi; 1080x2150; HUAWEI; ANE-LX1; HWANE; hi6250",
+  "26/8.0.0; 480dpi; 1080x2150; HUAWEI; ANE-LX2; HWANE; hi6250",
+  "26/8.0.0; 480dpi; 1080x2150; HUAWEI; ANE-LX3; HWANE; hi6250",
+  "26/8.0.0; 480dpi; 1080x2220; samsung; SM-A530F; jackpotlte; samsungexynos7885",
+  "26/8.0.0; 480dpi; 1080x2220; samsung; SM-G892A; cruiserlteatt; qcom",
+  "26/8.0.0; 480dpi; 1080x2220; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "26/8.0.0; 480dpi; 1080x2220; samsung; SM-G950U1; dreamqlteue; qcom",
+  "26/8.0.0; 480dpi; 1080x2220; samsung; SM-G950U; dreamqltesq; qcom",
+  "26/8.0.0; 480dpi; 1080x2220; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "26/8.0.0; 480dpi; 1080x2220; samsung; SM-G960F; starlte; samsungexynos9810",
+  "26/8.0.0; 480dpi; 1080x2220; samsung; SM-G960U; starqltesq; qcom",
+  "26/8.0.0; 480dpi; 1080x2280; HUAWEI; ANE-LX1; HWANE; hi6250",
+  "26/8.0.0; 480dpi; 1920x1080; Xiaomi/xiaomi; Mi A1; tissot_sprout; qcom",
+  "26/8.0.0; 480dpi; 1920x1080; samsung/Verizon; SM-G930V; heroqltevzw; qcom",
+  "26/8.0.0; 480dpi; 1920x1080; samsung; SM-A520F; a5y17lte; samsungexynos7880",
+  "26/8.0.0; 480dpi; 2033x1080; HUAWEI; FIG-LX1; HWFIG-H; hi6250",
+  "26/8.0.0; 540dpi; 1080x1920; asus; ASUS_Z012DC; ASUS_Z012D; qcom",
+  "26/8.0.0; 540dpi; 1080x1920; samsung/Verizon; SM-G930V; heroqltevzw; qcom",
+  "26/8.0.0; 540dpi; 1080x1920; samsung; SAMSUNG-SM-G891A; poseidonlteatt; qcom",
+  "26/8.0.0; 540dpi; 1080x1920; samsung; SM-A520F; a5y17lte; samsungexynos7880",
+  "26/8.0.0; 540dpi; 1080x1920; samsung; SM-A720F; a7y17lte; samsungexynos7880",
+  "26/8.0.0; 540dpi; 1080x1920; samsung; SM-A910F; a9xproltesea; qcom",
+  "26/8.0.0; 540dpi; 1080x1920; samsung; SM-G930F; herolte; samsungexynos8890",
+  "26/8.0.0; 540dpi; 1080x1920; samsung; SM-G930P; heroqltespr; qcom",
+  "26/8.0.0; 540dpi; 1080x1920; samsung; SM-G930T; heroqltetmo; qcom",
+  "26/8.0.0; 540dpi; 1080x1920; samsung; SM-G935F; hero2lte; samsungexynos8890",
+  "26/8.0.0; 540dpi; 1080x2032; HUAWEI/HONOR; LLD-L31; HWLLD-H; hi6250",
+  "26/8.0.0; 540dpi; 1080x2058; samsung; SM-A530F; jackpotlte; samsungexynos7885",
+  "26/8.0.0; 540dpi; 1080x2058; samsung; SM-A605FN; a6plte; qcom",
+  "26/8.0.0; 540dpi; 1080x2058; samsung; SM-A750FN; a7y18lte; samsungexynos7885",
+  "26/8.0.0; 540dpi; 1080x2058; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "26/8.0.0; 540dpi; 1080x2058; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "26/8.0.0; 560dpi; 1440x2400; HUAWEI; VKY-L09; HWVKY; hi3660",
+  "26/8.0.0; 560dpi; 1440x2413; LGE/lge; LG-H820; h1; h1",
+  "26/8.0.0; 560dpi; 1440x2413; LGE/lge; VS995; elsa; elsa",
+  "26/8.0.0; 560dpi; 1440x2560; samsung; SAMSUNG-SM-G930A; heroqlteatt; qcom",
+  "26/8.0.0; 560dpi; 1440x2560; samsung; SM-G930F; herolte; samsungexynos8890",
+  "26/8.0.0; 560dpi; 1440x2560; samsung; SM-G935F; hero2lte; samsungexynos8890",
+  "26/8.0.0; 560dpi; 1440x2712; LGE/lge; LM-V350; judyp; judyp",
+  "26/8.0.0; 560dpi; 1440x2733; LGE/lge; LG-US998; joan; joan",
+  "26/8.0.0; 560dpi; 1440x2792; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "26/8.0.0; 560dpi; 1440x2792; samsung; SM-G950W; dreamqltecan; qcom",
+  "26/8.0.0; 560dpi; 1440x2792; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "26/8.0.0; 560dpi; 1440x2792; samsung; SM-G960U; starqltesq; qcom",
+  "26/8.0.0; 560dpi; 1440x2792; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "26/8.0.0; 560dpi; 2792x1440; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "26/8.0.0; 560dpi; 2952x1440; LGE/lge; LG-G710; judyln; judyln",
+  "26/8.0.0; 640dpi; 1440x2368; motorola; Moto Z (2; nash; qcom",
+  "26/8.0.0; 640dpi; 1440x2368; motorola; XT1650; griffin; qcom",
+  "26/8.0.0; 640dpi; 1440x2392; LGE/lge; LG-H830; h1; h1",
+  "26/8.0.0; 640dpi; 1440x2392; LGE/lge; LG-H918; elsa; elsa",
+  "26/8.0.0; 640dpi; 1440x2392; LGE/lge; LG-H990; elsa; elsa",
+  "26/8.0.0; 640dpi; 1440x2392; LGE/lge; LG-US996; elsa; elsa",
+  "26/8.0.0; 640dpi; 1440x2392; LGE/lge; VS987; h1; h1",
+  "26/8.0.0; 640dpi; 1440x2392; LGE/lge; VS995; elsa; elsa",
+  "26/8.0.0; 640dpi; 1440x2560; HTC/htc; HTC U11; htc_ocndugl; htc_ocn",
+  "26/8.0.0; 640dpi; 1440x2560; HTC/htc; HTC U11; htc_ocnuhl; htc_ocn",
+  "26/8.0.0; 640dpi; 1440x2560; motorola; Moto Z (2; nash; qcom",
+  "26/8.0.0; 640dpi; 1440x2560; samsung/Verizon; SM-G930V; heroqltevzw; qcom",
+  "26/8.0.0; 640dpi; 1440x2560; samsung/Verizon; SM-G935V; hero2qltevzw; qcom",
+  "26/8.0.0; 640dpi; 1440x2560; samsung; SAMSUNG-SM-G891A; poseidonlteatt; qcom",
+  "26/8.0.0; 640dpi; 1440x2560; samsung; SM-G930F; herolte; samsungexynos8890",
+  "26/8.0.0; 640dpi; 1440x2560; samsung; SM-G930P; heroqltespr; qcom",
+  "26/8.0.0; 640dpi; 1440x2560; samsung; SM-G930T; heroqltetmo; qcom",
+  "26/8.0.0; 640dpi; 1440x2560; samsung; SM-G930VL; heroqltetfnvzw; qcom",
+  "26/8.0.0; 640dpi; 1440x2560; samsung; SM-G930W8; heroltebmc; samsungexynos8890",
+  "26/8.0.0; 640dpi; 1440x2560; samsung; SM-G935F; hero2lte; samsungexynos8890",
+  "26/8.0.0; 640dpi; 1440x2560; samsung; SM-G935T; hero2qltetmo; qcom",
+  "26/8.0.0; 640dpi; 1440x2672; LGE/lge; LG-H870I; lucye; lucye",
+  "26/8.0.0; 640dpi; 1440x2672; LGE/lge; LG-H872; lucye; lucye",
+  "26/8.0.0; 640dpi; 1440x2672; LGE/lge; LG-LS993; lucye; lucye",
+  "26/8.0.0; 640dpi; 1440x2672; LGE/lge; VS988; lucye; lucye",
+  "26/8.0.0; 640dpi; 1440x2712; LGE/lge; LG-H930; joan; joan",
+  "26/8.0.0; 640dpi; 1440x2712; LGE/lge; LG-H932; joan; joan",
+  "26/8.0.0; 640dpi; 1440x2768; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "26/8.0.0; 640dpi; 2560x1440; HTC/htc; HTC U11; htc_ocnwhl; htc_ocn",
+  "26/8.0.0; 640dpi; 2712x1440; LGE/lge; VS996; joan; joan",
+  "26/8.0.0; 720dpi; 1440x2344; motorola; Moto Z (2; nash; qcom",
+  "26/8.0.0; 720dpi; 1440x2560; samsung/Verizon; SM-G930V; heroqltevzw; qcom",
+  "26/8.0.0; 720dpi; 1440x2560; samsung; SM-G930F; herolte; samsungexynos8890",
+  "26/8.0.0; 720dpi; 1440x2560; samsung; SM-G935T; hero2qltetmo; qcom",
+  "26/8.0.0; 720dpi; 1440x2646; LGE/lge; LG-H870; lucye; lucye",
+  "26/8.0.0; 720dpi; 1440x2664; LGE/lge; LM-V350; judyp; judyp",
+  "26/9; 640dpi; 1440x2560; samsung; SM-G960F; star2lte; samsungexynos8890",
+  "27/8.1.0; 420dpi; 1080x1920; OnePlus; ONEPLUS A5000; OnePlus5; qcom",
+  "27/8.1.0; 420dpi; 1080x1920; samsung; SM-C710F; jadelte; mt6757",
+  "27/8.1.0; 420dpi; 1080x1920; samsung; SM-G610M; on7xelte; samsungexynos7870",
+  "27/8.1.0; 420dpi; 1080x1920; samsung; SM-G615F; j7maxlte; mt6757",
+  "27/8.1.0; 420dpi; 1080x1920; samsung; SM-J730F; j7y17lte; samsungexynos7870",
+  "27/8.1.0; 420dpi; 1080x1920; samsung; SM-J730FM; j7y17lte; samsungexynos7870",
+  "27/8.1.0; 420dpi; 1080x1920; samsung; SM-J730G; j7y17lte; samsungexynos7870",
+  "27/8.1.0; 420dpi; 1080x2034; LGE/lge; LG-Q710AL; mcv7a; mcv7a",
+  "27/8.1.0; 420dpi; 1080x2034; LGE/lge; LG-Q710PL; mcv7a; mcv7a",
+  "27/8.1.0; 420dpi; 1080x2034; LGE/lge; LM-Q610(FGN; cv5a; cv5a",
+  "27/8.1.0; 420dpi; 1080x2034; LGE/lge; LM-Q610.FGN; mcv5a; mcv5a",
+  "27/8.1.0; 420dpi; 1080x2034; LGE/lge; LM-Q710(FGN; cv7a; cv7a",
+  "27/8.1.0; 420dpi; 1080x2034; LGE/lge; LM-Q710.FG; cv7a; cv7a",
+  "27/8.1.0; 420dpi; 1080x2034; LGE/lge; LML713DL; cv7a; cv7a",
+  "27/8.1.0; 420dpi; 1080x2034; OnePlus; ONEPLUS A5010; OnePlus5T; qcom",
+  "27/8.1.0; 420dpi; 1080x2094; samsung; SM-N960F; crownlte; samsungexynos9810",
+  "27/8.1.0; 420dpi; 1080x2094; samsung; SM-N960U; crownqltesq; qcom",
+  "27/8.1.0; 420dpi; 1080x2129; samsung; SM-M205FN; m20lte; samsungexynos7885",
+  "27/8.1.0; 420dpi; 1080x2136; samsung; SM-M305M; m30lte; samsungexynos7885",
+  "27/8.1.0; 420dpi; 1080x2160; OnePlus; ONEPLUS A5010; OnePlus5T; qcom",
+  "27/8.1.0; 420dpi; 1080x2214; LGE/lge; LM-Q850; falcon; falcon",
+  "27/8.1.0; 420dpi; 1080x2214; LGE/lge; LM-V405; judypn; judypn",
+  "27/8.1.0; 420dpi; 1080x2220; samsung; SM-N960U; crownqltesq; qcom",
+  "27/8.1.0; 420dpi; 2034x1080; OnePlus; ONEPLUS A5010; OnePlus5T; qcom",
+  "27/8.1.0; 420dpi; 2160x1080; OnePlus; ONEPLUS A5010; OnePlus5T; qcom",
+  "27/8.1.0; 440dpi; 1080x2030; Xiaomi/xiaomi; MI 6X; wayne; qcom",
+  "27/8.1.0; 440dpi; 1080x2030; Xiaomi/xiaomi; Redmi 5 Plus; vince; qcom",
+  "27/8.1.0; 440dpi; 1080x2030; Xiaomi/xiaomi; Redmi Note 5; whyred; qcom",
+  "27/8.1.0; 440dpi; 1080x2030; Xiaomi; MI MAX 3; nitrogen; qcom",
+  "27/8.1.0; 440dpi; 1080x2114; Xiaomi; MI 8 SE; sirius; qcom",
+  "27/8.1.0; 440dpi; 1080x2116; Xiaomi; POCOPHONE F1; beryllium; qcom",
+  "27/8.1.0; 440dpi; 1080x2118; Xiaomi; MI 8; dipper; qcom",
+  "27/8.1.0; 440dpi; 1080x2150; Xiaomi/xiaomi; MI PLAY; lotus; mt6765",
+  "27/8.1.0; 440dpi; 1080x2150; Xiaomi/xiaomi; Redmi 6 Pro; sakura_india; qcom",
+  "27/8.1.0; 440dpi; 1080x2150; Xiaomi/xiaomi; Redmi Note 6 Pro; tulip; qcom",
+  "27/8.1.0; 440dpi; 1080x2150; Xiaomi; MI 8 Lite; platina; qcom",
+  "27/8.1.0; 440dpi; 2030x1080; Xiaomi/xiaomi; MI 6X; whyred; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; HMD Global/Nokia; TA-1021; PLE; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; HMD Global/Nokia; TA-1025; PLE; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; OnePlus; ONE A2003; OnePlus2; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; Xiaomi/xiaomi; Mi A1; tissot_sprout; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; asus; ASUS_X00DD; ASUS_X00DD; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; asus; ASUS_X00DDA; ASUS_X00DD; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; asus; ZC553KL; ASUS_X00DD; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; bq; Aquaris X; bardock; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; motorola; Moto G (5 Plus; potter_nt; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; motorola; Moto G (5; cedric; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; motorola; Moto G (5S Plus; sanders_nt; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; motorola; Moto G (5S; montana; qcom",
+  "27/8.1.0; 480dpi; 1080x1920; samsung; SM-G610F; on7xelte; samsungexynos7870",
+  "27/8.1.0; 480dpi; 1080x1920; samsung; SM-G610M; on7xelte; samsungex",
+  "27/8.1.0; 480dpi; 1080x1920; samsung; SM-G610M; on7xelte; samsungexynos7870",
+  "27/8.1.0; 480dpi; 1080x1920; samsung; SM-J730FM; j7y17lte; samsungexynos7870",
+  "27/8.1.0; 480dpi; 1080x1920; samsung; SM-J730G; j7y17lte; samsungexynos7870",
+  "27/8.1.0; 480dpi; 1080x2004; LGE/lge; LG-M700; mh; mh",
+  "27/8.1.0; 480dpi; 1080x2004; LGE/lge; LG-M700; mhn; mh",
+  "27/8.1.0; 480dpi; 1080x2016; LGE/lge; LM-Q610.FGN; mcv5a; mcv5a",
+  "27/8.1.0; 480dpi; 1080x2016; LGE/lge; LM-Q710(FGN; cv7a; cv7a",
+  "27/8.1.0; 480dpi; 1080x2016; Meizu; 16th; 16th; qcom",
+  "27/8.1.0; 480dpi; 1080x2016; Meizu; meizu note8; meizunote8; qcom",
+  "27/8.1.0; 480dpi; 1080x2016; ZTE; ZTE BLADE V9; P450F10; qcom",
+  "27/8.1.0; 480dpi; 1080x2016; asus; ZB602KL; ASUS_X00T_6; qcom",
+  "27/8.1.0; 480dpi; 1080x2028; asus; ASUS_X018D; ASUS_X018_4; mt6755",
+  "27/8.1.0; 480dpi; 1080x2070; Meizu; 16; 16; qcom",
+  "27/8.1.0; 480dpi; 1080x2102; Multilaser; MS80X; ML-TI0D-MS80X; qcom",
+  "27/8.1.0; 480dpi; 1080x2116; HUAWEI; EML-L09; HWEML; kirin970",
+  "27/8.1.0; 480dpi; 1080x2116; HUAWEI; EML-L29; HWEML; kirin970",
+  "27/8.1.0; 480dpi; 1080x2118; HUAWEI; CLT-L09; HWCLT; kirin970",
+  "27/8.1.0; 480dpi; 1080x2118; HUAWEI; CLT-L29; HWCLT; kirin970",
+  "27/8.1.0; 480dpi; 1080x2136; Xiaomi/xiaomi; Mi A2 Lite; daisy_sprout; qcom",
+  "27/8.1.0; 480dpi; 1080x2140; HUAWEI/HONOR; ARE-L22HN; HWARE-QC; qcom",
+  "27/8.1.0; 480dpi; 1080x2150; HUAWEI/HONOR; COL-L29; HWCOL; kirin970",
+  "27/8.1.0; 480dpi; 1080x2154; vivo; vivo 1806; 1806; mt6771",
+  "27/8.1.0; 480dpi; 1080x2218; HUAWEI; INE-LX1; HWINE; kirin710",
+  "27/8.1.0; 480dpi; 1080x2218; HUAWEI; PAR-LX1; HWPAR; kirin970",
+  "27/8.1.0; 480dpi; 1080x2218; HUAWEI; SNE-LX1; HWSNE; kirin710",
+  "27/8.1.0; 480dpi; 1080x2222; HUAWEI/HONOR; JSN-L21; HWJSN-H; kirin710",
+  "27/8.1.0; 480dpi; 1080x2244; HUAWEI; EML-L09; HWEML; kirin970",
+  "27/8.1.0; 480dpi; 1080x2280; HUAWEI/HONOR; COL-L29; HWCOL; kirin970",
+  "27/8.1.0; 480dpi; 1080x2340; HUAWEI; SNE-LX3; HWSNE; kirin710",
+  "27/8.1.0; 480dpi; 2028x1080; asus; ASUS_X018D; ASUS_X018_4; mt6755",
+  "27/8.1.0; 503dpi; 1080x2030; Xiaomi/xiaomi; Redmi 5 Plus; vince; qcom",
+  "27/8.1.0; 540dpi; 1080x1920; asus; ASUS_X00DDA; ASUS_X00DD; qcom",
+  "27/8.1.0; 540dpi; 1080x1920; samsung; SM-G610F; on7xelte; samsungexynos7870",
+  "27/8.1.0; 540dpi; 1080x1920; samsung; SM-G610M; on7xelte; samsungexynos7870",
+  "27/8.1.0; 540dpi; 1080x1984; LGE/lge; LG-M700; mh; mh",
+  "27/8.1.0; 540dpi; 1080x1998; LGE/lge; LM-Q710(FGN; cv7a; cv7a",
+  "27/8.1.0; 540dpi; 1080x2030; Xiaomi/xiaomi; Redmi 5 Plus; vince; qcom",
+  "27/8.1.0; 540dpi; 1080x2218; HUAWEI; INE-LX1; HWINE; kirin710",
+  "27/8.1.0; 540dpi; 1080x2218; HUAWEI; SNE-LX1; HWSNE; kirin710",
+  "27/8.1.0; 540dpi; 1080x2340; HUAWEI/HONOR; JSN-AL00a; HWJSN-HM; kirin710",
+  "27/8.1.0; 560dpi; 1440x2392; RED; H1A1000; HydrogenONE; qcom",
+  "27/8.1.0; 560dpi; 1440x2560; samsung; SM-N910F; trlte; qcom",
+  "28 / 9; 420dpi; 1080x2094; Samsung; SM-N950U; greatqlte; qcom",
+  "28/9; 420dpi; 1080x1920; OnePlus; ONEPLUS A3003; OnePlus3T; qcom",
+  "28/9; 420dpi; 1080x1920; OnePlus; ONEPLUS A3010; OnePlus3T; qcom",
+  "28/9; 420dpi; 1080x1920; OnePlus; ONEPLUS A5000; OnePlus5; qcom",
+  "28/9; 420dpi; 1080x1920; samsung; SM-G611F; on7xreflte; samsungexynos7870",
+  "28/9; 420dpi; 1080x1920; samsung; SM-G611MT; on7xreflte; samsungexynos7870",
+  "28/9; 420dpi; 1080x1920; samsung; SM-J730F; j7y17lte; samsungexynos7870",
+  "28/9; 420dpi; 1080x1920; samsung; SM-J730FM; j7y17lte; samsungexynos7870",
+  "28/9; 420dpi; 1080x1920; samsung; SM-J730G; j7y17lte; samsungexynos7870",
+  "28/9; 420dpi; 1080x2034; LGE/lge; LGL722DL; cv7as; cv7as",
+  "28/9; 420dpi; 1080x2034; LGE/lge; LM-G810; betalm; betalm",
+  "28/9; 420dpi; 1080x2034; LGE/lge; LM-Q610(FGN; cv5a; cv5a",
+  "28/9; 420dpi; 1080x2034; LGE/lge; LM-Q710.FG; cv7a; cv7a",
+  "28/9; 420dpi; 1080x2034; LGE/lge; LM-Q720; cv7as-pr; cv7as",
+  "28/9; 420dpi; 1080x2034; LGE/lge; LM-Q720; cv7as; cv7as",
+  "28/9; 420dpi; 1080x2034; OnePlus; ONEPLUS A5010; OnePlus5T; qcom",
+  "28/9; 420dpi; 1080x2034; Sony; H4413; H4413; qcom",
+  "28/9; 420dpi; 1080x2042; samsung; SM-G973F; beyond1; exynos9820",
+  "28/9; 420dpi; 1080x2047; samsung; SM-G975F; beyond2; exynos9820",
+  "28/9; 420dpi; 1080x2047; samsung; SM-G975U; beyond2q; qcom",
+  "28/9; 420dpi; 1080x2064; samsung; SM-N970F; d1; exynos9825",
+  "28/9; 420dpi; 1080x2064; samsung; SM-N970U; d1q; qcom",
+  "28/9; 420dpi; 1080x2069; samsung; SM-N975F; d2s; exynos9825",
+  "28/9; 420dpi; 1080x2069; samsung; SM-N975U; d2q; qcom",
+  "28/9; 420dpi; 1080x2081; motorola; moto g(8 plus; doha; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-A530F; jackpotlte; samsungexynos7885",
+  "28/9; 420dpi; 1080x2094; samsung; SM-A605FN; a6plte; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-A730F; jackpot2lte; samsungexynos7885",
+  "28/9; 420dpi; 1080x2094; samsung; SM-A750FN; a7y18lte; samsungexynos7885",
+  "28/9; 420dpi; 1080x2094; samsung; SM-A750G; a7y18lte; samsungexynos7885",
+  "28/9; 420dpi; 1080x2094; samsung; SM-A920F; a9y18qlte; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G892A; cruiserlteatt; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G892U; cruiserltesq; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G950U1; dreamqlteue; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G950U; dreamqltesq; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G955U1; dream2qlteue; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G955U; dream2qltesq; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G955W; dream2qltecan; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G960F; starlte; samsungexynos9810",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G960U; starqltesq; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G9650; star2qltechn; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G965N; star2lteks; samsungexynos9810",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G965U1; star2qlteue; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-G965U; star2qltesq; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-N9500; greatqltechn; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "28/9; 420dpi; 1080x2094; samsung; SM-N950U1; greatqlteue; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-N950U; greatqlte; qcom",
+  "28/9; 420dpi; 1080x2094; samsung; SM-N960F; crownlte; samsungexynos9810",
+  "28/9; 420dpi; 1080x2094; samsung; SM-N960U; crownqltesq; qcom",
+  "28/9; 420dpi; 1080x2123; motorola; moto g(8 plus; doha; qcom",
+  "28/9; 420dpi; 1080x2128; samsung; SM-A405FM; a40; exynos7885",
+  "28/9; 420dpi; 1080x2131; samsung; SM-A305F; a30; exynos7885",
+  "28/9; 420dpi; 1080x2131; samsung; SM-A305FN; a30; exynos7885",
+  "28/9; 420dpi; 1080x2131; samsung; SM-A305GT; a30; exynos7885",
+  "28/9; 420dpi; 1080x2131; samsung; SM-A505F; a50; exynos9610",
+  "28/9; 420dpi; 1080x2131; samsung; SM-A505FM; a50; exynos9610",
+  "28/9; 420dpi; 1080x2131; samsung; SM-A505FN; a50; exynos9610",
+  "28/9; 420dpi; 1080x2131; samsung; SM-A505G; a50; exynos9610",
+  "28/9; 420dpi; 1080x2131; samsung; SM-A505GT; a50; exynos9610",
+  "28/9; 420dpi; 1080x2131; samsung; SM-A505U1; a50; exynos9610",
+  "28/9; 420dpi; 1080x2131; samsung; SM-A505U; a50; exynos9610",
+  "28/9; 420dpi; 1080x2131; samsung; SM-A505YN; a50; exynos9610",
+  "28/9; 420dpi; 1080x2131; samsung; SM-M307FN; m30s; exynos9611",
+  "28/9; 420dpi; 1080x2131; samsung; SM-S506DL; a50; exynos9610",
+  "28/9; 420dpi; 1080x2132; HMD Global/Nokia; Nokia 7.2; DDV_sprout; qcom",
+  "28/9; 420dpi; 1080x2135; OnePlus; ONEPLUS A6013; OnePlus6T; qcom",
+  "28/9; 420dpi; 1080x2137; LGE/lge; LM-G850; mh2lm; mh2lm",
+  "28/9; 420dpi; 1080x2137; motorola; motorola one zoom; parker; qcom",
+  "28/9; 420dpi; 1080x2143; LGE/lge; LG-G710; judyln; judyln",
+  "28/9; 420dpi; 1080x2146; LGE/lge; LM-V450; flashlm; flashlm",
+  "28/9; 420dpi; 1080x2147; LGE/lge; LM-V405; judypn; judypn",
+  "28/9; 420dpi; 1080x2181; samsung; SM-A605FN; a6plte; qcom",
+  "28/9; 420dpi; 1080x2181; samsung; SM-A730F; jackpot2lte; samsungexynos7885",
+  "28/9; 420dpi; 1080x2181; samsung; SM-A750FN; a7y18lte; samsungexynos7885",
+  "28/9; 420dpi; 1080x2181; samsung; SM-A920F; a9y18qlte; qcom",
+  "28/9; 420dpi; 1080x2181; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "28/9; 420dpi; 1080x2181; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "28/9; 420dpi; 1080x2181; samsung; SM-G955U; dream2qltesq; qcom",
+  "28/9; 420dpi; 1080x2181; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "28/9; 420dpi; 1080x2181; samsung; SM-G965U1; star2qlteue; qcom",
+  "28/9; 420dpi; 1080x2181; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "28/9; 420dpi; 1080x2181; samsung; SM-N950U; greatqlte; qcom",
+  "28/9; 420dpi; 1080x2198; samsung; SM-A705FN; a70q; qcom",
+  "28/9; 420dpi; 1080x2198; samsung; SM-A705MN; a70q; qcom",
+  "28/9; 420dpi; 1080x2198; samsung; SM-A705U; a70q; qcom",
+  "28/9; 420dpi; 1080x2198; samsung; SM-A705W; a70q; qcom",
+  "28/9; 420dpi; 1080x2218; samsung; SM-A305FN; a30; exynos7885",
+  "28/9; 420dpi; 1080x2218; samsung; SM-A505FM; a50; exynos9610",
+  "28/9; 420dpi; 1080x2218; samsung; SM-A505FN; a50; exynos9610",
+  "28/9; 420dpi; 1080x2218; samsung; SM-A505G; a50; exynos9610",
+  "28/9; 420dpi; 1080x2220; samsung; SM-A730F; jackpot2lte; samsungexynos7885",
+  "28/9; 420dpi; 1080x2220; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "28/9; 420dpi; 1080x2220; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "28/9; 420dpi; 1080x2257; samsung; SM-A505FN; a50; exynos9610",
+  "28/9; 420dpi; 1080x2260; OnePlus; GM1903; OnePlus7; qcom",
+  "28/9; 420dpi; 1080x2274; samsung; SM-A805F; r1q; qcom",
+  "28/9; 420dpi; 1080x2285; samsung; SM-A705FN; a70q; qcom",
+  "28/9; 420dpi; 1080x2394; Sony; I4293; I4293; qcom",
+  "28/9; 420dpi; 2047x1080; samsung/KDDI; SCV42; SCV42; qcom",
+  "28/9; 420dpi; 2094x1080; samsung; SM-N960U; crownqltesq; qcom",
+  "28/9; 440dpi; 1080x1920; Xiaomi; Mi Note 3; jason; qcom",
+  "28/9; 440dpi; 1080x2027; Xiaomi; POCO F1; beryllium; qcom",
+  "28/9; 440dpi; 1080x2027; Xiaomi; POCOPHONE F1; beryllium; qcom",
+  "28/9; 440dpi; 1080x2028; Google/google; Pixel 3; blueline; blueline",
+  "28/9; 440dpi; 1080x2028; Xiaomi; MI 8; dipper; qcom",
+  "28/9; 440dpi; 1080x2029; Xiaomi; MI 8 SE; sirius; qcom",
+  "28/9; 440dpi; 1080x2029; Xiaomi; MI 8; dipper; qcom",
+  "28/9; 440dpi; 1080x2030; Xiaomi/xiaomi; Redmi Note 5 Pro; whyred; qcom",
+  "28/9; 440dpi; 1080x2030; Xiaomi/xiaomi; Redmi Note 5; whyred; qcom",
+  "28/9; 440dpi; 1080x2030; Xiaomi; MI MAX 3; nitrogen; qcom",
+  "28/9; 440dpi; 1080x2030; Xiaomi; Mi MIX 2; chiron; qcom",
+  "28/9; 440dpi; 1080x2061; Xiaomi/xiaomi; Redmi 6 Pro; sakura; qcom",
+  "28/9; 440dpi; 1080x2061; Xiaomi/xiaomi; Redmi 6 Pro; sakura_india; qcom",
+  "28/9; 440dpi; 1080x2062; Xiaomi/xiaomi; Redmi Note 6 Pro; tulip; qcom",
+  "28/9; 440dpi; 1080x2068; Xiaomi; MI 8 Lite; platina; qcom",
+  "28/9; 440dpi; 1080x2120; Xiaomi/xiaomi; Redmi Note 7; lavender; qcom",
+  "28/9; 440dpi; 1080x2130; Xiaomi/xiaomi; Redmi Note 7; lavender; qcom",
+  "28/9; 440dpi; 1080x2130; Xiaomi/xiaomi; Redmi Note 8; ginkgo; qcom",
+  "28/9; 440dpi; 1080x2130; Xiaomi/xiaomi; Redmi Note 8T; willow; qcom",
+  "28/9; 440dpi; 1080x2131; Xiaomi/xiaomi; Redmi Note 7 Pro; violet; qcom",
+  "28/9; 440dpi; 1080x2131; Xiaomi/xiaomi; Redmi Note 7; lavender; qcom",
+  "28/9; 440dpi; 1080x2131; Xiaomi/xiaomi; Redmi Note 7S; lavender; qcom",
+  "28/9; 440dpi; 1080x2134; Xiaomi/Redmi; Redmi Note 8 Pro; begonia; mt6785",
+  "28/9; 440dpi; 1080x2134; Xiaomi/Redmi; Redmi Note 8 Pro; begoniain; mt6785",
+  "28/9; 440dpi; 1080x2135; Xiaomi; Mi 9 Lite; pyxis; qcom",
+  "28/9; 440dpi; 1080x2138; Xiaomi; Mi Note 10; tucana; qcom",
+  "28/9; 440dpi; 1080x2210; Xiaomi; Mi 9T; davinci; qcom",
+  "28/9; 440dpi; 1080x2210; Xiaomi; Mi MIX 3; perseus; qcom",
+  "28/9; 440dpi; 2028x1080; Google/google; Pixel 3; blueline; blueline",
+  "28/9; 440dpi; 2030x1080; Xiaomi/xiaomi; Redmi Note 5; whyred; qcom",
+  "28/9; 440dpi; 2048x2048; Xiaomi/xiaomi; Redmi Note 7; lavender; qcom",
+  "28/9; 440dpi; 2130x1080; Xiaomi/xiaomi; Redmi Note 8T; willow; qcom",
+  "28/9; 440dpi; 2131x1080; Xiaomi/xiaomi; Redmi Note 7; lavender; qcom",
+  "28/9; 454dpi; 1080x2160; motorola; moto g(6 plus; evert_nt; qcom",
+  "28/9; 460dpi; 1080x2115; motorola; moto g(8 plus; doha; qcom",
+  "28/9; 460dpi; 1080x2125; motorola; motorola one zoom; parker; qcom",
+  "28/9; 461dpi; 1080x2131; Xiaomi/xiaomi; Redmi Note 7; lavender; qcom",
+  "28/9; 480dpi; 1080x1920; HMD Global/Nokia; TA-1021; PLE; qcom",
+  "28/9; 480dpi; 1080x1920; HUAWEI/HONOR; STF-L09; HWSTF; hi3660",
+  "28/9; 480dpi; 1080x1920; HUAWEI; LON-L29; HWLON; hi3660",
+  "28/9; 480dpi; 1080x1920; HUAWEI; VKY-L29; HWVKY; hi3660",
+  "28/9; 480dpi; 1080x1920; HUAWEI; VTR-L09; HWVTR; hi3660",
+  "28/9; 480dpi; 1080x1920; HUAWEI; VTR-L29; HWVTR; hi3660",
+  "28/9; 480dpi; 1080x1920; OnePlus; ONEPLUS A3010; OnePlus3T; qcom",
+  "28/9; 480dpi; 1080x1920; Xiaomi/xiaomi; Mi A1; tissot_sprout; qcom",
+  "28/9; 480dpi; 1080x1920; Xiaomi; MI 6; sagit; qcom",
+  "28/9; 480dpi; 1080x1920; asus; ASUS_Z01KD; ASUS_Z01KDA; qcom",
+  "28/9; 480dpi; 1080x1920; motorola; Moto Z2 Play; albus; qcom",
+  "28/9; 480dpi; 1080x1920; samsung; SM-G611MT; on7xreflte; samsungexynos7870",
+  "28/9; 480dpi; 1080x1920; samsung; SM-J730F; j7y17lte; samsungexynos7870",
+  "28/9; 480dpi; 1080x1920; samsung; SM-J730FM; j7y17lte; samsungexynos7870",
+  "28/9; 480dpi; 1080x1920; samsung; SM-J730G; j7y17lte; samsungexynos7870",
+  "28/9; 480dpi; 1080x2012; Ulefone; Armor_6E; Armor_6E; mt6771",
+  "28/9; 480dpi; 1080x2014; Blackview; BV9600; BV9600; mt6771",
+  "28/9; 480dpi; 1080x2016; A-gold/Blackview; BV9500Plus; BV9500Plus; mt6771",
+  "28/9; 480dpi; 1080x2016; LGE/lge; LGL722DL; cv7as; cv7as",
+  "28/9; 480dpi; 1080x2016; LGE/lge; LM-Q710.FG; cv7a; cv7a",
+  "28/9; 480dpi; 1080x2016; LGE/lge; LM-Q720; cv7as; cv7as",
+  "28/9; 480dpi; 1080x2016; OnePlus; ONEPLUS A5010; OnePlus5T; qcom",
+  "28/9; 480dpi; 1080x2016; Xiaomi/xiaomi; Mi A2; jasmine_sprout; qcom",
+  "28/9; 480dpi; 1080x2016; Yulong/Coolpad; cp3705A; lithium; qcom",
+  "28/9; 480dpi; 1080x2016; Yulong/Coolpad; cp3705AS; koba; qcom",
+  "28/9; 480dpi; 1080x2016; motorola; Moto Z3 Play; beckham; qcom",
+  "28/9; 480dpi; 1080x2016; motorola; moto g(6 (XT1925DL); ali; qcom",
+  "28/9; 480dpi; 1080x2016; motorola; moto g(6 plus; evert; qcom",
+  "28/9; 480dpi; 1080x2016; motorola; moto g(6 plus; evert_nt; qcom",
+  "28/9; 480dpi; 1080x2016; motorola; moto g(6; ali; qcom",
+  "28/9; 480dpi; 1080x2016; motorola; moto g(7 plus; lake_n; qcom",
+  "28/9; 480dpi; 1080x2016; motorola; moto g(7; river; qcom",
+  "28/9; 480dpi; 1080x2016; motorola; moto z3; messi; qcom",
+  "28/9; 480dpi; 1080x2020; samsung; SM-G970F; beyond0; exynos9820",
+  "28/9; 480dpi; 1080x2020; samsung; SM-G970U; beyond0q; qcom",
+  "28/9; 480dpi; 1080x2024; samsung; SM-G973F; beyond1; exynos9820",
+  "28/9; 480dpi; 1080x2028; asus; ASUS_X00TD; ASUS_X00T_2; qcom",
+  "28/9; 480dpi; 1080x2028; asus; ASUS_X00TD; ASUS_X00T_4; qcom",
+  "28/9; 480dpi; 1080x2028; asus; ASUS_X00TD; ASUS_X00T_6; qcom",
+  "28/9; 480dpi; 1080x2028; asus; ASUS_X00TDB; ASUS_X00T_2; qcom",
+  "28/9; 480dpi; 1080x2028; asus; ASUS_X00TDB; ASUS_X00T_4; qcom",
+  "28/9; 480dpi; 1080x2028; asus; ASUS_X017D; ASUS_X017D_1; qcom",
+  "28/9; 480dpi; 1080x2028; asus; ZB602KL; ASUS_X00T_6; qcom",
+  "28/9; 480dpi; 1080x2029; samsung; SM-G975U; beyond2q; qcom",
+  "28/9; 480dpi; 1080x2030; asus; ASUS_X00QD; ASUS_X00QD; qcom",
+  "28/9; 480dpi; 1080x2030; asus; ZE620KL; ASUS_X00QD; qcom",
+  "28/9; 480dpi; 1080x2031; HUAWEI; EML-L09; HWEML; kirin970",
+  "28/9; 480dpi; 1080x2031; HUAWEI; EML-L29; HWEML; kirin970",
+  "28/9; 480dpi; 1080x2032; HUAWEI/HONOR; LLD-L21; HWLLD-H; hi6250",
+  "28/9; 480dpi; 1080x2032; HUAWEI/HONOR; LLD-L31; HWLLD-H; hi6250",
+  "28/9; 480dpi; 1080x2032; HUAWEI; FIG-LX1; HWFIG-H; hi6250",
+  "28/9; 480dpi; 1080x2032; HUAWEI; FIG-LX3; HWFIG-H; hi6250",
+  "28/9; 480dpi; 1080x2034; asus; ASUS_X017DA; ASUS_X017D_2; qcom",
+  "28/9; 480dpi; 1080x2037; HUAWEI; CLT-L09; HWCLT; kirin970",
+  "28/9; 480dpi; 1080x2037; HUAWEI; CLT-L29; HWCLT; kirin970",
+  "28/9; 480dpi; 1080x2038; HUAWEI/HONOR; BND-L21; HWBND-H; hi6250",
+  "28/9; 480dpi; 1080x2038; HUAWEI; FLA-LX1; HWFLA-H; hi6250",
+  "28/9; 480dpi; 1080x2040; HUAWEI/HONOR; BKL-L09; HWBKL; kirin970",
+  "28/9; 480dpi; 1080x2040; HUAWEI; BLA-A09; HWBLA; kirin970",
+  "28/9; 480dpi; 1080x2040; HUAWEI; BLA-L09; HWBLA; kirin970",
+  "28/9; 480dpi; 1080x2040; HUAWEI; BLA-L29; HWBLA; kirin970",
+  "28/9; 480dpi; 1080x2043; Xiaomi/xiaomi; Mi A2 Lite; daisy_sprout; qcom",
+  "28/9; 480dpi; 1080x2047; A-gold/Blackview; BV9700Pro; BV9700Pro; mt6771",
+  "28/9; 480dpi; 1080x2049; HUAWEI; HMA-L29; HWHMA; kirin980",
+  "28/9; 480dpi; 1080x2055; asus; ASUS_A001D; ASUS_A001D_1; qcom",
+  "28/9; 480dpi; 1080x2055; asus; ASUS_A001D; ASUS_A001D_2; qcom",
+  "28/9; 480dpi; 1080x2059; DOOGEE; N20; N20; mt6763",
+  "28/9; 480dpi; 1080x2060; HUAWEI/HONOR; COL-L29; HWCOL; kirin970",
+  "28/9; 480dpi; 1080x2060; HUAWEI; ANE-LX1; HWANE; hi6250",
+  "28/9; 480dpi; 1080x2060; HUAWEI; ANE-LX2; HWANE; hi6250",
+  "28/9; 480dpi; 1080x2060; HUAWEI; ANE-LX3; HWANE; hi6250",
+  "28/9; 480dpi; 1080x2064; UMIDIGI; A5_Pro; A5_Pro; mt6763",
+  "28/9; 480dpi; 1080x2064; motorola; Moto Z3 Play; beckham; qcom",
+  "28/9; 480dpi; 1080x2064; motorola; moto z3; messi; qcom",
+  "28/9; 480dpi; 1080x2066; asus; ASUS_X01BDA; ASUS_X01BD_2; qcom",
+  "28/9; 480dpi; 1080x2068; OPPO; CPH1821; CPH1821; mt6771",
+  "28/9; 480dpi; 1080x2075; vivo; vivo 1723; 1723; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-A530F; jackpotlte; samsungexynos7885",
+  "28/9; 480dpi; 1080x2076; samsung; SM-A530W; jackpotltecan; samsungexynos7885",
+  "28/9; 480dpi; 1080x2076; samsung; SM-A605FN; a6plte; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-A730F; jackpot2lte; samsungexynos7885",
+  "28/9; 480dpi; 1080x2076; samsung; SM-A750FN; a7y18lte; samsungexynos7885",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G892A; cruiserlteatt; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G892U; cruiserltesq; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G950F; dreamqltechn; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G950N; dreamlteks; samsungexynos8895",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G950U1; dreamqlteue; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G950U; dreamqltesq; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G955U1; dream2qlteue; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G955U; dream2qltesq; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G9600; starqltechn; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G960F; starlte; samsungexynos9810",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G960U; starqltesq; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-G965U; star2qltesq; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "28/9; 480dpi; 1080x2076; samsung; SM-N950U; greatqlte; qcom",
+  "28/9; 480dpi; 1080x2076; samsung; SM-N960U1; crownqlteue; qcom",
+  "28/9; 480dpi; 1080x2081; vivo; vivo 1806; 1806; mt6771",
+  "28/9; 480dpi; 1080x2090; HUAWEI/HONOR; PCT-L29; HWPCT; kirin980",
+  "28/9; 480dpi; 1080x2107; HUAWEI/HONOR; MAR-LX1H; HWMAR; kirin710",
+  "28/9; 480dpi; 1080x2107; HUAWEI; MAR-LX1A; HWMAR; kirin710",
+  "28/9; 480dpi; 1080x2107; HUAWEI; MAR-LX1M; HWMAR; kirin710",
+  "28/9; 480dpi; 1080x2107; HUAWEI; MAR-LX3A; HWMAR; kirin710",
+  "28/9; 480dpi; 1080x2110; HUAWEI/HONOR; YAL-L21; HWYAL; kirin980",
+  "28/9; 480dpi; 1080x2110; HUAWEI/HONOR; YAL-L41; HWYAL; kirin980",
+  "28/9; 480dpi; 1080x2110; samsung; SM-A405FM; a40; exynos7885",
+  "28/9; 480dpi; 1080x2110; samsung; SM-A405FN; a40; exynos7885",
+  "28/9; 480dpi; 1080x2113; samsung; SM-A305FN; a30; exynos7885",
+  "28/9; 480dpi; 1080x2113; samsung; SM-A305G; a30; exynos7885",
+  "28/9; 480dpi; 1080x2113; samsung; SM-A505FN; a50; exynos9610",
+  "28/9; 480dpi; 1080x2113; samsung; SM-S506DL; a50; exynos9610",
+  "28/9; 480dpi; 1080x2116; A-gold/UMIDIGI; F1; F1; mt6771",
+  "28/9; 480dpi; 1080x2116; OPPO; CPH1823; CPH1823; mt6771",
+  "28/9; 480dpi; 1080x2116; OPPO; CPH1881; CPH1881; mt6771",
+  "28/9; 480dpi; 1080x2119; samsung; SM-G970F; beyond0; exynos9820",
+  "28/9; 480dpi; 1080x2120; OPPO; CPH1911; OP4883; mt6771",
+  "28/9; 480dpi; 1080x2120; OPPO; CPH1979; OP48A1; mt6779",
+  "28/9; 480dpi; 1080x2128; HUAWEI/HONOR; COR-L29; HWCOR; kirin970",
+  "28/9; 480dpi; 1080x2128; HUAWEI; INE-LX1; HWINE; kirin710",
+  "28/9; 480dpi; 1080x2128; HUAWEI; INE-LX2; HWINE; kirin710",
+  "28/9; 480dpi; 1080x2128; HUAWEI; SNE-LX1; HWSNE; kirin710",
+  "28/9; 480dpi; 1080x2128; HUAWEI; SNE-LX3; HWSNE; kirin710",
+  "28/9; 480dpi; 1080x2128; samsung; SM-G975U; beyond2q; qcom",
+  "28/9; 480dpi; 1080x2129; Xiaomi; MI 9 SE; grus; qcom",
+  "28/9; 480dpi; 1080x2129; Xiaomi; Mi 9 SE; grus; qcom",
+  "28/9; 480dpi; 1080x2130; HUAWEI; PAR-LX1; HWPAR; kirin970",
+  "28/9; 480dpi; 1080x2130; Xiaomi/xiaomi; Redmi Note 8; ginkgo; qcom",
+  "28/9; 480dpi; 1080x2130; vivo; vivo 1915; 1915; mt6768",
+  "28/9; 480dpi; 1080x2132; OPPO; CPH1893; CPH1893; qcom",
+  "28/9; 480dpi; 1080x2132; realme; RMX1921; RMX1921; qcom",
+  "28/9; 480dpi; 1080x2137; HUAWEI/HONOR; JSN-L21; HWJSN-H; kirin710",
+  "28/9; 480dpi; 1080x2137; HUAWEI/HONOR; JSN-L22; HWJSN-H; kirin710",
+  "28/9; 480dpi; 1080x2137; HUAWEI; JKM-LX3; HWJKM-H; kirin710",
+  "28/9; 480dpi; 1080x2139; HUAWEI/HONOR; HRY-LX1; HWHRY-H; kirin710",
+  "28/9; 480dpi; 1080x2139; HUAWEI/HONOR; HRY-LX1T; HWHRY-HF; kirin710",
+  "28/9; 480dpi; 1080x2139; HUAWEI; POT-LX1; HWPOT-H; kirin710",
+  "28/9; 480dpi; 1080x2139; HUAWEI; POT-LX1T; HWPOT-H; kirin710",
+  "28/9; 480dpi; 1080x2139; HUAWEI; POT-LX3; HWPOT-H; kirin710",
+  "28/9; 480dpi; 1080x2141; vivo; vivo 1804; 1804; qcom",
+  "28/9; 480dpi; 1080x2141; vivo; vivo 1907; 1907; mt6768",
+  "28/9; 480dpi; 1080x2141; vivo; vivo 1907; 1907N; mt6768",
+  "28/9; 480dpi; 1080x2141; vivo; vivo 1920; 1920; qcom",
+  "28/9; 480dpi; 1080x2159; HUAWEI; CLT-L29; HWCLT; kirin970",
+  "28/9; 480dpi; 1080x2159; HUAWEI; EML-L09; HWEML; kirin970",
+  "28/9; 480dpi; 1080x2159; HUAWEI; EML-L29; HWEML; kirin970",
+  "28/9; 480dpi; 1080x2159; asus; ASUS_X00QD; ASUS_X00QD; qcom",
+  "28/9; 480dpi; 1080x2160; HUAWEI/HONOR; BND-L21; HWBND-H; hi6250",
+  "28/9; 480dpi; 1080x2160; motorola; moto g(6 plus; evert_nt; qcom",
+  "28/9; 480dpi; 1080x2160; motorola; moto g(6; ali; qcom",
+  "28/9; 480dpi; 1080x2160; motorola; moto g(6; ali_n; qcom",
+  "28/9; 480dpi; 1080x2175; samsung; SM-A530F; jackpotlte; samsungexynos7885",
+  "28/9; 480dpi; 1080x2175; samsung; SM-A730F; jackpot2lte; samsungexynos7885",
+  "28/9; 480dpi; 1080x2175; samsung; SM-A750FN; a7y18lte; samsungexynos7885",
+  "28/9; 480dpi; 1080x2175; samsung; SM-G892A; cruiserlteatt; qcom",
+  "28/9; 480dpi; 1080x2175; samsung; SM-G892U; cruiserltesq; qcom",
+  "28/9; 480dpi; 1080x2175; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "28/9; 480dpi; 1080x2175; samsung; SM-G950U1; dreamqlteue; qcom",
+  "28/9; 480dpi; 1080x2175; samsung; SM-G950U; dreamqltesq; qcom",
+  "28/9; 480dpi; 1080x2175; samsung; SM-G955U; dream2qltesq; qcom",
+  "28/9; 480dpi; 1080x2175; samsung; SM-G960F; starlte; samsungexynos9810",
+  "28/9; 480dpi; 1080x2175; samsung; SM-G960U; starqltesq; qcom",
+  "28/9; 480dpi; 1080x2175; samsung; SM-N950U; greatqlte; qcom",
+  "28/9; 480dpi; 1080x2180; samsung; SM-A705FN; a70q; qcom",
+  "28/9; 480dpi; 1080x2190; HUAWEI/HONOR; COL-L29; HWCOL; kirin970",
+  "28/9; 480dpi; 1080x2190; HUAWEI; ANE-LX1; HWANE; hi6250",
+  "28/9; 480dpi; 1080x2196; OPPO; CPH1969; OP4863; mt6771",
+  "28/9; 480dpi; 1080x2201; realme; RMX1931; RMX1931L1; qcom",
+  "28/9; 480dpi; 1080x2208; OPPO; CPH1951; OP4B65L1; mt6779",
+  "28/9; 480dpi; 1080x2208; UMIDIGI; A5_Pro; A5_Pro; mt6763",
+  "28/9; 480dpi; 1080x2209; motorola; motorola one vision; kane_sprout; exynos9610",
+  "28/9; 480dpi; 1080x2209; samsung; SM-A405FM; a40; exynos7885",
+  "28/9; 480dpi; 1080x2209; samsung; SM-A405FN; a40; exynos7885",
+  "28/9; 480dpi; 1080x2214; vivo; vivo 1818; 1818; qcom",
+  "28/9; 480dpi; 1080x2220; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "28/9; 480dpi; 1080x2220; samsung; SM-G950U; dreamqltesq; qcom",
+  "28/9; 480dpi; 1080x2220; samsung; SM-G960U; starqltesq; qcom",
+  "28/9; 480dpi; 1080x2220; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "28/9; 480dpi; 1080x2224; HUAWEI/HONOR; STK-LX1; HWSTK-HF; kirin710",
+  "28/9; 480dpi; 1080x2224; HUAWEI; STK-LX1; HWSTK-HF; kirin710",
+  "28/9; 480dpi; 1080x2232; meizu; meizu 16Xs; meizu16Xs; qcom",
+  "28/9; 480dpi; 1080x2250; HUAWEI; INE-LX1; HWINE; kirin710",
+  "28/9; 480dpi; 1080x2252; HUAWEI; PAR-LX1; HWPAR; kirin970",
+  "28/9; 480dpi; 1080x2255; HUAWEI/HONOR; JSN-L21; HWJSN-H; kirin710",
+  "28/9; 480dpi; 1080x2257; motorola; motorola one action; troika_sprout; exynos9610",
+  "28/9; 480dpi; 1080x2259; HUAWEI/HONOR; HRY-LX1T; HWHRY-HF; kirin710",
+  "28/9; 480dpi; 1080x2259; HUAWEI; POT-LX3; HWPOT-H; kirin710",
+  "28/9; 480dpi; 1080x2264; OPPO; PBCM30; PBCM30; qcom",
+  "28/9; 480dpi; 1080x2267; vivo; vivo 1917; 1917; qcom",
+  "28/9; 480dpi; 1080x2268; OPPO; CPH1907; OP4B83L1; qcom",
+  "28/9; 480dpi; 1080x2310; vivo; V1938T; PD1938; exynos980",
+  "28/9; 480dpi; 1080x2324; samsung; SM-A705MN; a70q; qcom",
+  "28/9; 480dpi; 1080x2340; HUAWEI/HONOR; STK-LX1; HWSTK-HF; kirin710",
+  "28/9; 480dpi; 1080x2340; OPPO; CPH1823; CPH1823; mt6771",
+  "28/9; 480dpi; 1080x2340; OPPO; CPH1951; OP4B65L1; mt6779",
+  "28/9; 480dpi; 1440x2416; Razer/razer; Phone 2; aura; qcom",
+  "28/9; 480dpi; 1440x2416; Razer/razer; Phone 2; bolt; qcom",
+  "28/9; 480dpi; 1600x2454; HUAWEI; SHT-AL09; HWSHT; hi3660",
+  "28/9; 480dpi; 2033x1080; HUAWEI; FIG-LX1; HWFIG-H; hi6250",
+  "28/9; 480dpi; 2038x1080; HUAWEI; CLT-L29; HWCLT; kirin970",
+  "28/9; 480dpi; 2041x1080; HUAWEI/HONOR; BKL-L09; HWBKL; kirin970",
+  "28/9; 480dpi; 2061x1080; HUAWEI/HONOR; COL-L29; HWCOL; kirin970",
+  "28/9; 480dpi; 2076x1080; samsung; SM-A530F; jackpotlte; samsungexynos7885",
+  "28/9; 480dpi; 2076x1080; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "28/9; 480dpi; 2076x1080; samsung; SM-G950U; dreamqltesq; qcom",
+  "28/9; 480dpi; 2076x1080; samsung; SM-G960F; starlte; samsungexynos9810",
+  "28/9; 480dpi; 2111x1080; HUAWEI/HONOR; YAL-L21; HWYAL; kirin980",
+  "28/9; 480dpi; 2129x1080; Xiaomi; Mi 9 SE; grus; qcom",
+  "28/9; 480dpi; 2264x1080; OPPO; CPH1893; CPH1893L1; qcom",
+  "28/9; 480dpi; 2268x1080; OPPO; CPH1907; OP4B83L1; qcom",
+  "28/9; 490dpi; 1080x2130; Xiaomi/xiaomi; Redmi Note 8; ginkgo; qcom",
+  "28/9; 490dpi; 1080x2131; Xiaomi/xiaomi; Redmi Note 7; lavender; qcom",
+  "28/9; 490dpi; 1080x2138; Xiaomi; Mi Note 10 Pro; tucana; qcom",
+  "28/9; 490dpi; 2013x1080; Google/google; Pixel 3; blueline; blueline",
+  "28/9; 493dpi; 1080x2012; motorola; moto g(6; ali; qcom",
+  "28/9; 540dpi; 1080x1920; Xiaomi/xiaomi; Mi A1; tissot_sprout; qcom",
+  "28/9; 540dpi; 1080x1920; motorola; moto x4; payton; qcom",
+  "28/9; 540dpi; 1080x1920; samsung; SM-G611MT; on7xreflte; samsungexynos7870",
+  "28/9; 540dpi; 1080x1920; samsung; SM-J730F; j7y17lte; samsungexynos7870",
+  "28/9; 540dpi; 1080x1920; samsung; SM-J730G; j7y17lte; samsungexynos7870",
+  "28/9; 540dpi; 1080x1998; LGE/lge; LM-Q710.FGN; cv7a; cv7a",
+  "28/9; 540dpi; 1080x1998; Yulong/Coolpad; cp3705A; lithium; qcom",
+  "28/9; 540dpi; 1080x1998; motorola; Moto Z3 Play; beckham; qcom",
+  "28/9; 540dpi; 1080x1998; motorola; moto g(6 plus; evert_nt; qcom",
+  "28/9; 540dpi; 1080x1998; motorola; moto g(6; ali; qcom",
+  "28/9; 540dpi; 1080x1998; motorola; moto z3; messi; qcom",
+  "28/9; 540dpi; 1080x2002; samsung; SM-G970F; beyond0; exynos9820",
+  "28/9; 540dpi; 1080x2011; asus; ASUS_X00TD; ASUS_X00T_4; qcom",
+  "28/9; 540dpi; 1080x2014; asus; ZE620KL; ASUS_X00QD; qcom",
+  "28/9; 540dpi; 1080x2032; HUAWEI/HONOR; LLD-L31; HWLLD-H; hi6250",
+  "28/9; 540dpi; 1080x2032; HUAWEI; FIG-LX1; HWFIG-H; hi6250",
+  "28/9; 540dpi; 1080x2037; HUAWEI; CLT-L29; HWCLT; kirin970",
+  "28/9; 540dpi; 1080x2037; asus; ASUS_A001D; ASUS_A001D_1; qcom",
+  "28/9; 540dpi; 1080x2038; HUAWEI/HONOR; BND-L21; HWBND-H; hi6250",
+  "28/9; 540dpi; 1080x2038; HUAWEI; FLA-LX1; HWFLA-H; hi6250",
+  "28/9; 540dpi; 1080x2040; HUAWEI/HONOR; BKL-L09; HWBKL; kirin970",
+  "28/9; 540dpi; 1080x2045; motorola; moto g(8 plus; doha; qcom",
+  "28/9; 540dpi; 1080x2058; samsung; SM-A530F; jackpotlte; samsungexynos7885",
+  "28/9; 540dpi; 1080x2058; samsung; SM-A730F; jackpot2lte; samsungexynos7885",
+  "28/9; 540dpi; 1080x2058; samsung; SM-A750FN; a7y18lte; samsungexynos7885",
+  "28/9; 540dpi; 1080x2058; samsung; SM-G892A; cruiserlteatt; qcom",
+  "28/9; 540dpi; 1080x2058; samsung; SM-G892U; cruiserltesq; qcom",
+  "28/9; 540dpi; 1080x2058; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "28/9; 540dpi; 1080x2058; samsung; SM-G950U1; dreamqlteue; qcom",
+  "28/9; 540dpi; 1080x2058; samsung; SM-G950U; dreamqltesq; qcom",
+  "28/9; 540dpi; 1080x2058; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "28/9; 540dpi; 1080x2058; samsung; SM-G955U; dream2qltesq; qcom",
+  "28/9; 540dpi; 1080x2058; samsung; SM-G960F; starlte; samsungexynos9810",
+  "28/9; 540dpi; 1080x2058; samsung; SM-G960U; starqltesq; qcom",
+  "28/9; 540dpi; 1080x2058; samsung; SM-N950U; greatqlte; qcom",
+  "28/9; 540dpi; 1080x2060; HUAWEI/HONOR; COL-L29; HWCOL; kirin970",
+  "28/9; 540dpi; 1080x2060; HUAWEI; ANE-LX1; HWANE; hi6250",
+  "28/9; 540dpi; 1080x2090; HUAWEI/HONOR; PCT-L29; HWPCT; kirin980",
+  "28/9; 540dpi; 1080x2095; samsung; SM-A505F; a50; exynos9610",
+  "28/9; 540dpi; 1080x2095; samsung; SM-A505FN; a50; exynos9610",
+  "28/9; 540dpi; 1080x2095; samsung; SM-A505GT; a50; exynos9610",
+  "28/9; 540dpi; 1080x2107; HUAWEI/HONOR; MAR-LX1H; HWMAR; kirin710",
+  "28/9; 540dpi; 1080x2107; HUAWEI; MAR-LX1A; HWMAR; kirin710",
+  "28/9; 540dpi; 1080x2110; HUAWEI/HONOR; YAL-L21; HWYAL; kirin980",
+  "28/9; 540dpi; 1080x2130; Xiaomi/xiaomi; Redmi Note 8T; willow; qcom",
+  "28/9; 540dpi; 1080x2131; Xiaomi/xiaomi; Redmi Note 7; lavender; qcom",
+  "28/9; 540dpi; 1080x2137; HUAWEI/HONOR; JSN-L21; HWJSN-H; kirin710",
+  "28/9; 540dpi; 1080x2139; HUAWEI/HONOR; HRY-LX1; HWHRY-H; kirin710",
+  "28/9; 540dpi; 1080x2139; HUAWEI/HONOR; HRY-LX1T; HWHRY-HF; kirin710",
+  "28/9; 540dpi; 1080x2169; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "28/9; 540dpi; 1080x2191; motorola; motorola one action; troika_sprout; exynos9610",
+  "28/9; 540dpi; 1080x2250; HUAWEI; INE-LX1; HWINE; kirin710",
+  "28/9; 540dpi; 2058x1080; samsung; SM-N950U; greatqlte; qcom",
+  "28/9; 544dpi; 1440x2397; motorola; Moto Z (2; nash; qcom",
+  "28/9; 560dpi; 1440x2392; Google/google; Pixel XL; marlin; marlin",
+  "28/9; 560dpi; 1440x2392; LGE/lge; LG-H918; elsa; qcom",
+  "28/9; 560dpi; 1440x2560; HMD Global/Nokia; TA-1004; NB1; qcom",
+  "28/9; 560dpi; 1440x2621; Google/google; Pixel 3 XL; crosshatch; crosshatch",
+  "28/9; 560dpi; 1440x2698; LGE/lge; LG-H870; lucye; lucye",
+  "28/9; 560dpi; 1440x2712; Google/google; Pixel 2 XL; taimen; taimen",
+  "28/9; 560dpi; 1440x2712; LGE/lge; LM-V350; judyp; judyp",
+  "28/9; 560dpi; 1440x2730; samsung; SM-G975F; beyond2; exynos9820",
+  "28/9; 560dpi; 1440x2759; samsung; SM-N975F; d2s; exynos9825",
+  "28/9; 560dpi; 1440x2792; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "28/9; 560dpi; 1440x2792; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "28/9; 560dpi; 1440x2792; samsung; SM-G955N; dream2lteks; samsungexynos8895",
+  "28/9; 560dpi; 1440x2792; samsung; SM-G955U; dream2qltesq; qcom",
+  "28/9; 560dpi; 1440x2792; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "28/9; 560dpi; 1440x2792; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "28/9; 560dpi; 1440x2792; samsung; SM-N950U; greatqlte; qcom",
+  "28/9; 560dpi; 1440x2792; samsung; SM-N960F; crownlte; samsungexynos9810",
+  "28/9; 560dpi; 1440x2792; samsung; SM-N960U; crownqltesq; qcom",
+  "28/9; 560dpi; 1440x2858; LGE/lge; LG-G710; judyln; judyln",
+  "28/9; 560dpi; 1440x2858; LGE/lge; LM-G710; judyln; judyln",
+  "28/9; 560dpi; 1440x2858; LGE/lge; LM-G710VM; judyln; judyln",
+  "28/9; 560dpi; 1440x2861; LGE/lge; LM-V450; flashlm; flashlm",
+  "28/9; 560dpi; 1440x2907; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "28/9; 560dpi; 1440x2907; samsung; SM-G955U; dream2qltesq; qcom",
+  "28/9; 560dpi; 1440x2907; samsung; SM-N950U; greatqlte; qcom",
+  "28/9; 560dpi; 1440x2960; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "28/9; 560dpi; 1440x2960; samsung; SM-G955F; dream2lte; samsungexynos8895",
+  "28/9; 560dpi; 1440x2960; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "28/9; 560dpi; 1440x2960; samsung; SM-N950U; greatqlte; qcom",
+  "28/9; 640dpi; 1440x2368; motorola; Moto Z (2; nash; qcom",
+  "28/9; 640dpi; 1440x2560; HTC/htc; HTC U11; htc_ocndugl; htc_ocn",
+  "28/9; 640dpi; 1440x2560; motorola; Moto Z (2; nash; qcom",
+  "28/9; 640dpi; 1440x2672; LGE/lge; LG-H870; lucye; lucye",
+  "28/9; 640dpi; 1440x2672; LGE/lge; LG-H870DS; lucye; lucye",
+  "28/9; 640dpi; 1440x2672; LGE/lge; LG-H871; lucye; lucye",
+  "28/9; 640dpi; 1440x2672; LGE/lge; VS988; lucye; lucye",
+  "28/9; 640dpi; 1440x2678; HTC/htc; HTC U12+; htc_imedugl; htc_ime",
+  "28/9; 640dpi; 1440x2688; LGE/lge; LM-V350; judyp; judyp",
+  "28/9; 640dpi; 1440x2699; samsung; SM-G973F; beyond1; exynos9820",
+  "28/9; 640dpi; 1440x2712; LGE/lge; LG-H931; joan; joan",
+  "28/9; 640dpi; 1440x2712; LGE/lge; LG-H932; joan; joan",
+  "28/9; 640dpi; 1440x2712; LGE/lge; VS996; joan; joan",
+  "28/9; 640dpi; 1440x2768; samsung; SM-G892A; cruiserlteatt; qcom",
+  "28/9; 640dpi; 1440x2768; samsung; SM-G892U; cruiserltesq; qcom",
+  "28/9; 640dpi; 1440x2768; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "28/9; 640dpi; 1440x2768; samsung; SM-G950U; dreamqltesq; qcom",
+  "28/9; 640dpi; 1440x2768; samsung; SM-G955U; dream2qltesq; qcom",
+  "28/9; 640dpi; 1440x2768; samsung; SM-G960U; starqltesq; qcom",
+  "28/9; 640dpi; 1440x2768; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "28/9; 640dpi; 1440x2768; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "28/9; 640dpi; 1440x2768; samsung; SM-N950U; greatqlte; qcom",
+  "28/9; 640dpi; 1440x2768; samsung; SM-N960U1; crownqlteue; qcom",
+  "28/9; 640dpi; 1440x2834; LGE/lge; LM-G710; judyln; judyln",
+  "28/9; 640dpi; 1440x2834; LGE/lge; LM-G710VM; judyln; judyln",
+  "28/9; 640dpi; 1440x2900; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "28/9; 640dpi; 1440x2900; samsung; SM-G950U; dreamqltesq; qcom",
+  "28/9; 640dpi; 1440x2900; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "28/9; 640dpi; 1440x2960; samsung; SM-G950F; dreamlte; samsungexynos8895",
+  "28/9; 720dpi; 1440x2344; motorola; Moto Z (2; nash; qcom",
+  "28/9; 720dpi; 1440x2560; HMD Global/Nokia; TA-1004; NB1; qcom",
+  "28/9; 720dpi; 1440x2560; HUAWEI; VKY-AL00; HWVKY; hi3660",
+  "28/9; 720dpi; 1440x2691; LGE/lge; VS996; joan; joan",
+  "28/9; 720dpi; 1440x2744; samsung; SM-G892A; cruiserlteatt; qcom",
+  "28/9; 720dpi; 1440x2744; samsung; SM-G950U; dreamqltesq; qcom",
+  "28/9; 720dpi; 1440x2744; samsung; SM-G955U; dream2qltesq; qcom",
+  "28/9; 720dpi; 1440x2744; samsung; SM-N950F; greatlte; samsungexynos8895",
+  "28/9; 720dpi; 1440x2744; samsung; SM-N950U; greatqlte; qcom",
+  "28/9; 720dpi; 1440x2810; LGE/lge; LG-G710; judyln; judyln",
+  "28/9; 720dpi; 1440x2810; LGE/lge; LM-G710; judyln; judyln",
+  "28/9; 720dpi; 1440x2810; LGE/lge; LM-G710VM; judyln; judyln",
+  "28/9; 720dpi; 1440x2814; LGE/lge; LM-V405; judypn; judypn",
+  "28/9; 720dpi; 1440x2960; samsung; SM-G950U; dreamqltesq; qcom",
+  "29/10; 420dpi; 1080x1920; OnePlus; ONEPLUS A5000; OnePlus5; qcom",
+  "29/10; 420dpi; 1080x2034; HMD Global/Nokia; Nokia 7 plus; B2N_sprout; qcom",
+  "29/10; 420dpi; 1080x2034; HMD Global/Nokia; Nokia 8.1; PNX_sprout; qcom",
+  "29/10; 420dpi; 1080x2034; LGE/lge; LM-Q720; cv7as; cv7as",
+  "29/10; 420dpi; 1080x2034; OnePlus; ONEPLUS A5010; OnePlus5T; qcom",
+  "29/10; 420dpi; 1080x2038; samsung; SM-G970F; beyond0; exynos9820",
+  "29/10; 420dpi; 1080x2038; samsung; SM-G970U1; beyond0q; qcom",
+  "29/10; 420dpi; 1080x2038; samsung; SM-G970U; beyond0q; qcom",
+  "29/10; 420dpi; 1080x2042; samsung; SM-G9730; beyond1q; qcom",
+  "29/10; 420dpi; 1080x2042; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 420dpi; 1080x2042; samsung; SM-G973U1; beyond1q; qcom",
+  "29/10; 420dpi; 1080x2042; samsung; SM-G973U; beyond1q; qcom",
+  "29/10; 420dpi; 1080x2047; samsung; SM-G9750; beyond2q; qcom",
+  "29/10; 420dpi; 1080x2047; samsung; SM-G975F; beyond2; exynos9820",
+  "29/10; 420dpi; 1080x2047; samsung; SM-G975U1; beyond2q; qcom",
+  "29/10; 420dpi; 1080x2047; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 420dpi; 1080x2050; samsung; SM-G977U; beyondxq; qcom",
+  "29/10; 420dpi; 1080x2058; motorola; moto g stylus; sofiap; qcom",
+  "29/10; 420dpi; 1080x2058; motorola; moto g(8 power; sofiar; qcom",
+  "29/10; 420dpi; 1080x2064; samsung; SM-N9700; d1q; qcom",
+  "29/10; 420dpi; 1080x2064; samsung; SM-N970F; d1; exynos9825",
+  "29/10; 420dpi; 1080x2064; samsung; SM-N970U1; d1q; qcom",
+  "29/10; 420dpi; 1080x2064; samsung; SM-N970U; d1q; qcom",
+  "29/10; 420dpi; 1080x2065; HMD Global/Nokia; Nokia 6.1 Plus; DRG_sprout; qcom",
+  "29/10; 420dpi; 1080x2065; HMD Global/Nokia; Nokia 7.1; CTL_sprout; qcom",
+  "29/10; 420dpi; 1080x2069; samsung; SM-N9750; d2q; qcom",
+  "29/10; 420dpi; 1080x2069; samsung; SM-N975F; d2s; exynos9825",
+  "29/10; 420dpi; 1080x2069; samsung; SM-N975U1; d2q; qcom",
+  "29/10; 420dpi; 1080x2069; samsung; SM-N975U; d2q; qcom",
+  "29/10; 420dpi; 1080x2069; samsung; SM-N976N; d2x; exynos9825",
+  "29/10; 420dpi; 1080x2069; samsung; SM-N976U; d2xq2; qcom",
+  "29/10; 420dpi; 1080x2069; samsung; SM-N976V; d2xq; qcom",
+  "29/10; 420dpi; 1080x2075; OnePlus; ONEPLUS A6003; OnePlus6; qcom",
+  "29/10; 420dpi; 1080x2081; motorola; moto g(8 plus; doha; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-A605FN; a6plte; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-A605GN; a6plte; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-A750F; a7y18lte; exynos7885",
+  "29/10; 420dpi; 1080x2094; samsung; SM-A750FN; a7y18lte; exynos7885",
+  "29/10; 420dpi; 1080x2094; samsung; SM-A750G; a7y18lte; exyno",
+  "29/10; 420dpi; 1080x2094; samsung; SM-A750G; a7y18lte; exynos7885",
+  "29/10; 420dpi; 1080x2094; samsung; SM-A750GN; a7y18lte; exynos7885",
+  "29/10; 420dpi; 1080x2094; samsung; SM-A920F; a9y18qlte; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-G9600; starqltechn; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-G960F; starlte; samsungexynos9810",
+  "29/10; 420dpi; 1080x2094; samsung; SM-G960U1; starqlteue; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-G960U; starqltesq; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-G9650; star2qltechn; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "29/10; 420dpi; 1080x2094; samsung; SM-G965U1; star2qlteue; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-G965U; star2qltesq; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-N9600; crownqltechn; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-N960F; crownlte; samsungexynos9810",
+  "29/10; 420dpi; 1080x2094; samsung; SM-N960N; crownlteks; samsungexynos9810",
+  "29/10; 420dpi; 1080x2094; samsung; SM-N960U1; crownqlteue; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-N960U; crownqltesq; qcom",
+  "29/10; 420dpi; 1080x2094; samsung; SM-N960W; crownqltecs; qcom",
+  "29/10; 420dpi; 1080x2121; motorola; moto g power; sofia; qcom",
+  "29/10; 420dpi; 1080x2121; motorola; moto g stylus; sofiap; qcom",
+  "29/10; 420dpi; 1080x2121; motorola; moto g(8 power; sofiar; qcom",
+  "29/10; 420dpi; 1080x2125; samsung; SM-G970F; beyond0; exynos9820",
+  "29/10; 420dpi; 1080x2129; samsung; SM-G9730; beyond1q; qcom",
+  "29/10; 420dpi; 1080x2129; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 420dpi; 1080x2129; samsung; SM-G973U; beyond1q; qcom",
+  "29/10; 420dpi; 1080x2129; samsung; SM-M205FN; m20lte; exynos7904",
+  "29/10; 420dpi; 1080x2129; samsung; SM-M205M; m20lte; exynos7904",
+  "29/10; 420dpi; 1080x2131; samsung; SM-A305F; a30; exynos7904",
+  "29/10; 420dpi; 1080x2131; samsung; SM-A305FN; a30; exynos7904",
+  "29/10; 420dpi; 1080x2131; samsung; SM-A305G; a30; exynos7904",
+  "29/10; 420dpi; 1080x2131; samsung; SM-A305GT; a30; exynos7904",
+  "29/10; 420dpi; 1080x2131; samsung; SM-A305YN; a30; exynos7904",
+  "29/10; 420dpi; 1080x2131; samsung; SM-A505F; a50; exynos9610",
+  "29/10; 420dpi; 1080x2131; samsung; SM-A505FM; a50; exynos9610",
+  "29/10; 420dpi; 1080x2131; samsung; SM-A505FN; a50; exynos9610",
+  "29/10; 420dpi; 1080x2131; samsung; SM-A505G; a50; exynos9610",
+  "29/10; 420dpi; 1080x2131; samsung; SM-A505GT; a50; exynos9610",
+  "29/10; 420dpi; 1080x2131; samsung; SM-A505U1; a50; exynos9610",
+  "29/10; 420dpi; 1080x2131; samsung; SM-A505U; a50; exynos9610",
+  "29/10; 420dpi; 1080x2131; samsung; SM-M305F; m30lte; exynos7904",
+  "29/10; 420dpi; 1080x2131; samsung; SM-M305M; m30lte; exynos7904",
+  "29/10; 420dpi; 1080x2131; samsung; SM-M307F; m30s; exynos9611",
+  "29/10; 420dpi; 1080x2131; samsung; SM-M307FN; m30s; exynos9611",
+  "29/10; 420dpi; 1080x2131; samsung; SM-M315F; m31; exynos9611",
+  "29/10; 420dpi; 1080x2132; HMD Global/Nokia; Nokia 7.2; DDV_sprout; qcom",
+  "29/10; 420dpi; 1080x2134; OnePlus; GM1900; OnePlus7; qcom",
+  "29/10; 420dpi; 1080x2134; OnePlus; GM1903; OnePlus7; qcom",
+  "29/10; 420dpi; 1080x2134; OnePlus; ONEPLUS A6010; OnePlus6T; qcom",
+  "29/10; 420dpi; 1080x2134; OnePlus; ONEPLUS A6013; OnePlus6T; qcom",
+  "29/10; 420dpi; 1080x2134; OnePlus; ONEPLUS A6013; OnePlus6TSingle; qcom",
+  "29/10; 420dpi; 1080x2134; samsung; SM-G975F; beyond2; exynos9820",
+  "29/10; 420dpi; 1080x2134; samsung; SM-G975U1; beyond2q; qcom",
+  "29/10; 420dpi; 1080x2134; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 420dpi; 1080x2137; motorola; motorola one zoom; parker; qcom",
+  "29/10; 420dpi; 1080x2138; LGE/lge; LM-G850; mh2lm; mh2lm",
+  "29/10; 420dpi; 1080x2138; Realme; Realme XT; RMX1921; qcom",
+  "29/10; 420dpi; 1080x2144; LGE/lge; LM-G820; alphalm; alphalm",
+  "29/10; 420dpi; 1080x2144; motorola; moto g(8 plus; doha; qcom",
+  "29/10; 420dpi; 1080x2147; LGE/lge; LM-V405; judypn; judypn",
+  "29/10; 420dpi; 1080x2147; LGE/lge; LM-V450; flashlm; flashlm",
+  "29/10; 420dpi; 1080x2151; samsung; SM-N970F; d1; exynos9825",
+  "29/10; 420dpi; 1080x2151; samsung; SM-N970U; d1q; qcom",
+  "29/10; 420dpi; 1080x2156; samsung; SM-N975F; d2s; exynos9825",
+  "29/10; 420dpi; 1080x2156; samsung; SM-N975U; d2q; qcom",
+  "29/10; 420dpi; 1080x2159; asus; ZS620KL; ASUS_Z01R_1; qcom",
+  "29/10; 420dpi; 1080x2168; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 420dpi; 1080x2171; OnePlus; IN2013; OnePlus8; qcom",
+  "29/10; 420dpi; 1080x2171; OnePlus; IN2019; OnePlus8VZW; qcom",
+  "29/10; 420dpi; 1080x2173; samsung; SM-G975F; beyond2; exynos9820",
+  "29/10; 420dpi; 1080x2173; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 420dpi; 1080x2174; samsung; SM-G975U1; beyond2q; qcom",
+  "29/10; 420dpi; 1080x2179; motorola; motorola one zoom; parker; qcom",
+  "29/10; 420dpi; 1080x2181; samsung; SM-A605GN; a6plte; qcom",
+  "29/10; 420dpi; 1080x2181; samsung; SM-A750F; a7y18lte; exynos7885",
+  "29/10; 420dpi; 1080x2181; samsung; SM-A750FN; a7y18lte; exynos7885",
+  "29/10; 420dpi; 1080x2181; samsung; SM-A750G; a7y18lte; exynos7885",
+  "29/10; 420dpi; 1080x2181; samsung; SM-A920F; a9y18qlte; qcom",
+  "29/10; 420dpi; 1080x2181; samsung; SM-G770F; r5q; qcom",
+  "29/10; 420dpi; 1080x2181; samsung; SM-G960F; starlte; samsungexynos9810",
+  "29/10; 420dpi; 1080x2181; samsung; SM-G960U; starqltesq; qcom",
+  "29/10; 420dpi; 1080x2181; samsung; SM-G9650; star2qltechn; qcom",
+  "29/10; 420dpi; 1080x2181; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "29/10; 420dpi; 1080x2181; samsung; SM-G965U1; star2qlteue; qcom",
+  "29/10; 420dpi; 1080x2181; samsung; SM-G965U; star2qltesq; qcom",
+  "29/10; 420dpi; 1080x2181; samsung; SM-N9600; crownqltechn; qcom",
+  "29/10; 420dpi; 1080x2181; samsung; SM-N960U; crownqltesq; qcom",
+  "29/10; 420dpi; 1080x2183; samsung; SM-A715F; a71; qcom",
+  "29/10; 420dpi; 1080x2183; samsung; SM-A716U; a71xq; qcom",
+  "29/10; 420dpi; 1080x2184; samsung; SM-A415F; a41; mt6768",
+  "29/10; 420dpi; 1080x2186; samsung; SM-A515F; a51; exynos9611",
+  "29/10; 420dpi; 1080x2186; samsung; SM-A515U; a51; exynos9611",
+  "29/10; 420dpi; 1080x2189; samsung; SM-N770F; r7; exynos9810",
+  "29/10; 420dpi; 1080x2193; OnePlus; GM1913; OnePlus7Pro; qcom",
+  "29/10; 420dpi; 1080x2195; samsung; SM-A315F; a31; mt6768",
+  "29/10; 420dpi; 1080x2195; samsung; SM-A315G; a31; mt6768",
+  "29/10; 420dpi; 1080x2196; samsung; SM-G981U; x1q; qcom",
+  "29/10; 420dpi; 1080x2198; samsung; SM-A7050; a70q; qcom",
+  "29/10; 420dpi; 1080x2198; samsung; SM-A705FN; a70q; qcom",
+  "29/10; 420dpi; 1080x2198; samsung; SM-A705MN; a70q; qcom",
+  "29/10; 420dpi; 1080x2198; samsung; SM-A705U; a70q; qcom",
+  "29/10; 420dpi; 1080x2198; samsung; SM-A908B; r3q; qcom",
+  "29/10; 420dpi; 1080x2200; motorola; motorola one zoom; parker; qcom",
+  "29/10; 420dpi; 1080x2200; samsung; SM-G988B; z3s; exynos990",
+  "29/10; 420dpi; 1080x2200; samsung; SM-G988U1; z3q; qcom",
+  "29/10; 420dpi; 1080x2200; samsung; SM-G988U; z3q; qcom",
+  "29/10; 420dpi; 1080x2201; OnePlus; ONEPLUS A6000; OnePlus6; qcom",
+  "29/10; 420dpi; 1080x2201; OnePlus; ONEPLUS A6003; OnePlus6; qcom",
+  "29/10; 420dpi; 1080x2201; samsung; SM-G985F; y2s; exynos990",
+  "29/10; 420dpi; 1080x2201; samsung; SM-G986B; y2s; exynos990",
+  "29/10; 420dpi; 1080x2201; samsung; SM-G986U1; y2q; qcom",
+  "29/10; 420dpi; 1080x2201; samsung; SM-G986U; y2q; qcom",
+  "29/10; 420dpi; 1080x2206; OnePlus; HD1900; OnePlus7T; qcom",
+  "29/10; 420dpi; 1080x2206; OnePlus; HD1903; OnePlus7T; qcom",
+  "29/10; 420dpi; 1080x2206; OnePlus; HD1905; OnePlus7T; qcom",
+  "29/10; 420dpi; 1080x2207; OnePlus; HD1907; OnePlus7TTMO; qcom",
+  "29/10; 420dpi; 1080x2214; asus; ASUS_I001DD; ASUS_I001_1; qcom",
+  "29/10; 420dpi; 1080x2214; asus; ASUS_I01WD; ASUS_I01WD; qcom",
+  "29/10; 420dpi; 1080x2214; motorola; motorola one hyper; def; qcom",
+  "29/10; 420dpi; 1080x2216; samsung; SM-M205FN; m20lte; exynos7904",
+  "29/10; 420dpi; 1080x2218; OnePlus; ONEPLUS A6010; OnePlus6T; qcom",
+  "29/10; 420dpi; 1080x2218; samsung; SM-A305FN; a30; exynos7904",
+  "29/10; 420dpi; 1080x2218; samsung; SM-A305GT; a30; exynos7904",
+  "29/10; 420dpi; 1080x2218; samsung; SM-A505FM; a50; exynos9610",
+  "29/10; 420dpi; 1080x2218; samsung; SM-A505FN; a50; exynos9610",
+  "29/10; 420dpi; 1080x2218; samsung; SM-A505G; a50; exynos9610",
+  "29/10; 420dpi; 1080x2218; samsung; SM-A505GT; a50; exynos9610",
+  "29/10; 420dpi; 1080x2218; samsung; SM-M305M; m30lte; exynos7904",
+  "29/10; 420dpi; 1080x2218; samsung; SM-M307F; m30s; exynos9611",
+  "29/10; 420dpi; 1080x2218; samsung; SM-M307FN; m30s; exynos9611",
+  "29/10; 420dpi; 1080x2218; samsung; SM-S506DL; a50; exynos9610",
+  "29/10; 420dpi; 1080x2220; samsung; SM-A750F; a7y18lte; exynos7885",
+  "29/10; 420dpi; 1080x2220; samsung; SM-A750FN; a7y18lte; exynos7885",
+  "29/10; 420dpi; 1080x2220; samsung; SM-G9650; star2qltechn; qcom",
+  "29/10; 420dpi; 1080x2220; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "29/10; 420dpi; 1080x2220; samsung; SM-N9600; crownqltechn; qcom",
+  "29/10; 420dpi; 1080x2229; OnePlus; AC2003; Nord; qcom",
+  "29/10; 420dpi; 1080x2232; LGE/lge; LM-Q730; mdh50lm; mdh50lm",
+  "29/10; 420dpi; 1080x2255; OnePlus; IN2017; OnePlus8TMO; qcom",
+  "29/10; 420dpi; 1080x2256; motorola; motorola one hyper; def; qcom",
+  "29/10; 420dpi; 1080x2257; samsung; SM-A305FN; a30; exynos7904",
+  "29/10; 420dpi; 1080x2257; samsung; SM-A505FM; a50; exynos9610",
+  "29/10; 420dpi; 1080x2258; LGE/lge; LM-V600; timelm; timelm",
+  "29/10; 420dpi; 1080x2260; OnePlus; GM1900; OnePlus7; qcom",
+  "29/10; 420dpi; 1080x2260; OnePlus; ONEPLUS A6010; OnePlus6T; qcom",
+  "29/10; 420dpi; 1080x2260; OnePlus; ONEPLUS A6013; OnePlus6T; qcom",
+  "29/10; 420dpi; 1080x2260; Xiaomi; Redmi Note 7 Pro; violet; qcom",
+  "29/10; 420dpi; 1080x2270; samsung; SM-A715F; a71; qcom",
+  "29/10; 420dpi; 1080x2273; samsung; SM-A515F; a51; exynos9611",
+  "29/10; 420dpi; 1080x2274; samsung; SM-A805F; r1q; qcom",
+  "29/10; 420dpi; 1080x2276; samsung; SM-N770F; r7; exynos9810",
+  "29/10; 420dpi; 1080x2285; samsung; SM-A705GM; a70q; qcom",
+  "29/10; 420dpi; 1080x2287; samsung; SM-G988B; z3s; exynos990",
+  "29/10; 420dpi; 1080x2287; samsung; SM-G988U; z3q; qcom",
+  "29/10; 420dpi; 1080x2288; samsung; SM-G985F; y2s; exynos990",
+  "29/10; 420dpi; 1080x2288; samsung; SM-G986U; y2q; qcom",
+  "29/10; 420dpi; 1080x2297; OnePlus; IN2010; OnePlus8; qcom",
+  "29/10; 420dpi; 1080x2309; samsung; SM-A715F; a71; qcom",
+  "29/10; 420dpi; 1080x2312; samsung; SM-A515W; a51; exynos9611",
+  "29/10; 420dpi; 1080x2316; LGE/lge; LM-Q730; mdh50lm; mdh50lm",
+  "29/10; 420dpi; 1080x2324; samsung; SM-A705FN; a70q; qcom",
+  "29/10; 420dpi; 1080x2324; samsung; SM-A705MN; a70q; qcom",
+  "29/10; 420dpi; 1080x2326; samsung; SM-G988U; z3q; qcom",
+  "29/10; 420dpi; 1080x2340; OnePlus; HD1910; OnePlus7TPro; qcom",
+  "29/10; 420dpi; 1080x2342; LGE/lge; LM-V600; timelm; timelm",
+  "29/10; 420dpi; 1080x2361; samsung; SM-A805F; r1q; qcom",
+  "29/10; 420dpi; 1080x2394; Sony; I3223; I3223; qcom",
+  "29/10; 420dpi; 1080x2394; Sony; J9210; J9210; qcom",
+  "29/10; 420dpi; 1096x2434; Sony; J9110; J9110; qcom",
+  "29/10; 420dpi; 1536x1922; samsung; SM-F900F; winner; qcom",
+  "29/10; 420dpi; 1600x2434; samsung; SM-T725; gts4lv; qcom",
+  "29/10; 420dpi; 2042x1080; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 420dpi; 2042x1080; samsung; SM-G973U1; beyond1q; qcom",
+  "29/10; 420dpi; 2042x1080; samsung; SM-G973U; beyond1q; qcom",
+  "29/10; 420dpi; 2047x1080; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 420dpi; 2069x1080; samsung; SM-N975U; d2q; qcom",
+  "29/10; 420dpi; 2094x1080; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "29/10; 420dpi; 2094x1080; samsung; SM-G965U; star2qltesq; qcom",
+  "29/10; 420dpi; 2094x1080; samsung; SM-N960F; crownlte; samsungexynos9810",
+  "29/10; 420dpi; 2094x1080; samsung; SM-N960U; crownqltesq; qcom",
+  "29/10; 420dpi; 2131x1080; samsung; SM-A505FN; a50; exynos9610",
+  "29/10; 420dpi; 2134x1080; OnePlus; ONEPLUS A6013; OnePlus6T; qcom",
+  "29/10; 420dpi; 2200x1080; samsung; SM-G988U; z3q; qcom",
+  "29/10; 420dpi; 2218x1080; samsung; SM-A505G; a50; exynos9610",
+  "29/10; 420dpi; 2260x1080; OnePlus; ONEPLUS A6010; OnePlus6T; qcom",
+  "29/10; 420dpi; 2274x1080; samsung; SM-A805F; r1q; qcom",
+  "29/10; 430dpi; 1080x2283; OnePlus; IN2020; OnePlus8Pro; qcom",
+  "29/10; 436dpi; 1080x2171; Xiaomi/Redmi; Redmi Note 9S; curtana; qcom",
+  "29/10; 440dpi; 1080x2027; Xiaomi; POCO F1; beryllium; qcom",
+  "29/10; 440dpi; 1080x2027; Xiaomi; POCOPHONE F1; beryllium; qcom",
+  "29/10; 440dpi; 1080x2028; Google/google; Pixel 3; blueline; blueline",
+  "29/10; 440dpi; 1080x2028; Xiaomi; MI 8; dipper; qcom",
+  "29/10; 440dpi; 1080x2029; Xiaomi; MI 8 Pro; equuleus; qcom",
+  "29/10; 440dpi; 1080x2029; Xiaomi; MI 8; dipper; qcom",
+  "29/10; 440dpi; 1080x2030; Xiaomi; MI MAX 3; nitrogen; qcom",
+  "29/10; 440dpi; 1080x2030; Xiaomi; Mi MIX 2S; polaris; qcom",
+  "29/10; 440dpi; 1080x2068; Xiaomi; MI 8 Lite; platina; qcom",
+  "29/10; 440dpi; 1080x2088; Google/google; Pixel 3a; sargo; sargo",
+  "29/10; 440dpi; 1080x2110; Xiaomi/Redmi; M2003J15SC; merlin; mt6768",
+  "29/10; 440dpi; 1080x2110; Xiaomi/Redmi; M2003J15SC; merlinnfc; mt6768",
+  "29/10; 440dpi; 1080x2116; Google/google; Pixel 3; blueline; blueline",
+  "29/10; 440dpi; 1080x2120; Xiaomi; Mi 10; umi; qcom",
+  "29/10; 440dpi; 1080x2130; Xiaomi/xiaomi; Redmi Note 7; lavender; qcom",
+  "29/10; 440dpi; 1080x2131; Xiaomi/xiaomi; Redmi Note 7; lavender; qcom",
+  "29/10; 440dpi; 1080x2134; Xiaomi/Redmi; M2004J19C; galahad; mt6768",
+  "29/10; 440dpi; 1080x2134; Xiaomi/Redmi; M2004J19C; lancelot; mt6768",
+  "29/10; 440dpi; 1080x2134; Xiaomi/Redmi; Redmi Note 8 Pro; begonia; mt6785",
+  "29/10; 440dpi; 1080x2134; Xiaomi/Redmi; Redmi Note 8 Pro; begoniain; mt6785",
+  "29/10; 440dpi; 1080x2135; Xiaomi; MI 9; cepheus; qcom",
+  "29/10; 440dpi; 1080x2135; Xiaomi; Mi 9 Lite; pyxis; qcom",
+  "29/10; 440dpi; 1080x2138; Xiaomi; Mi Note 10 Lite; toco; qcom",
+  "29/10; 440dpi; 1080x2138; Xiaomi; Mi Note 10 Pro; tucana; qcom",
+  "29/10; 440dpi; 1080x2138; Xiaomi; Mi Note 10; tucana; qcom",
+  "29/10; 440dpi; 1080x2148; Google/google; Pixel 4; flame; flame",
+  "29/10; 440dpi; 1080x2170; Xiaomi/Redmi; Redmi Note 9 Pro; joyeuse; qcom",
+  "29/10; 440dpi; 1080x2170; Xiaomi/Redmi; Redmi Note 9S; curtana; qcom",
+  "29/10; 440dpi; 1080x2174; motorola; motorola edge; racer; qcom",
+  "29/10; 440dpi; 1080x2176; Google/google; Pixel 3a; sargo; sargo",
+  "29/10; 440dpi; 1080x2196; Xiaomi/Redmi; M2003J15SC; merlin; mt6768",
+  "29/10; 440dpi; 1080x2201; Xiaomi; M2002J9G; monet; qcom",
+  "29/10; 440dpi; 1080x2210; Xiaomi; Mi 9T Pro; raphael; qcom",
+  "29/10; 440dpi; 1080x2210; Xiaomi; Mi 9T; davinci; qcom",
+  "29/10; 440dpi; 1080x2210; Xiaomi; Redmi K20 Pro; raphaelin; qcom",
+  "29/10; 440dpi; 1080x2220; Xiaomi/Redmi; Redmi Note 8 Pro; begonia; mt6785",
+  "29/10; 440dpi; 1080x2221; Xiaomi; MI 9; cepheus; qcom",
+  "29/10; 440dpi; 1080x2221; Xiaomi; Mi 9 Lite; pyxis; qcom",
+  "29/10; 440dpi; 1080x2224; Xiaomi; Mi Note 10 Lite; toco; qcom",
+  "29/10; 440dpi; 1080x2224; Xiaomi; Mi Note 10 Pro; tucana; qcom",
+  "29/10; 440dpi; 1080x2236; Google/google; Pixel 4; flame; flame",
+  "29/10; 440dpi; 1080x2270; Xiaomi/POCO; POCO F2 Pro; lmi; qcom",
+  "29/10; 440dpi; 1080x2296; Xiaomi; Mi 9T Pro; raphael; qcom",
+  "29/10; 440dpi; 1080x2296; Xiaomi; Mi 9T; davinci; qcom",
+  "29/10; 440dpi; 2068x1080; Xiaomi; MI 8 Lite; platina; qcom",
+  "29/10; 440dpi; 2198x1080; Xiaomi; MI 8 Lite; platina; qcom",
+  "29/10; 446dpi; 1080x2026; Google/google; Pixel 3a XL; bonito; bonito",
+  "29/10; 446dpi; 1080x2115; Google/google; Pixel 3a XL; bonito; bonito",
+  "29/10; 450dpi; 1080x2029; samsung; SM-G970U; beyond0q; qcom",
+  "29/10; 450dpi; 1080x2033; samsung; SM-G973U; beyond1q; qcom",
+  "29/10; 450dpi; 1080x2038; samsung; SM-G975U1; beyond2q; qcom",
+  "29/10; 450dpi; 1080x2060; samsung; SM-N976V; d2xq; qcom",
+  "29/10; 450dpi; 1080x2172; samsung; SM-G770F; r5q; qcom",
+  "29/10; 450dpi; 1080x2180; samsung; SM-N770F; r7; exynos9810",
+  "29/10; 450dpi; 1080x2186; samsung; SM-A315F; a31; mt6768",
+  "29/10; 450dpi; 1080x2187; samsung; SM-G980F; x1s; exynos990",
+  "29/10; 450dpi; 1080x2192; samsung; SM-G985F; y2s; exynos990",
+  "29/10; 450dpi; 1080x2192; samsung; SM-G986U1; y2q; qcom",
+  "29/10; 450dpi; 1080x2192; samsung; SM-G986U; y2q; qcom",
+  "29/10; 450dpi; 1080x2270; samsung; SM-A515F; a51; exynos9611",
+  "29/10; 450dpi; 1080x2280; samsung; SM-G981V; x1q; qcom",
+  "29/10; 450dpi; 1080x2285; samsung; SM-G985F; y2s; exynos990",
+  "29/10; 450dpi; 1080x2285; samsung; SM-G986U; y2q; qcom",
+  "29/10; 454dpi; 1080x2028; samsung; SM-G970U; beyond0q; qcom",
+  "29/10; 460dpi; 1080x2021; asus; ZS620KL; ASUS_Z01R_1; qcom",
+  "29/10; 460dpi; 1080x2171; motorola; motorola one zoom; parker; qcom",
+  "29/10; 460dpi; 1080x2194; motorola; motorola one zoom; parker; qcom",
+  "29/10; 460dpi; 1080x2340; asus; ASUS_I01WD; ASUS_I01WD; qcom",
+  "29/10; 461dpi; 1080x2027; Xiaomi; POCOPHONE F1; beryllium; qcom",
+  "29/10; 461dpi; 1080x2068; Xiaomi; MI 8 Lite; platina; qcom",
+  "29/10; 461dpi; 1080x2134; Xiaomi/Redmi; Redmi Note 8 Pro; begonia; mt6785",
+  "29/10; 461dpi; 1080x2134; Xiaomi; Mi 9 Lite; pyxis; qcom",
+  "29/10; 461dpi; 1080x2135; Xiaomi; MI 9; cepheus; qcom",
+  "29/10; 461dpi; 1080x2135; Xiaomi; Mi 9 Lite; pyxis; qcom",
+  "29/10; 461dpi; 1080x2210; Xiaomi; Mi 9T Pro; raphael; qcom",
+  "29/10; 461dpi; 1080x2210; Xiaomi; Mi 9T; davinci; qcom",
+  "29/10; 476dpi; 1440x2646; Google/google; Pixel 3 XL; crosshatch; crosshatch",
+  "29/10; 476dpi; 1440x2737; Google/google; Pixel 2 XL; taimen; taimen",
+  "29/10; 480dpi; 1080x2015; motorola; moto g(7 plus; lake_n; qcom",
+  "29/10; 480dpi; 1080x2016; Sony; H8216; H8216; qcom",
+  "29/10; 480dpi; 1080x2016; Sony; H8266; H8266; qcom",
+  "29/10; 480dpi; 1080x2016; Sony; H8314; H8314; qcom",
+  "29/10; 480dpi; 1080x2016; Xiaomi/xiaomi; Mi A2; jasmine_sprout; qcom",
+  "29/10; 480dpi; 1080x2016; motorola; moto g(7; river; qcom",
+  "29/10; 480dpi; 1080x2020; samsung; SM-G970F; beyond0; exynos9820",
+  "29/10; 480dpi; 1080x2020; samsung; SM-G970U1; beyond0q; qcom",
+  "29/10; 480dpi; 1080x2020; samsung; SM-G970U; beyond0q; qcom",
+  "29/10; 480dpi; 1080x2024; samsung; SM-G9730; beyond1q; qcom",
+  "29/10; 480dpi; 1080x2024; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 480dpi; 1080x2024; samsung; SM-G973U1; beyond1q; qcom",
+  "29/10; 480dpi; 1080x2024; samsung; SM-G973U; beyond1q; qcom",
+  "29/10; 480dpi; 1080x2024; samsung; SM-G973W; beyond1q; qcom",
+  "29/10; 480dpi; 1080x2029; samsung; SM-G975F; beyond2; exynos9820",
+  "29/10; 480dpi; 1080x2029; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 480dpi; 1080x2031; HUAWEI; EML-L09; HWEML; kirin970",
+  "29/10; 480dpi; 1080x2031; HUAWEI; EML-L29; HWEML; kirin970",
+  "29/10; 480dpi; 1080x2032; samsung; SM-G977U; beyondxq; qcom",
+  "29/10; 480dpi; 1080x2037; HUAWEI; CLT-L04; HWCLT; kirin970",
+  "29/10; 480dpi; 1080x2037; HUAWEI; CLT-L09; HWCLT; kirin970",
+  "29/10; 480dpi; 1080x2037; HUAWEI; CLT-L29; HWCLT; kirin970",
+  "29/10; 480dpi; 1080x2040; HUAWEI/HONOR; BKL-L09; HWBKL; kirin970",
+  "29/10; 480dpi; 1080x2040; HUAWEI; BLA-L09; HWBLA; kirin970",
+  "29/10; 480dpi; 1080x2040; HUAWEI; BLA-L29; HWBLA; kirin970",
+  "29/10; 480dpi; 1080x2043; Xiaomi/xiaomi; Mi A2 Lite; daisy_sprout; qcom",
+  "29/10; 480dpi; 1080x2046; samsung; SM-N970U; d1q; qcom",
+  "29/10; 480dpi; 1080x2049; HUAWEI; HMA-L29; HWHMA; kirin980",
+  "29/10; 480dpi; 1080x2051; samsung; SM-N975F; d2s; exynos9825",
+  "29/10; 480dpi; 1080x2051; samsung; SM-N975U; d2q; qcom",
+  "29/10; 480dpi; 1080x2051; samsung; SM-N976V; d2xq; qcom",
+  "29/10; 480dpi; 1080x2060; HUAWEI/HONOR; COL-L29; HWCOL; kirin970",
+  "29/10; 480dpi; 1080x2063; motorola; moto g(7 plus; lake_n; qcom",
+  "29/10; 480dpi; 1080x2064; motorola; moto g(7; river; qcom",
+  "29/10; 480dpi; 1080x2076; samsung; SM-A605FN; a6plte; qcom",
+  "29/10; 480dpi; 1080x2076; samsung; SM-A750FN; a7y18lte; exynos7885",
+  "29/10; 480dpi; 1080x2076; samsung; SM-A750G; a7y18lte; exynos7885",
+  "29/10; 480dpi; 1080x2076; samsung; SM-G9600; starqltechn; qcom",
+  "29/10; 480dpi; 1080x2076; samsung; SM-G960F; starlte; samsungexynos9810",
+  "29/10; 480dpi; 1080x2076; samsung; SM-G960U1; starqlteue; qcom",
+  "29/10; 480dpi; 1080x2076; samsung; SM-G960U; starqltesq; qcom",
+  "29/10; 480dpi; 1080x2076; samsung; SM-G9650; star2qltechn; qcom",
+  "29/10; 480dpi; 1080x2076; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "29/10; 480dpi; 1080x2076; samsung; SM-G965U1; star2qlteue; qcom",
+  "29/10; 480dpi; 1080x2076; samsung; SM-G965U; star2qltesq; qcom",
+  "29/10; 480dpi; 1080x2076; samsung; SM-N9600; crownqltechn; qcom",
+  "29/10; 480dpi; 1080x2076; samsung; SM-N960F; crownlte; samsungexynos9810",
+  "29/10; 480dpi; 1080x2076; samsung; SM-N960U1; crownqlteue; qcom",
+  "29/10; 480dpi; 1080x2076; samsung; SM-N960U; crownqltesq; qcom",
+  "29/10; 480dpi; 1080x2088; motorola; moto g(7; river; qcom",
+  "29/10; 480dpi; 1080x2088; motorola; motorola one power; chef_sprout; qcom",
+  "29/10; 480dpi; 1080x2090; HUAWEI/HONOR; PCT-L29; HWPCT; kirin980",
+  "29/10; 480dpi; 1080x2090; HUAWEI; JNY-LX1; HWJNY; kirin810",
+  "29/10; 480dpi; 1080x2107; HUAWEI/HONOR; MAR-LX1H; HWMAR; kirin710",
+  "29/10; 480dpi; 1080x2107; HUAWEI; MAR-LX1A; HWMAR; kirin710",
+  "29/10; 480dpi; 1080x2107; HUAWEI; MAR-LX1B; HWMAR; kirin710",
+  "29/10; 480dpi; 1080x2107; HUAWEI; MAR-LX1M; HWMAR; kirin710",
+  "29/10; 480dpi; 1080x2107; HUAWEI; MAR-LX3A; HWMAR; kirin710",
+  "29/10; 480dpi; 1080x2107; HUAWEI; MAR-LX3A; HWMAR; kirin710;",
+  "29/10; 480dpi; 1080x2110; HUAWEI/HONOR; YAL-L21; HWYAL; kirin980",
+  "29/10; 480dpi; 1080x2110; HUAWEI/HONOR; YAL-L41; HWYAL; kirin980",
+  "29/10; 480dpi; 1080x2110; HUAWEI; YAL-L21; HWYAL; kirin980",
+  "29/10; 480dpi; 1080x2110; samsung; SM-A405FM; a40; exynos7904",
+  "29/10; 480dpi; 1080x2110; samsung; SM-A405FN; a40; exynos7904",
+  "29/10; 480dpi; 1080x2112; Xiaomi/xiaomi; Mi A2; jasmine_sprout; qcom",
+  "29/10; 480dpi; 1080x2113; samsung; SM-A305FN; a30; exynos7904",
+  "29/10; 480dpi; 1080x2113; samsung; SM-A305GT; a30; exynos7904",
+  "29/10; 480dpi; 1080x2113; samsung; SM-A505FM; a50; exynos9610",
+  "29/10; 480dpi; 1080x2113; samsung; SM-A505FN; a50; exynos9610",
+  "29/10; 480dpi; 1080x2113; samsung; SM-A505GT; a50; exynos9610",
+  "29/10; 480dpi; 1080x2113; samsung; SM-M305M; m30lte; exynos7904",
+  "29/10; 480dpi; 1080x2113; samsung; SM-M315F; m31; exynos9611",
+  "29/10; 480dpi; 1080x2116; OnePlus; ONEPLUS A6013; OnePlus6TSingle; qcom",
+  "29/10; 480dpi; 1080x2119; samsung; SM-G970F; beyond0; exynos9820",
+  "29/10; 480dpi; 1080x2119; samsung; SM-G970U; beyond0q; qcom",
+  "29/10; 480dpi; 1080x2120; motorola; moto z4; foles; qcom",
+  "29/10; 480dpi; 1080x2123; ZTE; ZTE 9000; P671F60; mt6771",
+  "29/10; 480dpi; 1080x2123; samsung; SM-G973U; beyond1q; qcom",
+  "29/10; 480dpi; 1080x2126; LGE/lge; LM-G820; alphalm; alphalm",
+  "29/10; 480dpi; 1080x2128; HUAWEI; SNE-LX1; HWSNE; kirin710",
+  "29/10; 480dpi; 1080x2128; samsung; SM-G975F; beyond2; exynos9820",
+  "29/10; 480dpi; 1080x2128; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 480dpi; 1080x2129; LGE/lge; LM-V405; judypn; judypn",
+  "29/10; 480dpi; 1080x2129; Xiaomi; MI 9 SE; grus; qcom",
+  "29/10; 480dpi; 1080x2129; Xiaomi; Mi 9 SE; grus; qcom",
+  "29/10; 480dpi; 1080x2132; Realme; RMX1851; RMX1851; qcom",
+  "29/10; 480dpi; 1080x2132; realme; RMX1921; RMX1921; qcom",
+  "29/10; 480dpi; 1080x2132; realme; RMX1971; RMX1971; qcom",
+  "29/10; 480dpi; 1080x2132; realme; RMX1971; RMX1971L1; qcom",
+  "29/10; 480dpi; 1080x2132; realme; RMX1992; RMX1992L1; qcom",
+  "29/10; 480dpi; 1080x2137; HUAWEI/HONOR; JSN-L21; HWJSN-H; kirin710",
+  "29/10; 480dpi; 1080x2137; HUAWEI/HONOR; JSN-L22; HWJSN-H; kirin710",
+  "29/10; 480dpi; 1080x2139; HUAWEI/HONOR; HRY-LX1; HWHRY-H; kirin710",
+  "29/10; 480dpi; 1080x2139; HUAWEI/HONOR; HRY-LX1MEB; HWHRY-H; kirin710",
+  "29/10; 480dpi; 1080x2139; HUAWEI/HONOR; HRY-LX1T; HWHRY-HF; kirin710",
+  "29/10; 480dpi; 1080x2139; HUAWEI; ELE-L09; HWELE; kirin980",
+  "29/10; 480dpi; 1080x2139; HUAWEI; ELE-L29; HWELE; kirin980",
+  "29/10; 480dpi; 1080x2139; HUAWEI; POT-LX1; HWPOT-H; kirin710",
+  "29/10; 480dpi; 1080x2139; Xiaomi/xiaomi; Mi A2 Lite; daisy_sprout; qcom",
+  "29/10; 480dpi; 1080x2141; vivo; vivo 1907; 1907N; mt6768",
+  "29/10; 480dpi; 1080x2141; vivo; vivo 1917; 1917; qcom",
+  "29/10; 480dpi; 1080x2141; vivo; vivo 1920; 1920; qcom",
+  "29/10; 480dpi; 1080x2145; HUAWEI; LYA-L29; HWLYA; kirin980",
+  "29/10; 480dpi; 1080x2147; HUAWEI; VOG-L09; HWVOG; kirin980",
+  "29/10; 480dpi; 1080x2147; HUAWEI; VOG-L29; HWVOG; kirin980",
+  "29/10; 480dpi; 1080x2150; samsung; SM-N975F; d2s; exynos9825",
+  "29/10; 480dpi; 1080x2150; samsung; SM-N975U; d2q; qcom",
+  "29/10; 480dpi; 1080x2150; samsung; SM-N976U; d2xq2; qcom",
+  "29/10; 480dpi; 1080x2153; realme; RMX2001; RMX2001L1; mt6785",
+  "29/10; 480dpi; 1080x2159; HUAWEI; CLT-L29; HWCLT; kirin970",
+  "29/10; 480dpi; 1080x2159; HUAWEI; EML-L29; HWEML; kirin970",
+  "29/10; 480dpi; 1080x2160; realme; RMX2063; RMX2063L1; qcom",
+  "29/10; 480dpi; 1080x2166; samsung; SM-A415F; a41; mt6768",
+  "29/10; 480dpi; 1080x2168; motorola; moto z4; foles; qcom",
+  "29/10; 480dpi; 1080x2168; samsung; SM-A515F; a51; exynos9611",
+  "29/10; 480dpi; 1080x2168; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 480dpi; 1080x2173; samsung; SM-G975F; beyond2; exynos9820",
+  "29/10; 480dpi; 1080x2175; samsung; SM-G9600; starqltechn; qcom",
+  "29/10; 480dpi; 1080x2175; samsung; SM-G960F; starlte; samsungexynos9810",
+  "29/10; 480dpi; 1080x2175; samsung; SM-G960U; starqltesq; qcom",
+  "29/10; 480dpi; 1080x2175; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "29/10; 480dpi; 1080x2175; samsung; SM-G965U; star2qltesq; qcom",
+  "29/10; 480dpi; 1080x2177; samsung; SM-A315G; a31; mt6768",
+  "29/10; 480dpi; 1080x2178; samsung; SM-G980F; x1s; exynos990",
+  "29/10; 480dpi; 1080x2178; samsung; SM-G981B; x1s; exynos990",
+  "29/10; 480dpi; 1080x2178; samsung; SM-G981U1; x1q; qcom",
+  "29/10; 480dpi; 1080x2178; samsung; SM-G981U; x1q; qcom",
+  "29/10; 480dpi; 1080x2178; samsung; SM-G981V; x1q; qcom",
+  "29/10; 480dpi; 1080x2180; samsung; SM-A705FN; a70q; qcom",
+  "29/10; 480dpi; 1080x2180; samsung; SM-A705MN; a70q; qcom",
+  "29/10; 480dpi; 1080x2182; samsung; SM-G988B; z3s; exynos990",
+  "29/10; 480dpi; 1080x2182; samsung; SM-G988U; z3q; qcom",
+  "29/10; 480dpi; 1080x2183; samsung; SM-G985F; y2s; exynos990",
+  "29/10; 480dpi; 1080x2183; samsung; SM-G986B; y2s; exynos990",
+  "29/10; 480dpi; 1080x2183; samsung; SM-G986U; y2q; qcom",
+  "29/10; 480dpi; 1080x2188; OnePlus; HD1905; OnePlus7T; qcom",
+  "29/10; 480dpi; 1080x2189; OnePlus; HD1907; OnePlus7TTMO; qcom",
+  "29/10; 480dpi; 1080x2190; HUAWEI/HONOR; COL-L29; HWCOL; kirin970",
+  "29/10; 480dpi; 1080x2192; motorola; moto z4; foles; qcom",
+  "29/10; 480dpi; 1080x2208; HUAWEI/HONOR; PCT-L29; HWPCT; kirin980",
+  "29/10; 480dpi; 1080x2208; HUAWEI; JNY-LX1; HWJNY; kirin810",
+  "29/10; 480dpi; 1080x2208; OPPO; CPH1919; OP4845; qcom",
+  "29/10; 480dpi; 1080x2209; samsung; SM-A405FM; a40; exynos7904",
+  "29/10; 480dpi; 1080x2209; samsung; SM-A405FN; a40; exynos7904",
+  "29/10; 480dpi; 1080x2210; Xiaomi; Mi 9 SE; grus; qcom",
+  "29/10; 480dpi; 1080x2211; Xiaomi; MI 9 SE; grus; qcom",
+  "29/10; 480dpi; 1080x2212; OnePlus; ONEPLUS A6010; OnePlus6T; qcom",
+  "29/10; 480dpi; 1080x2212; samsung; SM-A505FM; a50; exynos9610",
+  "29/10; 480dpi; 1080x2212; samsung; SM-A505FN; a50; exynos9610",
+  "29/10; 480dpi; 1080x2212; samsung; SM-A505GT; a50; exynos9610",
+  "29/10; 480dpi; 1080x2212; samsung; SM-M305M; m30lte; exynos7904",
+  "29/10; 480dpi; 1080x2214; LGE/lge; LM-Q730; mdh50lm; mdh50lm",
+  "29/10; 480dpi; 1080x2214; vivo; vivo 1819; 1819; mt6771",
+  "29/10; 480dpi; 1080x2218; motorola; motorola one action; troika; exynos9610",
+  "29/10; 480dpi; 1080x2218; motorola; motorola one action; troika_sprout; exynos9610",
+  "29/10; 480dpi; 1080x2218; motorola; motorola one vision; kane_sprout; exynos9610",
+  "29/10; 480dpi; 1080x2220; samsung; SM-G9600; starqltechn; qcom",
+  "29/10; 480dpi; 1080x2224; HUAWEI/HONOR; STK-LX1; HWSTK-HF; kirin710",
+  "29/10; 480dpi; 1080x2224; HUAWEI/HONOR; STK-LX3; HWSTK-HF; kirin710",
+  "29/10; 480dpi; 1080x2224; HUAWEI; STK-L21; HWSTK-HF; kirin710",
+  "29/10; 480dpi; 1080x2224; HUAWEI; STK-LX1; HWSTK-HF; kirin710",
+  "29/10; 480dpi; 1080x2224; HUAWEI; STK-LX3; HWSTK-HF; kirin710",
+  "29/10; 480dpi; 1080x2231; HUAWEI; MAR-LX1M; HWMAR; kirin710",
+  "29/10; 480dpi; 1080x2232; HUAWEI/HONOR; YAL-L21; HWYAL; kirin980",
+  "29/10; 480dpi; 1080x2232; HUAWEI/HONOR; YAL-L41; HWYAL; kirin980",
+  "29/10; 480dpi; 1080x2250; HUAWEI; SNE-LX1; HWSNE; kirin710",
+  "29/10; 480dpi; 1080x2255; HUAWEI/HONOR; JSN-L21; HWJSN-H; kirin710",
+  "29/10; 480dpi; 1080x2256; samsung; SM-A805F; r1q; qcom",
+  "29/10; 480dpi; 1080x2259; HUAWEI/HONOR; HRY-LX1; HWHRY-H; kirin710",
+  "29/10; 480dpi; 1080x2259; HUAWEI/HONOR; HRY-LX1T; HWHRY-HF; kirin710",
+  "29/10; 480dpi; 1080x2259; HUAWEI; POT-LX1; HWPOT-H; kirin710",
+  "29/10; 480dpi; 1080x2259; HUAWEI; POT-LX3; HWPOT-H; kirin710",
+  "29/10; 480dpi; 1080x2264; OPPO; RMX1801; RMX1801; qcom",
+  "29/10; 480dpi; 1080x2264; Realme; RMX1851; RMX1851; qcom",
+  "29/10; 480dpi; 1080x2264; realme; RMX1971; RMX1971; qcom",
+  "29/10; 480dpi; 1080x2265; HUAWEI; ELE-AL00; HWELE; kirin980",
+  "29/10; 480dpi; 1080x2265; HUAWEI; ELE-L09; HWELE; kirin980",
+  "29/10; 480dpi; 1080x2265; HUAWEI; ELE-L29; HWELE; kirin980",
+  "29/10; 480dpi; 1080x2265; HUAWEI; LYA-L09; HWLYA; kirin980",
+  "29/10; 480dpi; 1080x2265; HUAWEI; LYA-L29; HWLYA; kirin980",
+  "29/10; 480dpi; 1080x2265; HUAWEI; VOG-L04; HWVOG; kirin980",
+  "29/10; 480dpi; 1080x2265; HUAWEI; VOG-L09; HWVOG; kirin980",
+  "29/10; 480dpi; 1080x2265; HUAWEI; VOG-L29; HWVOG; kirin980",
+  "29/10; 480dpi; 1080x2266; motorola; motorola one actio",
+  "29/10; 480dpi; 1080x2266; motorola; motorola one action; troika_sprout; exynos9610",
+  "29/10; 480dpi; 1080x2266; motorola; motorola one vision; kane_sprout; exynos9610",
+  "29/10; 480dpi; 1080x2277; samsung; SM-G980F; x1s; exynos990",
+  "29/10; 480dpi; 1080x2277; samsung; SM-G981B; x1s; exynos990",
+  "29/10; 480dpi; 1080x2277; samsung; SM-G981U; x1q; qcom",
+  "29/10; 480dpi; 1080x2281; samsung; SM-G988U; z3q; qcom",
+  "29/10; 480dpi; 1080x2282; samsung; SM-G985F; y2s; exynos990",
+  "29/10; 480dpi; 1080x2282; samsung; SM-G986U; y2q; qcom",
+  "29/10; 480dpi; 1080x2285; realme; RMX2001; RMX2001L1; mt6785",
+  "29/10; 480dpi; 1080x2290; motorola; motorola one vision; kane_sprout; exynos9610",
+  "29/10; 480dpi; 1080x2292; realme; RMX2063; RMX2063L1; qcom",
+  "29/10; 480dpi; 1080x2322; samsung; SM-G980F; x1s; exynos990",
+  "29/10; 480dpi; 1080x2340; HUAWEI/HONOR; STK-LX1; HWSTK-HF; kirin710",
+  "29/10; 480dpi; 1080x2340; HUAWEI/HONOR; TNY-TL00; HWTNY; kirin980",
+  "29/10; 480dpi; 1080x2340; HUAWEI; STK-L22; HWSTK-HF; kirin710",
+  "29/10; 480dpi; 1080x2340; Realme; RMX1901; RMX1901; qcom",
+  "29/10; 480dpi; 1080x2376; Sony; I4113; I4113; qcom",
+  "29/10; 480dpi; 1080x2401; samsung; SM-F700F; bloomq; qcom",
+  "29/10; 480dpi; 1080x2401; samsung; SM-F700U; bloomq; qcom",
+  "29/10; 480dpi; 1176x2328; HUAWEI; LIO-AN00; HWLIO; kirin990",
+  "29/10; 480dpi; 2046x1080; samsung; SM-N970U; d1q; qcom",
+  "29/10; 480dpi; 2048x2048; HUAWEI; LYA-L29; HWLYA; kirin980",
+  "29/10; 480dpi; 2050x1080; HUAWEI; HMA-L29; HWHMA; kirin980",
+  "29/10; 480dpi; 2091x1080; HUAWEI/HONOR; PCT-L29; HWPCT; kirin980",
+  "29/10; 480dpi; 2108x1080; HUAWEI; MAR-LX1A; HWMAR; kirin710",
+  "29/10; 480dpi; 2110x1080; samsung; SM-A405FN; a40; exynos7904",
+  "29/10; 480dpi; 2140x1080; HUAWEI; ELE-L29; HWELE; kirin980",
+  "29/10; 480dpi; 2175x1080; samsung; SM-G960U; starqltesq; qcom",
+  "29/10; 480dpi; 2182x1080; samsung; SM-G988U; z3q; qcom",
+  "29/10; 480dpi; 2224x1080; HUAWEI; STK-LX1; HWSTK-HF; kirin710",
+  "29/10; 480dpi; 2265x1080; HUAWEI; VOG-L29; HWVOG; kirin980",
+  "29/10; 490dpi; 1080x2013; Google/google; Pixel 3; blueline; blueline",
+  "29/10; 490dpi; 1080x2073; Google/google; Pixel 3a; sargo; sargo",
+  "29/10; 490dpi; 1440x3120; OnePlus; GM1917; OnePlus7Pro; qcom",
+  "29/10; 492dpi; 1080x2012; Google/google; Pixel 3a XL; bonito; bonito",
+  "29/10; 500dpi; 1080x2183; OnePlus; HD1907; OnePlus7TTMO; qcom",
+  "29/10; 500dpi; 1080x2240; motorola; motorola one hyper; def; qcom",
+  "29/10; 510dpi; 1080x2011; samsung; SM-G970F; beyond0; exynos9820",
+  "29/10; 510dpi; 1080x2020; samsung; SM-G975F; beyond2; exynos9820",
+  "29/10; 510dpi; 1080x2154; samsung; SM-G770F; r5q; qcom",
+  "29/10; 510dpi; 1080x2168; samsung; SM-A315G; a31; mt6768",
+  "29/10; 510dpi; 1080x2169; samsung; SM-G981U; x1q; qcom",
+  "29/10; 510dpi; 1080x2172; samsung; SM-G9600; starqltechn; qcom",
+  "29/10; 510dpi; 1080x2173; samsung; SM-G988U; z3q; qcom",
+  "29/10; 530dpi; 1200x2499; HUAWEI; ELS-NX9; HWELS; kirin990",
+  "29/10; 540dpi; 1080x2002; samsung; SM-G970F; beyond0; exynos9820",
+  "29/10; 540dpi; 1080x2002; samsung; SM-G970U; beyond0q; qcom",
+  "29/10; 540dpi; 1080x2006; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 540dpi; 1080x2006; samsung; SM-G973U; beyond1q; qcom",
+  "29/10; 540dpi; 1080x2011; samsung; SM-G975F; beyond2; exynos9820",
+  "29/10; 540dpi; 1080x2011; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 540dpi; 1080x2028; samsung; SM-N970F; d1; exynos9825",
+  "29/10; 540dpi; 1080x2028; samsung; SM-N970U; d1q; qcom",
+  "29/10; 540dpi; 1080x2031; HUAWEI; EML-L29; HWEML; kirin970",
+  "29/10; 540dpi; 1080x2033; samsung; SM-N975U; d2q; qcom",
+  "29/10; 540dpi; 1080x2033; samsung; SM-N976U; d2xq2; qcom",
+  "29/10; 540dpi; 1080x2058; samsung; SM-A750FN; a7y18lte; exynos7885",
+  "29/10; 540dpi; 1080x2058; samsung; SM-A750G; a7y18lte; exynos7885",
+  "29/10; 540dpi; 1080x2058; samsung; SM-A920F; a9y18qlte; qcom",
+  "29/10; 540dpi; 1080x2058; samsung; SM-G9600; starqltechn; qcom",
+  "29/10; 540dpi; 1080x2058; samsung; SM-G960F; starlte; samsungexynos9810",
+  "29/10; 540dpi; 1080x2058; samsung; SM-G960U; starqltesq; qcom",
+  "29/10; 540dpi; 1080x2058; samsung; SM-G9650; star2qltechn; qcom",
+  "29/10; 540dpi; 1080x2058; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "29/10; 540dpi; 1080x2058; samsung; SM-G965U1; star2qlteue; qcom",
+  "29/10; 540dpi; 1080x2058; samsung; SM-G965U; star2qltesq; qcom",
+  "29/10; 540dpi; 1080x2058; samsung; SM-N960N; crownlteks; samsungexynos9810",
+  "29/10; 540dpi; 1080x2058; samsung; SM-N960U; crownqltesq; qcom",
+  "29/10; 540dpi; 1080x2060; HUAWEI/HONOR; COL-L29; HWCOL; kirin970",
+  "29/10; 540dpi; 1080x2079; motorola; moto g(7; river; qcom",
+  "29/10; 540dpi; 1080x2090; HUAWEI/HONOR; PCT-L29; HWPCT; kirin980",
+  "29/10; 540dpi; 1080x2092; samsung; SM-A405FN; a40; exynos7904",
+  "29/10; 540dpi; 1080x2093; samsung; SM-M205M; m20lte; exynos7904",
+  "29/10; 540dpi; 1080x2095; samsung; SM-A305GT; a30; exynos7904",
+  "29/10; 540dpi; 1080x2095; samsung; SM-A505FM; a50; exynos9610",
+  "29/10; 540dpi; 1080x2095; samsung; SM-A505GT; a50; exynos9610",
+  "29/10; 540dpi; 1080x2098; OnePlus; ONEPLUS A6013; OnePlus6TSingle; qcom",
+  "29/10; 540dpi; 1080x2102; motorola; moto z4; foles; qcom",
+  "29/10; 540dpi; 1080x2103; motorola; moto g power; sofia; qcom",
+  "29/10; 540dpi; 1080x2107; HUAWEI; MAR-LX1A; HWMAR; kirin710",
+  "29/10; 540dpi; 1080x2110; HUAWEI/HONOR; YAL-L41; HWYAL; kirin980",
+  "29/10; 540dpi; 1080x2118; Google/google; Pixel 4; flame; flame",
+  "29/10; 540dpi; 1080x2128; HUAWEI; SNE-LX1; HWSNE; kirin710",
+  "29/10; 540dpi; 1080x2137; HUAWEI/HONOR; JSN-L21; HWJSN-H; kirin710",
+  "29/10; 540dpi; 1080x2139; HUAWEI/HONOR; HRY-LX1; HWHRY-H; kirin710",
+  "29/10; 540dpi; 1080x2139; HUAWEI/HONOR; HRY-LX1T; HWHRY-HF; kirin710",
+  "29/10; 540dpi; 1080x2139; HUAWEI; ELE-L29; HWELE; kirin980",
+  "29/10; 540dpi; 1080x2139; HUAWEI; POT-LX1; HWPOT-H; kirin710",
+  "29/10; 540dpi; 1080x2139; samsung; SM-N970F; d1; exynos9825",
+  "29/10; 540dpi; 1080x2150; samsung; SM-A515F; a51; exynos9611",
+  "29/10; 540dpi; 1080x2159; samsung; SM-A315G; a31; mt6768",
+  "29/10; 540dpi; 1080x2160; samsung; SM-G981V; x1q; qcom",
+  "29/10; 540dpi; 1080x2162; samsung; SM-A705FN; a70q; qcom",
+  "29/10; 540dpi; 1080x2164; samsung; SM-G988U; z3q; qcom",
+  "29/10; 540dpi; 1080x2165; samsung; SM-G986U; y2q; qcom",
+  "29/10; 540dpi; 1080x2169; samsung; SM-G9600; starqltechn; qcom",
+  "29/10; 540dpi; 1080x2169; samsung; SM-G965U1; star2qlteue; qcom",
+  "29/10; 540dpi; 1080x2169; samsung; SM-G965U; star2qltesq; qcom",
+  "29/10; 540dpi; 1080x2200; motorola; motorola one action; troika_sprout; exynos9610",
+  "29/10; 540dpi; 1080x2200; motorola; motorola one vision; kane_sprout; exynos9610",
+  "29/10; 540dpi; 1080x2224; HUAWEI/HONOR; STK-LX1; HWSTK-HF; kirin710",
+  "29/10; 540dpi; 1080x2226; Google/google; Pixel 4; flame; flame",
+  "29/10; 540dpi; 1080x2254; motorola; motorola one vision; kane_sprout; exynos9610",
+  "29/10; 540dpi; 1080x2304; LGE/lge; LM-Q730; mdh50lm; mdh50lm",
+  "29/10; 544dpi; 1440x2860; HUAWEI; LYA-L29; HWLYA; kirin980",
+  "29/10; 560dpi; 1440x2392; Google/google; Pixel XL; marlin; marlin",
+  "29/10; 560dpi; 1440x2621; Google/google; Pixel 3 XL; crosshatch; crosshatch",
+  "29/10; 560dpi; 1440x2712; Google/google; Pixel 2 XL; taimen; taimen",
+  "29/10; 560dpi; 1440x2712; LGE/lge; LM-V350; judyp; judyp",
+  "29/10; 560dpi; 1440x2712; Sony; H8416; H8416; qcom",
+  "29/10; 560dpi; 1440x2723; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 560dpi; 1440x2723; samsung; SM-G973U; beyond1q; qcom",
+  "29/10; 560dpi; 1440x2730; samsung; SM-G975F; beyond2; exynos9820",
+  "29/10; 560dpi; 1440x2730; samsung; SM-G975U1; beyond2q; qcom",
+  "29/10; 560dpi; 1440x2730; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 560dpi; 1440x2733; Google/google; Pixel 3 XL; crosshatch; crosshatch",
+  "29/10; 560dpi; 1440x2759; samsung; SM-N975F; d2s; exynos9825",
+  "29/10; 560dpi; 1440x2759; samsung; SM-N975U1; d2q; qcom",
+  "29/10; 560dpi; 1440x2759; samsung; SM-N975U; d2q; qcom",
+  "29/10; 560dpi; 1440x2759; samsung; SM-N976U; d2xq2; qcom",
+  "29/10; 560dpi; 1440x2759; samsung; SM-N976V; d2xq; qcom",
+  "29/10; 560dpi; 1440x2792; samsung; SM-G9600; starqltechn; qcom",
+  "29/10; 560dpi; 1440x2792; samsung; SM-G960U; starqltesq; qcom",
+  "29/10; 560dpi; 1440x2792; samsung; SM-G9650; star2qltechn; qcom",
+  "29/10; 560dpi; 1440x2792; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "29/10; 560dpi; 1440x2792; samsung; SM-G965U1; star2qlteue; qcom",
+  "29/10; 560dpi; 1440x2792; samsung; SM-G965U; star2qltesq; qcom",
+  "29/10; 560dpi; 1440x2792; samsung; SM-N9600; crownqltechn; qcom",
+  "29/10; 560dpi; 1440x2792; samsung; SM-N960F; crownlte; samsungexynos9810",
+  "29/10; 560dpi; 1440x2792; samsung; SM-N960U1; crownqlteue; qcom",
+  "29/10; 560dpi; 1440x2792; samsung; SM-N960U; crownqltesq; qcom",
+  "29/10; 560dpi; 1440x2824; Google/google; Pixel 2 XL; taimen; taimen",
+  "29/10; 560dpi; 1440x2838; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 560dpi; 1440x2838; samsung; SM-G973U; beyond1q; qcom",
+  "29/10; 560dpi; 1440x2845; samsung; SM-G975F; beyond2; exynos9820",
+  "29/10; 560dpi; 1440x2845; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 560dpi; 1440x2858; LGE/lge; LM-G710; judyln; judyln",
+  "29/10; 560dpi; 1440x2862; LGE/lge; LM-V405; judypn; judypn",
+  "29/10; 560dpi; 1440x2872; Google/google; Pixel 4 XL; coral; coral",
+  "29/10; 560dpi; 1440x2874; samsung; SM-N975U; d2q; qcom",
+  "29/10; 560dpi; 1440x2874; samsung; SM-N976U; d2xq2; qcom",
+  "29/10; 560dpi; 1440x2891; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 560dpi; 1440x2898; samsung; SM-G975F; beyond2; exynos9820",
+  "29/10; 560dpi; 1440x2898; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 560dpi; 1440x2907; samsung; SM-G9600; starqltechn; qcom",
+  "29/10; 560dpi; 1440x2907; samsung; SM-N960U1; crownqlteue; qcom",
+  "29/10; 560dpi; 1440x2907; samsung; SM-N960U; crownqltesq; qcom",
+  "29/10; 560dpi; 1440x2924; OnePlus; GM1910; OnePlus7Pro; qcom",
+  "29/10; 560dpi; 1440x2924; OnePlus; GM1915; OnePlus7ProTMO; qcom",
+  "29/10; 560dpi; 1440x2924; OnePlus; GM1917; OnePlus7Pro; qcom",
+  "29/10; 560dpi; 1440x2924; OnePlus; HD1925; OnePlus7TProNR; qcom",
+  "29/10; 560dpi; 1440x2927; samsung; SM-N975F; d2s; exynos9825",
+  "29/10; 560dpi; 1440x2927; samsung; SM-N976V; d2xq; qcom",
+  "29/10; 560dpi; 1440x2933; samsung; SM-G988U; z3q; qcom",
+  "29/10; 560dpi; 1440x2935; samsung; SM-G985F; y2s; exynos990",
+  "29/10; 560dpi; 1440x2960; samsung; SM-G9650; star2qltechn; qcom",
+  "29/10; 560dpi; 1440x2960; samsung; SM-G965F; star2lte; samsungexynos9810",
+  "29/10; 560dpi; 1440x2960; samsung; SM-N960U1; crownqlteue; qcom",
+  "29/10; 560dpi; 1440x2984; Google/google; Pixel 4 XL; coral; coral",
+  "29/10; 560dpi; 1440x3048; samsung; SM-G988B; z3s; exynos990",
+  "29/10; 560dpi; 1440x3064; OnePlus; GM1911; OnePlus7Pro; qcom",
+  "29/10; 560dpi; 1440x3120; OnePlus; GM1910; OnePlus7Pro; qcom",
+  "29/10; 560dpi; 1440x3120; OnePlus; HD1910; OnePlus7TPro; qcom",
+  "29/10; 560dpi; 2621x1440; Google/google; Pixel 3 XL; crosshatch; crosshatch",
+  "29/10; 560dpi; 2874x1440; samsung; SM-N975U; d2q; qcom",
+  "29/10; 600dpi; 1440x2718; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 600dpi; 1440x2721; samsung; SM-G977U; beyondxq; qcom",
+  "29/10; 600dpi; 1440x2910; OnePlus; GM1913; OnePlus7Pro; qcom",
+  "29/10; 600dpi; 1440x2921; samsung; SM-G988B; z3s; exynos990",
+  "29/10; 600dpi; 1440x2921; samsung; SM-G988U; z3q; qcom",
+  "29/10; 600dpi; 1440x2923; samsung; SM-G986U; y2q; qcom",
+  "29/10; 600dpi; 1440x3047; samsung; SM-G985F; y2s; exynos990",
+  "29/10; 612dpi; 1440x2605; Google/google; Pixel 3 XL; crosshatch; crosshatch",
+  "29/10; 612dpi; 1440x2696; Google/google; Pixel 2 XL; taimen; taimen",
+  "29/10; 612dpi; 1440x2979; Google/google; Pixel 4 XL; coral; coral",
+  "29/10; 640dpi; 1440x2699; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 640dpi; 1440x2699; samsung; SM-G973U; beyond1q; qcom",
+  "29/10; 640dpi; 1440x2706; samsung; SM-G975F; beyond2; exynos9820",
+  "29/10; 640dpi; 1440x2706; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 640dpi; 1440x2735; samsung; SM-N975F; d2s; exynos9825",
+  "29/10; 640dpi; 1440x2735; samsung; SM-N975U1; d2q; qcom",
+  "29/10; 640dpi; 1440x2735; samsung; SM-N975U; d2q; qcom",
+  "29/10; 640dpi; 1440x2768; samsung; SM-G9600; starqltechn; qcom",
+  "29/10; 640dpi; 1440x2768; samsung; SM-G960F; starlte; samsungexynos9810",
+  "29/10; 640dpi; 1440x2768; samsung; SM-G960U1; starqlteue; qcom",
+  "29/10; 640dpi; 1440x2768; samsung; SM-G960U; starqltesq; qcom",
+  "29/10; 640dpi; 1440x2768; samsung; SM-G9650; star2qltechn; qcom",
+  "29/10; 640dpi; 1440x2768; samsung; SM-G965U; star2qltesq; qcom",
+  "29/10; 640dpi; 1440x2768; samsung; SM-N960U1; crownqlteue; qcom",
+  "29/10; 640dpi; 1440x2768; samsung; SM-N960U; crownqltesq; qcom",
+  "29/10; 640dpi; 1440x2831; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 640dpi; 1440x2834; LGE/lge; LM-G710; judyln; judyln",
+  "29/10; 640dpi; 1440x2834; LGE/lge; LM-G820; alphalm; alphalm",
+  "29/10; 640dpi; 1440x2867; samsung; SM-N975U; d2q; qcom",
+  "29/10; 640dpi; 1440x2898; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 640dpi; 1440x2900; samsung; SM-G960F; starlte; samsungexynos9810",
+  "29/10; 640dpi; 1440x2900; samsung; SM-G960U; starqltesq; qcom",
+  "29/10; 640dpi; 1440x2900; samsung; SM-G9650; star2qltechn; qcom",
+  "29/10; 640dpi; 1440x2900; samsung; SM-G965U; star2qltesq; qcom",
+  "29/10; 640dpi; 1440x2960; samsung; SM-G9600; starqltechn; qcom",
+  "29/10; 640dpi; 2699x1440; samsung; SM-G973F; beyond1; exynos9820",
+  "29/10; 640dpi; 2699x1440; samsung; SM-G973U; beyond1q; qcom",
+  "29/10; 666dpi; 1440x2589; Google/google; Pixel 3 XL; crosshatch; crosshatch",
+  "29/10; 720dpi; 1440x2664; Google/google; Pixel 2 XL; taimen; taimen",
+  "29/10; 720dpi; 1440x2682; samsung; SM-G975U; beyond2q; qcom",
+  "29/10; 720dpi; 1440x2711; samsung; SM-N976U; d2xq2; qcom",
+  "29/10; 720dpi; 1440x2744; samsung; SM-G965U; star2qltesq; qcom",
+  "29/10; 720dpi; 1440x2744; samsung; SM-N960U; crownqltesq; qcom",
+  "29/10; 720dpi; 1440x2810; LGE/lge; LM-G710; judyln; judyln",
+  "29/10; 720dpi; 1440x2880; samsung; SM-G981U; x1q; qcom",
+  "29/10; 720dpi; 1440x2892; samsung; SM-G960U; starqltesq; qcom",
+  "29/10; 720dpi; 1440x3028; samsung; SM-G981U; x1q; qcom",
+  "29/10; 720dpi; 2711x1440; samsung; SM-N975U; d2q; qcom",
+  "30/11; 476dpi; 1440x2646; Google/google; Pixel 3 XL; crosshatch; crosshatch",
+  "30/11; 560dpi; 1440x2733; Google/google; Pixel 3 XL; crosshatch; crosshatch"
+]

+ 26 - 0
mauigpapi/state/samples/supported-capabilities.json

@@ -0,0 +1,26 @@
+[
+  {
+    "name": "SUPPORTED_SDK_VERSIONS",
+    "value": "13.0,14.0,15.0,16.0,17.0,18.0,19.0,20.0,21.0,22.0,23.0,24.0,25.0,26.0,27.0,28.0,29.0,30.0,31.0,32.0,33.0,34.0,35.0,36.0,37.0,38.0,39.0,40.0,41.0,42.0,43.0,44.0,45.0,46.0,47.0,48.0,49.0,50.0,51.0,52.0,53.0,54.0,55.0,56.0,57.0,58.0,59.0,60.0,61.0,62.0,63.0,64.0,65.0,66.0"
+  },
+  {
+    "name": "FACE_TRACKER_VERSION",
+    "value": 12
+  },
+  {
+    "name": "segmentation",
+    "value": "segmentation_enabled"
+  },
+  {
+    "name": "COMPRESSION",
+    "value": "ETC2_COMPRESSION"
+  },
+  {
+    "name": "world_tracker",
+    "value": "world_tracker_enabled"
+  },
+  {
+    "name": "gyroscope",
+    "value": "gyroscope_enabled"
+  }
+]

+ 31 - 0
mauigpapi/state/session.py

@@ -0,0 +1,31 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import Optional, Union
+
+from mautrix.types import SerializableAttrs, dataclass, field
+
+
+@dataclass
+class AndroidSession(SerializableAttrs['AndroidSession']):
+    eu_dc_enabled: Optional[bool] = field(default=None, json="euDCEnabled")
+    thumbnail_cache_busting_value: int = field(default=1000, json="thumbnailCacheBustingValue")
+    ads_opt_out: bool = field(default=None, json="adsOptOut")
+
+    ig_www_claim: Optional[str] = field(default=None, json="igWWWClaim")
+    authorization: Optional[str] = None
+    password_encryption_pubkey: Optional[str] = field(default=None, json="passwordEncryptionPubKey")
+    password_encryption_key_id: Union[None, str, int] = field(default=None, json="passwordEncryptionKeyId")
+    region_hint: Optional[str] = field(default=None, json="regionHint")

+ 72 - 0
mauigpapi/state/state.py

@@ -0,0 +1,72 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2020 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+from typing import Optional
+from uuid import UUID
+import random
+import time
+
+from mautrix.types import SerializableAttrs, field, dataclass
+
+from ..errors import IGNoCheckpointError, IGCookieNotFoundError
+from .device import AndroidDevice
+from .session import AndroidSession
+from .application import AndroidApplication
+from .cookies import Cookies
+
+
+@dataclass
+class AndroidState(SerializableAttrs['AndroidState']):
+    device: AndroidDevice = field(factory=lambda: AndroidDevice())
+    session: AndroidSession = field(factory=lambda: AndroidSession())
+    application: AndroidApplication = field(factory=lambda: AndroidApplication())
+    # experiments: AndroidExperiments
+    client_session_id_lifetime: int = 1_200_000
+    pigeon_session_id_lifetime: int = 1_200_000
+    challenge: 'Optional[ChallengeStateResponse]' = None
+    _challenge_path: Optional[str] = field(default=None, json="challenge_path")
+    cookies: Cookies = field(factory=lambda: Cookies())
+
+    @property
+    def client_session_id(self) -> str:
+        return str(self._gen_temp_uuid("clientSessionId", self.client_session_id_lifetime))
+
+    @property
+    def pigeon_session_id(self) -> str:
+        return str(self._gen_temp_uuid("pigeonSessionId", self.pigeon_session_id_lifetime))
+
+    @property
+    def user_agent(self) -> str:
+        return (f"Instagram {self.application.APP_VERSION} Android ({self.device.descriptor}; "
+                f"{self.device.language}; {self.application.APP_VERSION_CODE})")
+
+    @property
+    def user_id(self) -> str:
+        try:
+            return self.cookies.user_id
+        except IGCookieNotFoundError:
+            if not self.challenge or not self.challenge.user_id:
+                raise IGUserIDNotFoundError()
+            return str(self.challenge.user_id)
+
+    @property
+    def challenge_path(self) -> str:
+        if not self._challenge_path:
+            raise IGNoCheckpointError()
+        return self._challenge_path
+
+    def _gen_temp_uuid(self, seed: str, lifetime: int) -> UUID:
+        rand = random.Random(f"{seed}{self.device.id}{round(time.time() * 1000 / lifetime)}")
+        return UUID(int=rand.getrandbits(128), version=4)

+ 2 - 0
mautrix_instagram/__init__.py

@@ -0,0 +1,2 @@
+__version__ = "0.1.0+dev"
+__author__ = "Tulir Asokan <tulir@maunium.net>"

+ 50 - 0
mautrix_instagram/get_version.py

@@ -0,0 +1,50 @@
+import subprocess
+import shutil
+import os
+
+from . import __version__
+
+cmd_env = {
+    "PATH": os.environ["PATH"],
+    "HOME": os.environ["HOME"],
+    "LANG": "C",
+    "LC_ALL": "C",
+}
+
+
+def run(cmd):
+    return subprocess.check_output(cmd, stderr=subprocess.DEVNULL, env=cmd_env)
+
+
+if os.path.exists(".git") and shutil.which("git"):
+    try:
+        git_revision = run(["git", "rev-parse", "HEAD"]).strip().decode("ascii")
+        git_revision_url = f"https://github.com/tulir/mautrix-instagram/commit/{git_revision}"
+        git_revision = git_revision[:8]
+    except (subprocess.SubprocessError, OSError):
+        git_revision = "unknown"
+        git_revision_url = None
+
+    try:
+        git_tag = run(["git", "describe", "--exact-match", "--tags"]).strip().decode("ascii")
+    except (subprocess.SubprocessError, OSError):
+        git_tag = None
+else:
+    git_revision = "unknown"
+    git_revision_url = None
+    git_tag = None
+
+git_tag_url = (f"https://github.com/tulir/mautrix-instagram/releases/tag/{git_tag}"
+               if git_tag else None)
+
+if git_tag and __version__ == git_tag[1:].replace("-", ""):
+    version = __version__
+    linkified_version = f"[{version}]({git_tag_url})"
+else:
+    if not __version__.endswith("+dev"):
+        __version__ += "+dev"
+    version = f"{__version__}.{git_revision}"
+    if git_revision_url:
+        linkified_version = f"{__version__}.[{git_revision}]({git_revision_url})"
+    else:
+        linkified_version = version

+ 1 - 0
mautrix_instagram/version.py

@@ -0,0 +1 @@
+from .get_version import git_tag, git_revision, version, linkified_version

+ 10 - 0
optional-requirements.txt

@@ -0,0 +1,10 @@
+# Format: #/name defines a new extras_require group called name
+# Uncommented lines after the group definition insert things into that group.
+
+#/e2be
+python-olm>=3,<4
+pycryptodome>=3,<4
+unpaddedbase64>=1,<2
+
+#/metrics
+prometheus_client>=0.6,<0.10

+ 8 - 0
requirements.txt

@@ -0,0 +1,8 @@
+ruamel.yaml>=0.15.35,<0.17
+python-magic>=0.4,<0.5
+commonmark>=0.8,<0.10
+aiohttp>=3,<4
+yarl>=1,<2
+attrs>=19.1
+mautrix>=0.8,<0.9
+asyncpg>=0.20,<0.22

+ 70 - 0
setup.py

@@ -0,0 +1,70 @@
+import setuptools
+
+from mautrix_instagram.get_version import git_tag, git_revision, version, linkified_version
+
+with open("requirements.txt") as reqs:
+    install_requires = reqs.read().splitlines()
+
+with open("optional-requirements.txt") as reqs:
+    extras_require = {}
+    current = []
+    for line in reqs.read().splitlines():
+        if line.startswith("#/"):
+            extras_require[line[2:]] = current = []
+        elif not line or line.startswith("#"):
+            continue
+        else:
+            current.append(line)
+
+extras_require["all"] = list({dep for deps in extras_require.values() for dep in deps})
+
+try:
+    long_desc = open("README.md").read()
+except IOError:
+    long_desc = "Failed to read README.md"
+
+with open("mautrix_instagram/version.py", "w") as version_file:
+    version_file.write(f"""# Generated in setup.py
+
+git_tag = {git_tag!r}
+git_revision = {git_revision!r}
+version = {version!r}
+linkified_version = {linkified_version!r}
+""")
+
+setuptools.setup(
+    name="mautrix-instagram",
+    version=version,
+    url="https://github.com/tulir/mautrix-instagram",
+
+    author="Tulir Asokan",
+    author_email="tulir@maunium.net",
+
+    description="A Matrix-Instagram puppeting bridge.",
+    long_description=long_desc,
+    long_description_content_type="text/markdown",
+
+    packages=setuptools.find_packages(),
+
+    install_requires=install_requires,
+    extras_require=extras_require,
+    python_requires="~=3.7",
+
+    classifiers=[
+        "Development Status :: 3 - Alpha",
+        "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
+        "Topic :: Communications :: Chat",
+        "Framework :: AsyncIO",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+    ],
+    package_data={"mautrix_instagram": [
+        "example-config.yaml",
+    ]},
+    data_files=[
+        (".", ["mautrix_instagram/example-config.yaml"]),
+    ],
+)