[
  {
    "path": ".github/workflows/build_binaries.yml",
    "content": "name: Build binaries for moe-sticker-bot and msbimport\n\non: push\n\njobs:\n  build_msb:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-go@v3\n        with:\n          go-version: '>=1.20'\n      - name: Build MSB CLI\n        run: |\n          go version\n          GOOS=linux GOARCH=amd64 go build -o moe-sticker-bot_linux_amd64 cmd/moe-sticker-bot/main.go\n          GOOS=linux GOARCH=arm64 go build -o moe-sticker-bot_linux_aarch64 cmd/moe-sticker-bot/main.go\n          GOOS=windows GOARCH=amd64 go build -o moe-sticker-bot_windows_amd64.exe cmd/moe-sticker-bot/main.go\n          GOOS=windows GOARCH=arm64 go build -o moe-sticker-bot_windows_aarch64.exe cmd/moe-sticker-bot/main.go\n          GOOS=darwin GOARCH=amd64 go build -o moe-sticker-bot_macos_amd64 cmd/moe-sticker-bot/main.go\n          GOOS=darwin GOARCH=arm64 go build -o moe-sticker-bot_macos_aarch64 cmd/moe-sticker-bot/main.go\n\n      - name: Upload Artifacts\n        uses: actions/upload-artifact@v3.1.2\n        with:\n          name: msb_bins\n          path: moe-sticker-bot*\n\n  build_msbimport:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-go@v3\n        with:\n          go-version: '>=1.20'\n      - name: Build Msbimport CLI\n        run: |\n          go version\n          GOOS=linux GOARCH=amd64 go build -o msbimport_linux_amd64 cmd/msbimport/main.go\n          GOOS=linux GOARCH=arm64 go build -o msbimport_linux_aarch64 cmd/msbimport/main.go\n          GOOS=windows GOARCH=amd64 go build -o msbimport_windows_amd64.exe cmd/msbimport/main.go\n          GOOS=windows GOARCH=arm64 go build -o msbimport_windows_aarch64.exe cmd/msbimport/main.go\n          GOOS=darwin GOARCH=amd64 go build -o msbimport_macos_amd64 cmd/msbimport/main.go\n          GOOS=darwin GOARCH=arm64 go build -o msbimport_macos_aarch64 cmd/msbimport/main.go\n\n      - name: Upload Artifacts\n        uses: actions/upload-artifact@v3.1.2\n        with:\n          name: msbimport_bins\n          path: msbimport*\n\n  release:\n    name: Release\n    if: startsWith(github.ref, 'refs/tags/')\n    needs: [ build_msb, build_msbimport ]\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Download artifacts\n        uses: actions/download-artifact@v3\n        with:\n          path: releases/\n\n      - name: Publish Release\n        uses: softprops/action-gh-release@v1\n        with:\n          generate_release_notes: true\n          # append_body: true\n          body_path: \".github/workflows/release_note.md\"\n          files: |\n            releases/*/*\n"
  },
  {
    "path": ".github/workflows/msb_nginx.yml",
    "content": "name: Build nginx for @moe-sticker-bot\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\n  workflow_dispatch:\n\njobs:\n  build_amd64:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: '18.x'\n      - name: Build nginx container amd64\n        run: |\n          bash .github/workflows/msb_nginx_amd64.sh ${{ secrets.GITHUB_TOKEN }}\n\n  build_aarch64:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: '18.x'\n      - name: Build nginx container aarch64\n        run: |\n          bash .github/workflows/msb_nginx_aarch64.sh ${{ secrets.GITHUB_TOKEN }}\n\n"
  },
  {
    "path": ".github/workflows/msb_nginx_aarch64.sh",
    "content": "#!/usr/bin/bash\nGITHUB_TOKEN=$1\n\nbuildah login -u star-39 -p $GITHUB_TOKEN ghcr.io\n\n#AArch64\nc1=$(buildah from --arch=arm64 docker://arm64v8/nginx:latest)\n\n# Copy nginx template and app link validation json.\nbuildah copy $c1 web/nginx/default.conf.template /etc/nginx/templates/\nbuildah copy $c1 web/nginx/assetlinks.json /www/.well-known/assetlinks.json\n\n# Build react app\ncd web/webapp3/\nnpm install\nPUBLIC_URL=/webapp REACT_APP_HOST=msb39.eu.org npm run build\nbuildah copy $c1 build/ /webapp\ncd ../..\n\nbuildah commit $c1 moe-sticker-bot:msb_nginx_aarch64\n\nbuildah push moe-sticker-bot:msb_nginx_aarch64 ghcr.io/star-39/moe-sticker-bot:msb_nginx_aarch64\n"
  },
  {
    "path": ".github/workflows/msb_nginx_amd64.sh",
    "content": "#!/usr/bin/bash\nGITHUB_TOKEN=$1\n\nbuildah login -u star-39 -p $GITHUB_TOKEN ghcr.io\n\n#AMD64\nc1=$(buildah from docker://nginx:latest)\n\n# Copy nginx template and app link validation json.\nbuildah run  $c1 -- mkdir -p /etc/nginx/templates\nbuildah run  $c1 -- mkdir -p /www/.well-known\nbuildah copy $c1 web/nginx/default.conf.template /etc/nginx/templates/\nbuildah copy $c1 web/nginx/assetlinks.json /www/.well-known/assetlinks.json\n\n# Build react app\ncd web/webapp3/\nnpm install\nPUBLIC_URL=/webapp REACT_APP_HOST=msb39.eu.org npm run build\nbuildah copy $c1 build/ /webapp\ncd ../..\n\nbuildah commit $c1 moe-sticker-bot:msb_nginx\n\nbuildah push moe-sticker-bot:msb_nginx ghcr.io/star-39/moe-sticker-bot:msb_nginx\n"
  },
  {
    "path": ".github/workflows/msb_oci.yml",
    "content": "name: Build OCI container for moe-sticker-bot\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\njobs:\n  build_amd64:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-go@v3\n        with:\n          go-version: '>=1.20'\n      - name: Build MSB OCI container amd64\n        run: |\n          go version\n          bash .github/workflows/msb_oci_amd64.sh ${{ secrets.GITHUB_TOKEN }}\n\n\n  build_aarch64:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-go@v3\n        with:\n          go-version: '>=1.20'\n      - name: Build MSB OCI container aarch64\n        run: |\n          go version\n          bash .github/workflows/msb_oci_aarch64.sh ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/msb_oci_aarch64.sh",
    "content": "#!/bin/sh\n\nGITHUB_TOKEN=$1\n\nbuildah login -u star-39 -p $GITHUB_TOKEN ghcr.io\n\n# AArch64\n#################################\nif false ; then\n\nc1=$(buildah from --arch=arm64 docker://lopsided/archlinux-arm64v8:latest)\n\nbuildah run $c1 -- pacman -Sy\nbuildah run $c1 -- pacman --noconfirm -S libwebp libheif imagemagick curl gifsicle libarchive python python-pip make gcc\n\nbuildah run $c1 -- pip3 install emoji rlottie-python Pillow --break-system-packages\n\nbuildah run $c1 -- pacman --noconfirm -Rsc make gcc python-pip\nbuildah run $c1 -- sh -c 'yes | pacman -Scc'\n\n\nbuildah config --cmd '/moe-sticker-bot' $c1\n\nbuildah commit $c1 moe-sticker-bot:base_aarch64\n\nbuildah push moe-sticker-bot:base_aarch64 ghcr.io/star-39/moe-sticker-bot:base_aarch64\n\nfi\n#################################\n\n# Build container image.\nc1=$(buildah from --arch=arm64 ghcr.io/star-39/moe-sticker-bot:base_aarch64)\n\n\n# Install static build of ffmpeg.\ncurl -JOL \"https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz\"\ntar -xvf ffmpeg-master-latest-linuxarm64-gpl.tar.xz\nbuildah copy $c1 ffmpeg-master-latest-linuxarm64-gpl/bin/ffmpeg /usr/local/bin/ffmpeg\n\n\n# Build MSB go bin\nGOOS=linux GOARCH=arm64 go build -o moe-sticker-bot cmd/moe-sticker-bot/main.go \nbuildah copy $c1 moe-sticker-bot /moe-sticker-bot\n\n# Copy tools.\nbuildah copy $c1 tools/msb_kakao_decrypt.py /usr/local/bin/msb_kakao_decrypt.py\nbuildah copy $c1 tools/msb_emoji.py /usr/local/bin/msb_emoji.py\nbuildah copy $c1 tools/msb_rlottie.py /usr/local/bin/msb_rlottie.py\n\nbuildah commit $c1 moe-sticker-bot:aarch64\n\nbuildah push moe-sticker-bot:aarch64 ghcr.io/star-39/moe-sticker-bot:aarch64\n"
  },
  {
    "path": ".github/workflows/msb_oci_amd64.sh",
    "content": "#!/bin/sh\n\nGITHUB_TOKEN=$1\n\nbuildah login -u star-39 -p $GITHUB_TOKEN ghcr.io\n\n# AMD64\n#################################\nif true ; then\n\nc1=$(buildah from docker://archlinux:latest)\n\nbuildah run $c1 -- pacman -Sy\nbuildah run $c1 -- pacman --noconfirm -S libwebp libheif imagemagick curl gifsicle libarchive python python-pip make gcc\n\nbuildah run $c1 -- pip3 install emoji rlottie-python Pillow --break-system-packages\n\nbuildah run $c1 -- pacman --noconfirm -Rsc make gcc python-pip\nbuildah run $c1 -- sh -c 'yes | pacman -Scc'\n\nbuildah config --cmd '/moe-sticker-bot' $c1\n\nbuildah commit $c1 moe-sticker-bot:base\n\nbuildah push moe-sticker-bot:base ghcr.io/star-39/moe-sticker-bot:base\n\nfi\n#################################\n\n# Build container image.\nc1=$(buildah from ghcr.io/star-39/moe-sticker-bot:base)\n\n# Install static build of ffmpeg.\ncurl -JOL \"https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz\"\ntar -xvf ffmpeg-master-latest-linux64-gpl.tar.xz\nbuildah copy $c1 ffmpeg-master-latest-linux64-gpl/bin/ffmpeg /usr/local/bin/ffmpeg\n\n\n# Build MSB go bin\ngo build -o moe-sticker-bot cmd/moe-sticker-bot/main.go \nbuildah copy $c1 moe-sticker-bot /moe-sticker-bot\n\n# Copy tools.\nbuildah copy $c1 tools/msb_kakao_decrypt.py /usr/local/bin/msb_kakao_decrypt.py\nbuildah copy $c1 tools/msb_emoji.py /usr/local/bin/msb_emoji.py\nbuildah copy $c1 tools/msb_rlottie.py /usr/local/bin/msb_rlottie.py\n\nbuildah commit $c1 moe-sticker-bot:latest\n\nbuildah push moe-sticker-bot ghcr.io/star-39/moe-sticker-bot:amd64\nbuildah push moe-sticker-bot ghcr.io/star-39/moe-sticker-bot:latest\n"
  },
  {
    "path": ".github/workflows/release_note.md",
    "content": "See full changelog on https://github.com/star-39/moe-sticker-bot#changelog\n\n__moe-sticker-bot*__ is the binary for the bot itself.\n\n__msbimport*__ is the msbimport CLI utility to download and convert LINE/Kakao stickers from share link.\n\nRemember to install required dependencies!\n\n* ImageMagick\n* bsdtar (libarchive-tools)\n* ffmpeg\n* curl\n* python3 (optional, for following tools)\n* [msb_kakao_decrypt.py](https://github.com/star-39/moe-sticker-bot/tree/master/tools/msb_kakao_decrypt.py) (optional, for decrypting animated kakao)\n\nmoe-sticker-bot requires more dependencies:\n\n* gifsicle (optional, for converting GIF)\n* [msb_emoji.py](https://github.com/star-39/moe-sticker-bot/tree/master/tools/msb_emoji.py) (optional, for emoji assign)\n* [msb_rlottie.py](https://github.com/star-39/moe-sticker-bot/tree/master/tools/msb_rlottie.py) (optional, for converting TGS)\n"
  },
  {
    "path": ".gitignore",
    "content": ".vscode\n.env\n.venv\n__debug*\n*_data/\n\n.DS_Store\ndeploy.yaml\ntmp/\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "# [@moe_sticker_bot](https://t.me/moe_sticker_bot)\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/star-39/moe-sticker-bot.svg)](https://pkg.go.dev/github.com/star-39/moe-sticker-bot)  ![Go Report](https://goreportcard.com/badge/github.com/star-39/moe-sticker-bot)  ![CI](https://github.com/star-39/moe-sticker-bot/actions/workflows/msb_oci.yml/badge.svg)  ![CI](https://github.com/star-39/moe-sticker-bot/actions/workflows/build_binaries.yml/badge.svg) \n\n\n[<img width=\"500\" src=\"https://user-images.githubusercontent.com/75669297/222379608-1359ac0f-18ed-4a25-a91e-32974994d27b.png\">](https://t.me/moe_sticker_bot)\n\n---\n\nUse moe-sticker-bot, a Telegram bot, to easily import or download LINE/Kakaotalk/Telegram stickers, use your own image or video to create Telegram sticker set or CustomEmoji and manage it.\n\n---\n\nTelegram用萌萌貼圖BOT。\n\n匯入或下載LINE和kakaotalk貼圖包到Telegram. 使用自己的圖片和影片創建Telegram貼圖包或表情貼並管理.\n\n下載Telegram貼圖包/GIF。\n\n\n## Features/功能\n  * Import LINE or kakao stickers to Telegram without effort, you can batch or separately assign emojis.\n  * Create your own sticker set or CustomEmoji with your own images or videos in any format.\n  * Support mixed-format sticker set. You can put animated and static stickers in the same set.\n  * Batch download and convert Telegram stickers or GIFs to original or common formats.\n  * Export Telegram stickers to WhatsApp (requires [Msb App](https://github.com/star-39/msb_app), supports iPhone and Android).\n  * Manage your sticker set interactively through WebApp: add/move/remove/edit sticker and emoji.\n  * Provides a CLI app [msbimport](https://github.com/star-39/moe-sticker-bot/tree/master/pkg/msbimport) for downloading LINE/Kakaotalk stickers.\n\n  * 輕鬆匯入LINE/kakao貼圖包到Telegram, 可以統一或分開指定emoji.\n  * 輕鬆使用自己任意格式的圖片和影片來創建自己的貼圖包或表情貼.\n  * 支援混合貼圖包。動態靜態貼圖可以放在同一個包內。\n  * 下載Telegram/LINE/kakao貼圖包和GIF, 自動變換為常用格式, 並且保留原檔.\n  * 匯出Telegram的貼圖包至WhatsApp（需要安裝[Msb App](https://github.com/star-39/msb_app), 支援iPhone和Android）。\n  * 互動式WebApp可以輕鬆管理自己的貼圖包: 可以新增/刪除貼圖, 移動位置或修改emoji.\n  * 提供名為[msbimport](https://github.com/star-39/moe-sticker-bot/tree/master/pkg/msbimport)的終端機程式， 用於下載LINE/kakao貼圖。\n  \n  \n## Screenshots\n[![MSB](https://img.shields.io/badge/-%40moe__sticker__bot-blue?style=plastic&logo=telegram)](https://t.me/moe_sticker_bot)\n\n<img width=\"487\" alt=\"スクリーンショット 2023-02-27 午後7 29 35\" src=\"https://user-images.githubusercontent.com/75669297/221539624-c0cc32a9-477c-425f-8e98-6566326385b4.png\">\n\n\n<img width=\"500\" alt=\"スクリーンショット 2023-02-27 午後7 24 14\" src=\"https://user-images.githubusercontent.com/75669297/221538927-526a878a-5d86-4b45-ab9a-d324743e3b91.png\">\n\n<img width=\"500\" alt=\"スクリーンショット 2023-02-27 午後7 37 17\" src=\"https://user-images.githubusercontent.com/75669297/221541547-4618c9ef-9be3-4d50-b7da-fdc5b25e64b8.png\">\n\n\n<!-- <img width=\"500\" alt=\"スクリーンショット 2023-02-27 午後7 21 22\" src=\"https://user-images.githubusercontent.com/75669297/221538953-6c69dc08-5cb1-4f07-a9ce-43bacb9f1566.png\"> -->\n\n<!-- ![スクリーンショット 2022-02-12 003746](https://user-images.githubusercontent.com/75669297/153621406-16a619a8-e897-4857-947b-7d41e88fddcb.png) \n\n<img width=\"511\" alt=\"スクリーンショット 2022-03-24 19 58 09\" src=\"https://user-images.githubusercontent.com/75669297/159902095-fefbcbbf-1c5e-4c3e-9e55-eb28b48567e0.png\"> -->\n<!--<img width=\"500\" alt=\"スクリーンショット 2022-05-11 19 33 27\" src=\"https://user-images.githubusercontent.com/75669297/167830628-1dfc9941-4b3c-408d-bf64-1ef814e3efe8.png\"> <img width=\"500\" alt=\"スクリーンショット 2022-05-11 19 51 46\" src=\"https://user-images.githubusercontent.com/75669297/167833015-806b4f11-ecd9-4f10-9b9c-ecb7a20f8f97.png\">-->\n\n<!--<img width=\"500\" alt=\"スクリーンショット 2022-05-11 19 58 52\" src=\"https://user-images.githubusercontent.com/75669297/167834522-f1e988e8-bd24-44b1-a90c-f69791a9a623.png\">\n\n<img width=\"500\" alt=\"スクリーンショット 2023-02-11 午前1 53 55\" src=\"https://user-images.githubusercontent.com/75669297/218149914-65db79c0-c3f9-44ed-8043-1673eba41bc0.png\">\n-->\n<!--\n<img width=\"500\" alt=\"スクリーンショット 2023-02-11 午前2 15 56\" src=\"https://user-images.githubusercontent.com/75669297/218154599-c0af7aa5-e8ff-4f6d-9110-9b7175fbe585.png\">\n\n<img width=\"517\" alt=\"スクリーンショット 2022-12-12 午後2 31 32\" src=\"https://user-images.githubusercontent.com/75669297/206968834-d86c69d5-7e1d-4e36-9370-a66addc0c4fa.png\">\n-->\n<!-- \n<img width=\"535\" alt=\"スクリーンショット 2022-12-12 午後2 26 46\" src=\"https://user-images.githubusercontent.com/75669297/206968863-1bb7e5cd-0c43-4573-8292-3e3e629f39bf.png\"> \n\n\n<img width=\"562\" alt=\"スクリーンショット 2023-02-11 午前2 21 40\" src=\"https://user-images.githubusercontent.com/75669297/218155866-912739bc-b954-4ca2-97c1-d99e43f02a89.png\">\n<!--<img width=\"517\" alt=\"スクリーンショット 2022-12-12 午後2 47 22\" src=\"https://user-images.githubusercontent.com/75669297/206969650-cff19478-898a-4344-a73a-80469184053c.png\">\n-->\n\n<img width=\"394\" alt=\"スクリーンショット 2022-12-12 午後2 27 10\" src=\"https://user-images.githubusercontent.com/75669297/206968889-1fe25c05-6071-422b-9e1b-549d56f5d351.png\">\n\n\n<img width=\"500\" alt=\"スクリーンショット 2023-02-11 午前2 24 37\" src=\"https://user-images.githubusercontent.com/75669297/218156358-0145264f-ab11-4010-bfcd-2e38621d7381.png\">\n\n\n<img width=\"300\" src=\"https://user-images.githubusercontent.com/75669297/218153727-5fb1d3e0-3770-4dc8-a2b5-3e0ecd89a003.png\"/> <img width=\"300\" src=\"https://user-images.githubusercontent.com/75669297/221529085-2581bcca-fe49-46b0-8123-5614e90a838c.png\"/>\n\n\n\n## Deployment\n### Deploy with pre-built containers\nIt is recommended to deploy moe-sticker-bot using containers.\nA pre-built OCI container is available at https://github.com/users/star-39/packages/container/package/moe-sticker-bot\n\nRun:\n```\ndocker run -dt ghcr.io/star-39/moe-sticker-bot /moe-sticker-bot --bot_token=\"...\"\n```\nIf you are on ARM64 machine, use `aarch64` tag.\n\nSee a real world deployment example on [deployments/kubernetes_msb.yaml](https://github.com/star-39/moe-sticker-bot/blob/master/deployments/kubernetes_msb.yaml).\n\n\n### System Dependencies\n* ImageMagick(6 or 7, both fine)\n* bsdtar (libarchive-tools)\n* ffmpeg (Requires a lot of components, use a fat static build or package manager's ones pls!)\n* curl\n* gifsicle (for converting GIF)\n* python3 (for following tools)\n* [msb_emoji.py](https://github.com/star-39/moe-sticker-bot/tree/master/tools/msb_emoji.py) (for extracting emoji)\n* [msb_kakao_decrypt.py](https://github.com/star-39/moe-sticker-bot/tree/master/tools/msb_kakao_decrypt.py) (for decrypting animated kakao)\n* [msb_rlottie.py](https://github.com/star-39/moe-sticker-bot/tree/master/tools/msb_rlottie.py) (for converting TGS)\n* mariadb-server (optional, for database)\n* nginx (optional, for WebApp)\n\nDependencies above must be accessible through PATH. Don't ask me why they are reported missing by bot.\n\n## Build\n### Build Dependencies\n * golang v1.18+\n * nodejs v18+ (optional, for WebApp)\n * react-js v18+ (optional, for WebApp)\n\n```bash\ngit clone https://github.com/star-39/moe-sticker-bot && cd moe-sticker-bot\n\ngo build -o moe-sticker-bot cmd/moe-sticker-bot/main.go \n```\n\n#### WebApp\nSince 2.0 version of moe-sticker-bot, managing sticker set's order and emoji is achieved using Telegram's new WebApp technology. \n\nSee details on [web/webapp](https://github.com/star-39/moe-sticker-bot/tree/master/web/webapp3)\n\n\n## CHANGELOG\nv2.5.0-RC1(20240531)\n* Support mix-typed sticker set.\n* You can add video to static set and vice versa.\n* Removed WhatsApp export temporarily .\n* Many bug fixes.\n\nv2.4.0-RC3(20240226)\n  * Remove deduplication during database curation.\n  * LINE emoji can be imported to either sticker or CustomEmoji.\n  * Support creating sticker set with under 50 stickers in batch.\n  * Import speed is imcreased significantly.\n\nv2.4.0-RC1-RC2(20240207)\n  * Support Importing LINE Emoji into CustomEmoji.\n  * Support creating CustomEmoji.\n  * Support editing sticker emoji and title.\n  \nv2.3.14-2.3.15(20230228)\n  * Fix missing libwebp in OCI.\n  * Support TGS export.\n  * Improve GIF efficiency.\n\nv2.3.11-v2.3.13 (20230227)\n  * Fix flood limit by ignoring it.\n  * Fix managing video stickers.\n  * Improved WhatsApp export.\n  * Support region locked LINE Message sticker.\n  * 修復flood limit匯入錯誤\n  * 修復動態貼圖管理。\n  * 改善WhatsApp匯出。\n  * 支援有區域鎖的line訊息貼圖。\n\nv2.3.8-2.3.10 (20230217)\n  * Fix kakao import fatal, support more animated kakao.\n  * 修復KAKAO匯入錯誤, 支援更多KAKAO動態貼圖.\n  * Fix static webp might oversize(256KB)\n  * Fix panic during assign: invalid sticker emojis\n  * Fix some kakao animated treated as static\n  * Improved kakao static sticker quality\n  * Improved user experience\n\nv2.3.6-2.3.7 (20230213)\n  * Support webhook.\n  * Support animated webp for user sticker.\n  * Add change sticker set title \"feature\"\n  * Fix \"sticker order mismatch\" when using WebApp to sort.\n  * Fix error on emoji assign.\n  * Fix too large animated kakao sticker.\n  \nv2.3.1-2.3.5 (20230209)\n  * Fix i18n titles.\n  * Fix flood limit by implementing channel to limit autocommit cocurrency.\n  * Fix error on webapp\n  * Fix import hang\n  * Fix fatal error not being reported to user.\n  * Fix typos\n  \nv2.3.0 (20230207)\n  * Fix flood limit by using local api server.\n  * Support webhook and local api server.\n  * Huge performance gain.\n  * Fix /search panic.\n\nv2.2.1 (20230204)\n  * Fix webm too big.\n  * Fix id too long.\n  * Tuned flood limit algorithm.\n\nv2.2.0 (20230131)\n  * Support animated kakao sticker.\n  * 支援動態kakao貼圖。\n\nv2.1.0 (20230129)\n  * Support exporting sticker to WhatsApp.\n  * 支援匯出貼圖到WhatsApp\n\n2.0.1 (20230106)\n  * Fix special LINE officialaccount sticker.\n  * Fix `--log_level` cmdline parsing.\n  * Thank you all! This project has reached 100 stars!\n\n2.0.0 (20230105)\n  * Use new WebApp from /manage command to edit sticker set with ease.\n  * Send text or use /search command to search imported LINE/kakao sticker sets by all users.\n  * Auto import now happens on backgroud.\n  * Downloading sticker set is now lot faster.\n  * Fix many LINE import issues.\n  * 通過/manage指令使用新的WebApp輕鬆管理貼圖包.\n  * 直接傳送文字或使用/search指令來搜尋所有用戶匯入的LINE/KAKAO貼圖包.\n  * 自動匯入現在會在背景處理.\n  * 下載整個貼圖包的速度現在會快許多.\n  * 修復了許多LINE貼圖匯入的問題.\n  \n<details>\n<summary>Detailed 2.0 Changelogs 詳細的2.0變更列表</summary>\n\n2.0.0 (20230104)\n  * Improve flood limit handling.\n  * Auto LINE import now happens on backgroud.\n  * Improve GIF download.\n\n2.0.0 RC-7 (20221230)\n  * Support /search in group chat.\n  * Fix search result.\n  * Fix empty sticker title.\n  * Sticker download is now parallel.\n\n2.0.0 RC-6 (20221220)\n  * Fix line APNG with unexpected `tEXt` chunk.\n  * Changed length of webm from 2.9s to 3s.\n  * Minor improvements.\n\n2.0.0 RC-5 (20221211)\n  * Fix potential panic when onError\n  * Warn user sticker set is full.\n  * Fix LINE message sticker with region lock.\n\n2.0.0 RC-4 (20221211)\n  * Fix edit sticker on iOS\n  * Fix error editing multiple emojis.\n\n2.0.0 RC-3 (20221210)\n  * Complies to LINE store's new UA requeirments.\n  * Fix animated sticker in webapp.\n  * Fixed sticker download\n  * Fixed webapp image aspect ratio.\n\n2.0.0 RC-2 (20221210)\n  * Fix fatalpanic on webapp.\n  * Add /search functionality.\n  * Removed gin TLS support.\n  * Auto database curation.\n\n2.0.0 RC-1 (20221206)\n  * WebApp support for edit stickers.\n  * Code structure refactored.\n  * Now accepts options from cmdline instead of env var.\n  * Support parallel sticker download.\n  * Fix LINE officialaccount/event/sticker\n  * Fix kakao link with queries.\n\n</details>\n\n1.2.4 (20221111)\n  * Minor improvements.\n  * Fixed(almost) flood limit.\n  * Fixed kakao link with queries.\n\n1.2.2 (20220523)\n  * Improved user experience.\n\n1.2.1 (20220520)\n  * Improved emoji edit.\n\n1.2 (20220518)\n  * Fix import error for LINE ID < 775 \n  * Improved UX during /import.\n  * Warn user if sticker set is already full.\n\n1.1 (20220517)\n  * Code refactors.\n  * UX improvements.\n  * Skip error on TGS to GIF due to lottie issues.\n\n1.0 (20220513)\n  * First stable release in go version.\n  * Added support for downloading TGS and convert to GIF.\n  * Backing database for @moe_sticker_bot has gone complete sanitization.\n\n1.0 RC-9(20220512)\n  * Add an administrative command to _sanitize_ database, which purges duplicated stickers.\n  * Add an advanced command /register, to register your sticker to database.\n  * Minor bug fixes.\n  * This is the REAL final RC release, next one is stable!\n\n1.0 RC-8 GO(20220512)\n  * Fix rand number in ID.\n  * Major code refactor.\n  * Downlaod sticker now happens on background.\n  * Better documentation.\n  * This release should be the final RC... hopefully.\n\n1.0 RC-7 GO(20220511)\n  * You can specify custom ID when /create.\n  * Changed import ID naming scheme for cleaner look.\n  * Die immediately if FloodLimit exceeds 4.\n  * If everything looks good, this should be the last RC for 1.0\n\n1.0 RC-6 GO(20220511)\n  * New feature: Change sticker order.\n  * New feature: Edit sticker emoji.\n  * New import support: kakaotalk emoticon.\n  * Fix possible panic when editMessage.\n  * We are closing to a stable release! RC should not exceed 8.\n\n1.0 RC-5 GO(20220510)\n  * New feature: download raw line stickers to zip.\n  * FatalError now prints stack trace.\n  * zh-Hant is now default in auto LINE title.\n  * Quality of video sticker should improve by a bit.\n  * Fix possible slice out of range panic.\n  * If user experience FloodLimit over 3 times, terminate process.s\n\n1.0 RC-4 GO(20220509)\n  * Use my custom fork of telebot\n  * User sent sticker now supports any file.\n\n1.0 RC-3 GO(20220509)\n  * Split large zip to under 50MB chunks.\n  * Split long message to chunks.\n  * GIF downloaded is now in original 512px resolution.\n  * You can press \"No\" now when sent link or sticker.\n  * If error is not HTTP400, bot will retry for 3 times.\n  * Other minor improvements.\n\n1.0 RC-2 GO(20220508)\n  * Fix SEGV when user requested /quit\n  * Ignore FloodLimit by default since API will do retry at TDLib level.\n  * Fix emoji in database.\n  * Fix video sticker when /manage.\n  * Support line message sticker and emoticon(emoji).\n\n1.0 RC-1 GO(20220506)\n  * Completely rewritten whole project to golang\n  * Countless bug fixes.\n  * You can send sticker or link without a command now.\n  * Performance gained by a lot thanks to goroutine and worker pool.\n\n<details>\n<summary>Old changelogs</summary>\n\n5.1 RC-4 (20220423)\n  * Fix duplicated sticker.\n  * Fix alpha channel converting GIF.\n  \n5.1 RC-3 (20220416)\n  * Do not use joblib due to their bugs.\n  * /download_telegram_sticker now converts to standard GIFs.\n\n5.1 RC-2 (20220326)\n  * Significantly improved perf by using parallel loop.\n  * Sanitize kakao id starting with -\n\n5.1 RC-1 (20220309)\n  * Support kakaotalk emoticon.\n  * Add more check for telegram sticker id.\n\n5.0 RC-12 (20220305)\n  * Database now records line link and emoji settings.\n  * Fix issue when line name has <> marks.\n  * Fix issue adding video to static set.\n  * Fix hang handling CallbackQuery.\n\n5.0 RC-11 (20220303)\n  * You can now delete a sticker set.\n  * /manage_sticker_set will show sets you created.\n  * Fix missing sticker during USER_STICKER.\n\n5.0 RC-10 (20220226)\n  * Performance is now significantly improved.\n  * Fix issue converting Line message stickers.\n  * Bypass some regional block by LINE.\n\n5.0 RC-9 (20220223)\n  * Splitted line popups to two categories, one keeping animated only.\n  * Bot now has a database stroing \"good\" imported sticker sets.\n  * Fix duplicated stickers in sticker set.\n\n5.0 RC-8 (20220222)\n  * Fix user sticker parsing.\n  * Add support for MdIcoFlashAni_b\n\n5.0 RC-7 (20220215)\n  * Fix exception if user sent nothing during USER_STICKER\n  * Fix a bug where /import_line_sticker may have no response.\n  * Corrected file download limit.\n  * Fix animated sticon\n  * Fix import hang due to missing ffmpeg '-y' param.\n\n5.0 RC-6 (20220215)\n  * Fix python-telegram-bot WebHook problem.\n  * Fix emoji assign.\n  * Fix black background video sticker.\n  * Fix \"Sticker too big\" when uploading video sticker.\n\n5.0 RC-5 (20220214)\n  * Allow using WebHook for better performance.\n  * Code refactors.\n  * Support Line name sticker.\n\n5.0 RC-4 (20220212)\n  * Improved user sticker exprerience.\n\n5.0 RC-3 (20220212)\n  * Fix a bug where creating sticker set with one sticker will cause fatal error.\n  * Fix missing clean_userdata in /download_line_sticker\n  * Tune VP9 params to avoid hitting 256K limit. This reduces video quality by a bit.\n  \n5.0 RC-2 (20220211)\n  * Fix media_group\n  * Minor bug fixes.\n  * Version 5.0 now enters freature freeze. No new feature will be added. Will have bug fixes only.\n\n5.0 RC-1 (20220211)\n  * Support Line popup sticker without sound\n  * Support AVIF.\n  * Many bug fixes.\n\n5.0 ALPHA-1 (20220211)\n  * Full support of animated(video) sticker. 完整支援動態貼圖. アニメーションスタンプフル対応。\n  * New feature: /manage_sticker_set, now you can add, delete, move sticker in a sticker set.\n  * Add support for Line full screen sticker(animated).\n\n4.0 ALPHA-5 (20220210)\n  * Bring back fake RetryAfter check since some people still having this issue.\n\n4.0 ALPHA-4 (20220210)\n  * Support user uploaded animated(video) stickers. You can both create or add to set.\n  * Better support sticon(line_emoji)\n  * Bug fixes.\n\n4.0 ALPHA-3 (20220209)\n  * Supports all special line stickers,\n  * including effect_animation and sticon(emoji)\n\n4.0 ALPHA-1 (20220209)\n  * Supports animated line sticker import.\n</details>\n\n <!--\n## Technical Details\n![MSB_INFO](https://user-images.githubusercontent.com/75669297/210700704-4c9b366a-c72c-42fe-919c-336b7b8024c4.svg)\n-->\n\n\n## Special Thanks:\n[<img width=200 src=\"https://idcs-cb5322c0a68345bb83637843d27aa437.identity.oraclecloud.com/ui/v1/public/common/asset/defaultBranding/oracle-desktop-logo.gif\">](https://www.oracle.com/cloud/) for free 4CPU AArch64 Cloud Instance.\n\n<a href=\"http://t.me/StickerGroup\">貼圖群 - Sticker Group Taiwan</a> for testing and reporting.\n\n[LINE Corp](https://linecorp.com/) / [Kakao Corp](http://www.kakaocorp.com/) for cute stickers.\n \nhttps://github.com/blluv/KakaoTalkEmoticonDownloader MIT License Copyright @blluv\n \nhttps://github.com/laggykiller/rlottie-python GPL-2.0 license  Copyright @laggykiller\n\nYou and all the users! ☺\n\n\n## License\nThe GPL V3 License\n\n![image](https://www.gnu.org/graphics/gplv3-with-text-136x68.png)\n"
  },
  {
    "path": "cmd/moe-sticker-bot/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"os\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/star-39/moe-sticker-bot/core\"\n)\n\n// Common abbr. in this project:\n// S : Sticker\n// SS : StickerSet\n\nfunc main() {\n\tconf := parseCmdLine()\n\tcore.Init(conf)\n}\n\nfunc parseCmdLine() core.ConfigTemplate {\n\tvar help = flag.Bool(\"help\", false, \"Show help\")\n\tvar adminUid = flag.Int64(\"admin_uid\", -1, \"Admin's UID(optional)\")\n\tvar botToken = flag.String(\"bot_token\", \"\", \"Telegram Bot Token\")\n\tvar dataDir = flag.String(\"data_dir\", \"\", \"Overwrites the working directory where msb puts data.\")\n\tvar webappUrl = flag.String(\"webapp_url\", \"\", \"Public HTTPS URL to WebApp, in unset, webapp will be disabled.\")\n\tvar WebappApiListenAddr = flag.String(\"webapp_listen_addr\", \"\", \"Webapp API server listen address(IP:PORT)\")\n\tvar webappDataDir = flag.String(\"webapp_data_dir\", \"\", \"Where to put webapp data to share with ReactApp \")\n\tvar dbAddr = flag.String(\"db_addr\", \"\", \"mariadb(mysql) address, if unset, database will be disabled.\")\n\tvar dbUser = flag.String(\"db_user\", \"\", \"mariadb(mysql) usernmae\")\n\tvar dbPass = flag.String(\"db_pass\", \"\", \"mariadb(mysql) password\")\n\tvar logLevel = flag.String(\"log_level\", \"debug\", \"Log level\")\n\t// var botApiAddr = flag.String(\"botapi_addr\", \"\", \"Local Bot API Server Address\")\n\t// var botApiDir = flag.String(\"botapi_dir\", \"\", \"Local Bot API Working directory\")\n\t// var webhookPublicAddr = flag.String(\"webhook_public_addr\", \"\", \"Webhook public address(WebhookEndpoint).\")\n\t// var webhookListenAddr = flag.String(\"webhook_listen_addr\", \"\", \"Webhook listen address(IP:PORT)\")\n\t// var webhookCert = flag.String(\"webhook_cert\", \"\", \"Certificate for WebHook\")\n\tflag.Parse()\n\tif *help {\n\t\tflag.Usage()\n\t\tprintln(\"Only --bot_token is required to run.\")\n\t\tos.Exit(0)\n\t}\n\n\tconf := core.ConfigTemplate{}\n\n\tconf.BotToken = *botToken\n\tif conf.BotToken == \"\" {\n\t\tlog.Error(\"Please set --bot_token\")\n\t\tlog.Error(\"Use --help to see options.\")\n\t\tlog.Fatal(\"No bot token provided!\")\n\t}\n\tif !strings.Contains(conf.BotToken, \":\") {\n\t\tlog.Fatal(\"Bad bot token!\")\n\t}\n\n\tconf.DbAddr = *dbAddr\n\tconf.DbUser = *dbUser\n\tconf.DbPass = *dbPass\n\n\tconf.WebappUrl = *webappUrl\n\tconf.WebappDataDir = *webappDataDir\n\tconf.WebappApiListenAddr = *WebappApiListenAddr\n\n\t// conf.BotApiAddr = *botApiAddr\n\t// conf.BotApiDir = *botApiDir\n\t// conf.WebhookPublicAddr = *webhookPublicAddr\n\t// conf.WebhookListenAddr = *webhookListenAddr\n\t// conf.WebhookCert = *webhookCert\n\n\tconf.LogLevel = *logLevel\n\n\tconf.AdminUid = *adminUid\n\tconf.DataDir = *dataDir\n\n\treturn conf\n\t// core.Config = conf\n}\n"
  },
  {
    "path": "cmd/msbimport/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/star-39/moe-sticker-bot/pkg/msbimport\"\n)\n\nfunc main() {\n\tvar link = flag.String(\"link\", \"\", \"Import link(LINE/kakao)\")\n\tvar convertTG = flag.Bool(\"convert\", false, \"Convert to Telegram format(WEBP/WEBM)\")\n\tvar outputJson = flag.Bool(\"json\", false, \"Output JSON serialized LineData, useful when integrating with other apps.\")\n\tvar workDir = flag.String(\"dir\", \"\", \"Where to put sticker files.\")\n\tvar logLevel = flag.String(\"log_level\", \"debug\", \"Log level\")\n\tflag.Parse()\n\n\tif *outputJson {\n\t\tlog.SetLevel(log.FatalLevel)\n\t} else {\n\t\tll, err := log.ParseLevel(*logLevel)\n\t\tif err != nil {\n\t\t\tlog.SetLevel(log.DebugLevel)\n\t\t} else {\n\t\t\tlog.SetLevel(ll)\n\t\t}\n\t}\n\n\tmsbimport.InitConvert()\n\n\tctx, _ := context.WithCancel(context.Background())\n\tld := &msbimport.LineData{}\n\n\t// LineData will be parsed to ld.\n\twarn, err := msbimport.ParseImportLink(*link, ld)\n\tif err != nil {\n\t\tlog.Error(\"Error parsing import link!\")\n\t\tlog.Fatalln(err)\n\t}\n\tif warn != \"\" {\n\t\tlog.Warnln(warn)\n\t}\n\n\terr = msbimport.PrepareImportStickers(ctx, ld, *workDir, *convertTG, false)\n\tif err != nil {\n\t\tlog.Fatalln(err)\n\t}\n\n\tfor _, lf := range ld.Files {\n\t\tlf.Wg.Wait()\n\t\tif lf.CError != nil {\n\t\t\tlog.Error(lf.CError)\n\t\t}\n\t\tlog.Infoln(\"Original File:\", lf.OriginalFile)\n\t\tif *convertTG {\n\t\t\tlog.Infoln(\"Converted File:\", lf.ConvertedFile)\n\t\t}\n\t}\n\n\tif *outputJson {\n\t\tld.TitleWg.Wait()\n\t\tjbytes, err := json.Marshal(ld)\n\t\tif err != nil {\n\t\t\tlog.Fatalln(err)\n\t\t}\n\t\tfmt.Print(string(jbytes))\n\t}\n}\n"
  },
  {
    "path": "core/admin.go",
    "content": "package core\n\nimport (\n\ttele \"gopkg.in/telebot.v3\"\n)\n\nfunc cmdSitRep(c tele.Context) error {\n\t// Report status.\n\t// stat := []string{}\n\t// py_emoji_ok, _ := httpGet(\"http://127.0.0.1:5000/status\")\n\t// stat = append(stat, \"py_emoji_ok? :\"+py_emoji_ok)\n\t// c.Send(strings.Join(stat, \"\\n\"))\n\n\treturn nil\n}\n\nfunc cmdGetFID(c tele.Context) error {\n\tinitUserData(c, \"getfid\", \"waitMFile\")\n\tif c.Message().Media() != nil {\n\t\treturn c.Reply(c.Message().Media().MediaFile().FileID)\n\t} else {\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "core/commands.go",
    "content": "package core\n\nimport (\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\ttele \"gopkg.in/telebot.v3\"\n)\n\nfunc cmdCreate(c tele.Context) error {\n\tinitUserData(c, \"create\", \"waitSType\")\n\treturn sendAskSTypeToCreate(c)\n}\n\nfunc cmdManage(c tele.Context) error {\n\terr := sendUserOwnedS(c)\n\tif err != nil {\n\t\treturn sendNoSToManage(c)\n\t}\n\t// V2.3: Do not init command on /manage\n\t// initUserData(c, \"manage\", \"waitSManage\")\n\tsendAskSToManage(c)\n\treturn nil\n}\n\nfunc cmdImport(c tele.Context) error {\n\t// V2.2: Do not init command on /import\n\t// initUserData(c, \"import\", \"waitImportLink\")\n\treturn sendAskImportLink(c)\n}\n\nfunc cmdDownload(c tele.Context) error {\n\t// V2.2: Do not init command on /download\n\t// initUserData(c, \"download\", \"waitSDownload\")\n\treturn sendAskWhatToDownload(c)\n}\n\nfunc cmdAbout(c tele.Context) error {\n\tsendAboutMessage(c)\n\treturn nil\n}\n\nfunc cmdFAQ(c tele.Context) error {\n\tsendFAQ(c)\n\treturn nil\n}\n\nfunc cmdPrivacy(c tele.Context) error {\n\treturn sendPrivacy(c)\n}\n\nfunc cmdChangelog(c tele.Context) error {\n\treturn sendChangelog(c)\n}\n\nfunc cmdStart(c tele.Context) error {\n\treturn sendStartMessage(c)\n}\n\nfunc cmdCommandList(c tele.Context) error {\n\treturn sendCommandList(c)\n}\n\nfunc cmdSearch(c tele.Context) error {\n\tif c.Chat().Type == tele.ChatGroup || c.Chat().Type == tele.ChatSuperGroup {\n\t\treturn cmdGroupSearch(c)\n\t}\n\tinitUserData(c, \"search\", \"waitSearchKW\")\n\treturn sendAskSearchKeyword(c)\n}\n\nfunc cmdGroupSearch(c tele.Context) error {\n\targs := strings.Split(c.Text(), \" \")\n\tif len(args) < 2 {\n\t\treturn sendBadSearchKeyword(c)\n\t}\n\tkeywords := args[1:]\n\tlines := searchLineS(keywords)\n\tif len(lines) == 0 {\n\t\treturn sendSearchNoResult(c)\n\t}\n\treturn sendSearchResult(10, lines, c)\n}\n\nfunc cmdQuit(c tele.Context) error {\n\tlog.Debug(\"Received user quit request.\")\n\tud, exist := users.data[c.Sender().ID]\n\tif !exist {\n\t\treturn c.Send(\"Please use /start\", &tele.ReplyMarkup{RemoveKeyboard: true})\n\t}\n\tc.Send(\"Please wait...\")\n\tud.cancel()\n\t// for _, s := range ud.stickerData.stickers {\n\t// \ts.wg.Wait()\n\t// }\n\tterminateSession(c)\n\treturn nil\n}\n"
  },
  {
    "path": "core/config.go",
    "content": "package core\n\nvar msbconf ConfigTemplate\n\ntype ConfigTemplate struct {\n\tAdminUid int64\n\tDataDir  string\n\tLogLevel string\n\t// UseDB            bool\n\tBotToken string\n\t// WebApp    bool\n\tWebappUrl           string\n\tWebappApiListenAddr string\n\tWebappDataDir       string\n\tDbAddr              string\n\tDbUser              string\n\tDbPass              string\n\t// BotApiAddr       string\n\t// BotApiDir        string\n\t// WebhookPublicAddr string\n\t// WebhookListenAddr string\n\t// WebhookCert        string\n\t// WebhookSecretToken string\n}\n"
  },
  {
    "path": "core/database.go",
    "content": "package core\n\nimport (\n\t\"database/sql\"\n\t\"strings\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n/*\n\nDATABASE VERSION 2 SCHEMA\nMariaDB > show tables;\n+----------------------------------+\n| Tables_in_BOT_NAME_db |\n+----------------------------------+\n| line                             |\n| properties                       |\n| stickers                         |\n+----------------------------------+\n\nMariaDB > desc line;\n+------------+--------------+------+-----+---------+-------+\n| Field      | Type         | Null | Key | Default | Extra |\n+------------+--------------+------+-----+---------+-------+\n| line_id    | varchar(128) | YES  |     | NULL    |       |\n| tg_id      | varchar(128) | YES  |     | NULL    |       |\n| tg_title   | varchar(255) | YES  |     | NULL    |       |\n| line_link  | varchar(512) | YES  |     | NULL    |       |\n| auto_emoji | tinyint(1)   | YES  |     | NULL    |       |\n+------------+--------------+------+-----+---------+-------+\n\nMariaDB > desc stickers;\n+-----------+--------------+------+-----+---------+-------+\n| Field     | Type         | Null | Key | Default | Extra |\n+-----------+--------------+------+-----+---------+-------+\n| user_id   | bigint(20)   | YES  |     | NULL    |       |\n| tg_id     | varchar(128) | YES  |     | NULL    |       |\n| tg_title  | varchar(255) | YES  |     | NULL    |       |\n| timestamp | bigint(20)   | YES  |     | NULL    |       |\n+-----------+--------------+------+-----+---------+-------+\n\nMariaDB > desc properties;\n+-------+--------------+------+-----+---------+-------+\n| Field | Type         | Null | Key | Default | Extra |\n+-------+--------------+------+-----+---------+-------+\n| name  | varchar(128) | NO   | PRI | NULL    |       |\n| value | varchar(128) | YES  |     | NULL    |       |\n+-------+--------------+------+-----+---------+-------+\n\nCurrent entries for properties:\nname: DB_VER\nvalue: 2\nname: last_line_dedup_index\nvalue: -1\n\n*/\n\nvar db *sql.DB\n\nconst DB_VER = \"2\"\n\nfunc initDB(dbname string) error {\n\taddr := msbconf.DbAddr\n\tuser := msbconf.DbUser\n\tpass := msbconf.DbPass\n\tparams := make(map[string]string)\n\tparams[\"autocommit\"] = \"1\"\n\tdsn := &mysql.Config{\n\t\tUser:                 user,\n\t\tPasswd:               pass,\n\t\tNet:                  \"tcp\",\n\t\tAddr:                 addr,\n\t\tAllowNativePasswords: true,\n\t\tParams:               params,\n\t}\n\tdb, _ = sql.Open(\"mysql\", dsn.FormatDSN())\n\n\terr := verifyDB(dsn, dbname)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdb.Close()\n\tdsn.DBName = dbname\n\tdb, _ = sql.Open(\"mysql\", dsn.FormatDSN())\n\tlog.Debugln(\"DB DSN:\", dsn.FormatDSN())\n\n\tvar dbVer string\n\terr = db.QueryRow(\"SELECT value FROM properties WHERE name=?\", \"DB_VER\").Scan(&dbVer)\n\tif err != nil {\n\t\tlog.Errorln(\"Error quering dbVer, database corrupt? :\", err)\n\t\treturn err\n\t}\n\n\tlog.Infoln(\"Queried dbVer is :\", dbVer)\n\tcheckUpgradeDatabase(dbVer)\n\n\tlog.WithFields(log.Fields{\"Addr\": addr, \"DBName\": dbname}).Info(\"MariaDB OK.\")\n\n\treturn nil\n}\n\nfunc verifyDB(dsn *mysql.Config, dbname string) error {\n\terr := db.Ping()\n\tif err != nil {\n\t\tlog.Errorln(\"Error connecting to mariadb!! DSN: \", dsn.FormatDSN())\n\t\treturn err\n\t}\n\n\t_, err = db.Exec(\"USE \" + dbname)\n\tif err != nil {\n\t\tlog.Infoln(\"Can't USE database!\", err)\n\t\tlog.Infof(\"Database name:%s does not seem to exist, attempting to create.\", dbname)\n\t\terr2 := createMariadb(dsn, dbname)\n\t\tif err2 != nil {\n\t\t\tlog.Errorln(\"Error creating mariadb database!! DSN:\", dsn.FormatDSN())\n\t\t\treturn err2\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc checkUpgradeDatabase(queriedDbVer string) {\n\tif queriedDbVer == \"1\" {\n\t\tdb.Exec(\"INSERT properties (name, value) VALUES (?, ?)\", \"last_line_dedup_index\", \"-1\") //value is string!\n\t\tdb.Exec(\"UPDATE properties SET value=? WHERE name=?\", \"2\", \"DB_VER\")\n\t\tlog.Info(\"Upgraded DB_VER from 1 to 2\")\n\t}\n}\n\nfunc createMariadb(dsn *mysql.Config, dbname string) error {\n\t_, err := db.Exec(\"CREATE DATABASE \" + dbname + \" CHARACTER SET utf8mb4\")\n\tif err != nil {\n\t\tlog.Errorln(\"Error CREATE DATABASE!\", err)\n\t\treturn err\n\t}\n\tdb.Close()\n\tdsn.DBName = dbname\n\tdb, _ = sql.Open(\"mysql\", dsn.FormatDSN())\n\tdb.Exec(\"CREATE TABLE line (line_id VARCHAR(128), tg_id VARCHAR(128), tg_title VARCHAR(255), line_link VARCHAR(512), auto_emoji BOOL)\")\n\tdb.Exec(\"CREATE TABLE properties (name VARCHAR(128) PRIMARY KEY, value VARCHAR(128))\")\n\tdb.Exec(\"CREATE TABLE stickers (user_id BIGINT, tg_id VARCHAR(128), tg_title VARCHAR(255), timestamp BIGINT)\")\n\tdb.Exec(\"INSERT properties (name, value) VALUES (?, ?)\", \"last_line_dedup_index\", \"-1\")\n\tdb.Exec(\"INSERT properties (name, value) VALUES (?, ?)\", \"DB_VER\", DB_VER)\n\tlog.Infoln(\"Mariadb initialized with DB_VER :\", DB_VER)\n\treturn nil\n}\n\nfunc insertLineS(lineID string, lineLink string, tgID string, tgTitle string, aE bool) {\n\tif db == nil {\n\t\treturn\n\t}\n\tif lineID == \"\" || lineLink == \"\" || tgID == \"\" || tgTitle == \"\" {\n\t\tlog.Warn(\"Empty entry to insert line s\")\n\t\treturn\n\t}\n\t_, err := db.Exec(\"INSERT line (line_id, line_link, tg_id, tg_title, auto_emoji) VALUES (?, ?, ?, ?, ?)\",\n\t\tlineID, lineLink, tgID, tgTitle, aE)\n\n\tif err != nil {\n\t\tlog.Errorln(\"Failed to insert line s:\", lineID, lineLink)\n\t} else {\n\t\tlog.Infoln(\"Insert LineS OK ->\", lineID, lineLink, tgID, tgTitle, aE)\n\t}\n}\n\nfunc insertUserS(uid int64, tgID string, tgTitle string, timestamp int64) {\n\tif db == nil {\n\t\treturn\n\t}\n\tif tgID == \"\" || tgTitle == \"\" {\n\t\tlog.Warn(\"Empty entry to insert user s\")\n\t\treturn\n\t}\n\t_, err := db.Exec(\"INSERT stickers (user_id, tg_id, tg_title, timestamp) VALUES (?, ?, ?, ?)\",\n\t\tuid, tgID, tgTitle, timestamp)\n\n\tif err != nil {\n\t\tlog.Errorln(\"Failed to insert user s:\", tgID, tgTitle)\n\t} else {\n\t\tlog.Infoln(\"Insert UserS OK ->\", tgID, tgTitle, timestamp)\n\t}\n}\n\n// Pass QUERY_ALL to select all rows.\nfunc queryLineS(id string) []LineStickerQ {\n\tif db == nil {\n\t\treturn nil\n\t}\n\tvar qs *sql.Rows\n\tvar lines []LineStickerQ\n\tvar tgTitle string\n\tvar tgID string\n\tvar aE bool\n\tif id == \"QUERY_ALL\" {\n\t\tqs, _ = db.Query(\"SELECT tg_title, tg_id, auto_emoji FROM line\")\n\t} else {\n\t\tqs, _ = db.Query(\"SELECT tg_title, tg_id, auto_emoji FROM line WHERE line_id=?\", id)\n\t}\n\tdefer qs.Close()\n\tfor qs.Next() {\n\t\terr := qs.Scan(&tgTitle, &tgID, &aE)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tlines = append(lines, LineStickerQ{\n\t\t\tTg_id:    tgID,\n\t\t\tTg_title: tgTitle,\n\t\t\tAe:       aE,\n\t\t})\n\t\tlog.Debugf(\"Matched line record: id:%s | title:%s | ae:%v\", tgID, tgTitle, aE)\n\t}\n\terr := qs.Err()\n\tif err != nil {\n\t\tlog.Errorln(\"error quering line db: \", id)\n\t\treturn nil\n\t}\n\treturn lines\n}\n\n// Pass -1 to query all rows.\nfunc queryUserS(uid int64) []UserStickerQ {\n\tif db == nil {\n\t\treturn nil\n\t}\n\tvar usq []UserStickerQ\n\tvar q *sql.Rows\n\tvar tgTitle string\n\tvar tgID string\n\tvar timestamp int64\n\n\tif uid == -1 {\n\t\tq, _ = db.Query(\"SELECT tg_title, tg_id, timestamp FROM stickers\")\n\t} else {\n\t\tq, _ = db.Query(\"SELECT tg_title, tg_id, timestamp FROM stickers WHERE user_id=?\", uid)\n\t}\n\tdefer q.Close()\n\tfor q.Next() {\n\t\terr := q.Scan(&tgTitle, &tgID, &timestamp)\n\t\tif err != nil {\n\t\t\tlog.Errorln(\"error scanning user db all\", err)\n\t\t\treturn nil\n\t\t}\n\t\tusq = append(usq, UserStickerQ{\n\t\t\ttg_id:     tgID,\n\t\t\ttg_title:  tgTitle,\n\t\t\ttimestamp: timestamp,\n\t\t})\n\t}\n\terr := q.Err()\n\tif err != nil {\n\t\tlog.Errorln(\"error quering all user S\")\n\t\treturn nil\n\t}\n\treturn usq\n}\n\nfunc matchUserS(uid int64, id string) bool {\n\tif db == nil {\n\t\treturn false\n\t}\n\t//Allow admin to manage all sticker sets.\n\t// if uid == msbconf.AdminUid {\n\t// \treturn true\n\t// }\n\tqs, _ := db.Query(\"SELECT * FROM stickers WHERE user_id=? AND tg_id=?\", uid, id)\n\tdefer qs.Close()\n\treturn qs.Next()\n}\n\nfunc deleteUserS(tgID string) {\n\tif db == nil {\n\t\treturn\n\t}\n\t_, err := db.Exec(\"DELETE FROM stickers WHERE tg_id=?\", tgID)\n\tif err != nil {\n\t\tlog.Errorln(\"Delete user s err:\", err)\n\t} else {\n\t\tlog.Infoln(\"Deleted from database for user sticker:\", tgID)\n\t}\n}\n\nfunc deleteLineS(tgID string) {\n\tif db == nil {\n\t\treturn\n\t}\n\t_, err := db.Exec(\"DELETE FROM line WHERE tg_id=?\", tgID)\n\tif err != nil {\n\t\tlog.Errorln(\"Delete line s err:\", err)\n\t} else {\n\t\tlog.Infoln(\"Deleted from database for line sticker:\", tgID)\n\t}\n}\n\nfunc updateLineSAE(ae bool, tgID string) error {\n\tif db == nil {\n\t\treturn nil\n\t}\n\t_, err := db.Exec(\"UPDATE line SET auto_emoji=? WHERE tg_id=?\", ae, tgID)\n\treturn err\n}\n\nfunc searchLineS(keywords []string) []LineStickerQ {\n\tif db == nil {\n\t\treturn nil\n\t}\n\tvar statements []string\n\tfor _, s := range keywords {\n\t\tstatements = append(statements, \"'%\"+s+\"%'\")\n\t}\n\tstatement := strings.Join(statements, \" AND tg_title LIKE \")\n\tlog.Debugln(\"database: search statement:\", statement)\n\tqs, err := db.Query(\"SELECT tg_title, tg_id, auto_emoji FROM line WHERE tg_title LIKE \" + statement)\n\tif err != nil {\n\t\tlog.Warnln(\"db q err:\", err)\n\t\treturn nil\n\t}\n\n\tvar lines []LineStickerQ\n\tvar tgTitle string\n\tvar tgID string\n\tvar aE bool\n\tdefer qs.Close()\n\tfor qs.Next() {\n\t\terr := qs.Scan(&tgTitle, &tgID, &aE)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tlines = append(lines, LineStickerQ{\n\t\t\tTg_id:    tgID,\n\t\t\tTg_title: tgTitle,\n\t\t\tAe:       aE,\n\t\t})\n\t\tlog.Debugf(\"Search matched line record: id:%s | title:%s | ae:%v\", tgID, tgTitle, aE)\n\t}\n\terr = qs.Err()\n\tif err != nil {\n\t\tlog.Errorln(\"error searching line db: \", keywords)\n\t\treturn nil\n\t}\n\treturn lines\n}\n\nfunc curateDatabase() error {\n\tlog.Info(\"Starting database curation...\")\n\tinvalidSSCount := 0\n\t//Line stickers.\n\tls := queryLineS(\"QUERY_ALL\")\n\tfor _, l := range ls {\n\t\tlog.Debugf(\"Scanning:%s\", l.Tg_id)\n\t\tss, err := b.StickerSet(l.Tg_id)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"is invalid\") {\n\t\t\t\tlog.Infof(\"SS:%s is invalid. purging it from db...\", l.Tg_id)\n\t\t\t\tinvalidSSCount++\n\t\t\t\tdeleteLineS(l.Tg_id)\n\t\t\t\tdeleteUserS(l.Tg_id)\n\t\t\t} else {\n\t\t\t\tlog.Errorln(err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tfor si := range ss.Stickers {\n\t\t\tif si > 0 {\n\t\t\t\tif ss.Stickers[si].Emoji != ss.Stickers[si-1].Emoji {\n\t\t\t\t\tlog.Debugln(\"Setting auto emoji to FALSE for \", l.Tg_id)\n\t\t\t\t\tupdateLineSAE(false, l.Tg_id)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t//User stickers.\n\tus := queryUserS(-1)\n\tfor _, u := range us {\n\t\tlog.Debugf(\"Checking:%s\", u.tg_id)\n\t\t_, err := b.StickerSet(u.tg_id)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"is invalid\") {\n\t\t\t\tlog.Warnf(\"SS:%s is invalid. purging it from db...\", u.tg_id)\n\t\t\t\tdeleteUserS(u.tg_id)\n\t\t\t} else {\n\t\t\t\tlog.Errorln(err)\n\t\t\t}\n\t\t}\n\t}\n\n\tlog.Infof(\"Database curation done. invalid:%d\", invalidSSCount)\n\treturn nil\n}\n\n// func setLastLineDedupIndex(index int) {\n// \tvalue := strconv.Itoa(index)\n// \tdb.Exec(\"UPDATE properties SET value=? WHERE name=?\", value, \"last_line_dedup_index\")\n// \tlog.Infoln(\"setLastLineDedupIndex to :\", value)\n// }\n\n// func getLastLineDedupIndex() int {\n// \tvar value string\n// \tdb.QueryRow(\"SELECT value FROM properties WHERE name=?\", \"last_line_dedup_index\").Scan(&value)\n// \tindex, _ := strconv.Atoi(value)\n// \tlog.Infoln(\"getLastLineDedupIndex\", value)\n// \treturn index\n// }\n"
  },
  {
    "path": "core/define.go",
    "content": "package core\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/go-co-op/gocron\"\n\t\"github.com/panjf2000/ants/v2\"\n\t\"github.com/star-39/moe-sticker-bot/pkg/msbimport\"\n\ttele \"gopkg.in/telebot.v3\"\n)\n\nvar BOT_VERSION = \"2.5.0-RC1\"\n\nvar b *tele.Bot\nvar cronScheduler *gocron.Scheduler\n\nvar dataDir string\nvar botName string\n\n// ['uid'] -> bool channels\nvar autocommitWorkersList = make(map[int64][]chan bool)\nvar users Users\n\nvar MSB_DEFAULT_STICKER_KEYWORDS = []string{\"sticker\", \"moe_sticker_bot\", \"moe\"}\n\nconst (\n\tCB_DN_WHOLE           = \"dall\"\n\tCB_DN_SINGLE          = \"dsingle\"\n\tCB_OK_IMPORT          = \"yesimport\"\n\tCB_OK_IMPORT_EMOJI    = \"yesimportemoji\"\n\tCB_OK_DN              = \"yesd\"\n\tCB_BYE                = \"bye\"\n\tCB_MANAGE             = \"manage\"\n\tCB_DONE_ADDING        = \"done\"\n\tCB_YES                = \"yes\"\n\tCB_NO                 = \"no\"\n\tCB_DEFAULT_TITLE      = \"titledefault\"\n\tCB_EXPORT_WA          = \"exportwa\"\n\tCB_ADD_STICKER        = \"adds\"\n\tCB_DELETE_STICKER     = \"dels\"\n\tCB_DELETE_STICKER_SET = \"delss\"\n\tCB_CHANGE_TITLE       = \"changetitle\"\n\tCB_REGULAR_STICKER    = \"regular\"\n\tCB_CUSTOM_EMOJI       = \"customemoji\"\n\n\tST_WAIT_WEBAPP = \"waitWebApp\"\n\tST_PROCESSING  = \"process\"\n\n\tFID_KAKAO_SHARE_LINK = \"AgACAgEAAxkBAAEjezVj3_YXwaQ8DM-107IzlLSaXyG6yAACfKsxG3z7wEadGGF_gJrcnAEAAwIAA3kAAy4E\"\n\n\tLINK_TG     = \"t.me\"\n\tLINK_LINE   = \"line.me\"\n\tLINK_KAKAO  = \"kakao.com\"\n\tLINK_IMPORT = \"IMPORT\"\n)\n\n// Object for quering database for Line Sticker.\ntype LineStickerQ struct {\n\tLine_id   string\n\tLine_link string\n\tTg_id     string\n\tTg_title  string\n\tAe        bool\n}\n\n// Object for quering database for User Sticker.\ntype UserStickerQ struct {\n\ttg_id     string\n\ttg_title  string\n\ttimestamp int64\n}\n\n// Telegram API JSON.\ntype WebAppUser struct {\n\tId            int\n\tIs_bot        bool\n\tFirst_name    string\n\tLast_name     string\n\tUsername      string\n\tLanguage_code string\n\tIs_premium    bool\n\tPhoto_url     string\n}\n\n// Unique user data for one user and one session.\ntype UserData struct {\n\t//waitgroup for sticker set, wait before commit.\n\twg sync.WaitGroup\n\t//commit channel for emoji assign\n\tcommitChans []chan bool\n\tctx         context.Context\n\tcancel      context.CancelFunc\n\n\t//Current conversational state.\n\tstate     string\n\tsessionID string\n\tworkDir   string\n\t//Current command.\n\tcommand          string\n\tprogress         string\n\tprogressMsg      *tele.Message\n\tlineData         *msbimport.LineData\n\tstickerData      *StickerData\n\twebAppUser       *WebAppUser\n\twebAppQID        string\n\twebAppWorkerPool *ants.PoolWithFunc\n\tlastContext      tele.Context\n}\n\n// Map for users, identified by user id.\n// All temporal user data are stored in this struct.\ntype Users struct {\n\tmu   sync.Mutex\n\tdata map[int64]*UserData\n}\n\n// Object for ants worker function.\n// wg must be initiated with wg.Add(1)\ntype StickerMoveObject struct {\n\twg       sync.WaitGroup\n\terr      error\n\tsd       *StickerData\n\toldIndex int\n\tnewIndex int\n}\n\nfunc (ud *UserData) udSetState(state string) {\n\tud.state = state\n}\n\n// StickerFile object, for internal use.\n// Also used for ants worker function.\n// wg must be initialized with wg.Add(1) and must be waited when cPath is needed!\ntype StickerFile struct {\n\twg sync.WaitGroup\n\t//Telegram FileID(if exists on cloud)\n\tfileID string\n\t// path of original file\n\t// If fileID exists, oPath can be omitted.\n\toPath string\n\t// path of converted filea\n\tcPath  string\n\tcError error\n\t//////////////////\n\t//Following fields comply with tele.InputSticker\n\t//////////////////\n\temojis   []string `json:\"emoji_list\"`\n\tkeywords []string `json:\"keywords\"`\n\t//One of static, video, animated.\n\tformat string `json:\"format\"`\n}\n\n// General sticker data for internal use.\ntype StickerData struct {\n\tid string\n\t// link     string\n\ttitle          string\n\temojis         []string\n\tstickers       []*StickerFile\n\tstickerSet     *tele.StickerSet\n\tsDnObjects     []*StickerDownloadObject\n\tstickerSetType tele.StickerSetType\n\t//either static or video, used for CreateNewStickerSet\n\t// getFormat     StickerData\n\tisVideo       bool\n\tisAnimated    bool\n\tisCustomEmoji bool\n\tpos           int\n\t// amount of local files\n\tlAmount int\n\t// amount on cloud\n\tcAmount int\n\t// amout of flood error encounterd\n\tflCount int\n}\n\ntype StickerDownloadObject struct {\n\twg      sync.WaitGroup\n\tsticker tele.Sticker\n\tdest    string\n\tisVideo bool\n\t//Convert to conventional format?\n\tneedConvert bool\n\t//Shrink oversized GIF?\n\tshrinkGif bool\n\t//need to convert to WebApp use case\n\tforWebApp bool\n\t//need to convert to WhatsApp format\n\tforWhatsApp bool\n\t//need 96px PNG thumb for WhatsApp\n\tforWhatsAppThumb bool\n\t/*\n\t\tFollowing fields are yielded by worker after wg is done.\n\t*/\n\t//Original sticker file downloaded.\n\tof string\n\t//Converted sticker file.\n\tcf string\n\t//Returned error.\n\terr error\n}\n"
  },
  {
    "path": "core/init.go",
    "content": "package core\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\t\"time\"\n\n\t\"github.com/go-co-op/gocron\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/star-39/moe-sticker-bot/pkg/msbimport\"\n\ttele \"gopkg.in/telebot.v3\"\n)\n\nfunc Init(conf ConfigTemplate) {\n\tmsbconf = conf\n\tinitLogrus()\n\tmsbimport.InitConvert()\n\tb = initBot(conf)\n\tinitWorkspace(b)\n\tinitWorkersPool()\n\tgo initGoCron()\n\tif msbconf.WebappUrl != \"\" {\n\t\tInitWebAppServer()\n\t} else {\n\t\tlog.Info(\"WebApp not enabled.\")\n\t}\n\n\tlog.WithFields(log.Fields{\"botName\": botName, \"dataDir\": dataDir}).Info(\"Bot OK.\")\n\n\t// complies to telebot v3.1\n\tb.Use(Recover())\n\n\tb.Handle(\"/quit\", cmdQuit)\n\tb.Handle(\"/cancel\", cmdQuit)\n\tb.Handle(\"/exit\", cmdQuit)\n\tb.Handle(\"/faq\", cmdFAQ)\n\tb.Handle(\"/changelog\", cmdChangelog)\n\tb.Handle(\"/privacy\", cmdPrivacy)\n\tb.Handle(\"/help\", cmdStart)\n\tb.Handle(\"/about\", cmdAbout)\n\tb.Handle(\"/command_list\", cmdCommandList)\n\tb.Handle(\"/import\", cmdImport, checkState)\n\tb.Handle(\"/download\", cmdDownload, checkState)\n\tb.Handle(\"/create\", cmdCreate, checkState)\n\tb.Handle(\"/manage\", cmdManage, checkState)\n\tb.Handle(\"/search\", cmdSearch, checkState)\n\n\t// b.Handle(\"/register\", cmdRegister, checkState)\n\tb.Handle(\"/sitrep\", cmdSitRep, checkState)\n\tb.Handle(\"/getfid\", cmdGetFID, checkState)\n\n\tb.Handle(\"/start\", cmdStart, checkState)\n\n\tb.Handle(tele.OnText, handleMessage)\n\tb.Handle(tele.OnVideo, handleMessage)\n\tb.Handle(tele.OnAnimation, handleMessage)\n\tb.Handle(tele.OnSticker, handleMessage)\n\tb.Handle(tele.OnDocument, handleMessage)\n\tb.Handle(tele.OnPhoto, handleMessage)\n\tb.Handle(tele.OnCallback, handleMessage, autoRespond, sanitizeCallback)\n\n\tb.Start()\n}\n\n// Recover returns a middleware that recovers a panic happened in\n// the handler.\nfunc Recover(onError ...func(error)) tele.MiddlewareFunc {\n\treturn func(next tele.HandlerFunc) tele.HandlerFunc {\n\t\treturn func(c tele.Context) error {\n\t\t\tvar f func(error)\n\t\t\tif len(onError) > 0 {\n\t\t\t\tf = onError[0]\n\t\t\t} else {\n\t\t\t\tf = func(err error) {\n\t\t\t\t\tc.Bot().OnError(err, c)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tif err, ok := r.(error); ok {\n\t\t\t\t\t\tf(err)\n\t\t\t\t\t} else if s, ok := r.(string); ok {\n\t\t\t\t\t\tf(errors.New(s))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\treturn next(c)\n\t\t}\n\t}\n}\n\n// This one never say goodbye.\nfunc endSession(c tele.Context) {\n\tcleanUserDataAndDir(c.Sender().ID)\n}\n\n// This one will say goodbye.\nfunc terminateSession(c tele.Context) {\n\tcleanUserDataAndDir(c.Sender().ID)\n\tc.Send(\"Bye. /start\")\n}\n\nfunc endManageSession(c tele.Context) {\n\tud, exist := users.data[c.Sender().ID]\n\tif !exist {\n\t\treturn\n\t}\n\tif ud.stickerData.id == \"\" {\n\t\treturn\n\t}\n\tpath := filepath.Join(msbconf.WebappDataDir, ud.stickerData.id)\n\tos.RemoveAll(path)\n}\n\nfunc onError(err error, c tele.Context) {\n\tlog.Error(\"User encountered fatal error!\")\n\tlog.Errorln(\"Raw error:\", err)\n\tlog.Errorln(string(debug.Stack()))\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.Errorln(\"Recovered from onError!!\", r)\n\t\t}\n\t}()\n\tif c == nil {\n\t\treturn\n\t}\n\tsendFatalError(err, c)\n\tcleanUserDataAndDir(c.Sender().ID)\n}\n\nfunc initBot(conf ConfigTemplate) *tele.Bot {\n\tvar poller tele.Poller\n\turl := tele.DefaultApiURL\n\n\tpref := tele.Settings{\n\t\tURL:         url,\n\t\tToken:       msbconf.BotToken,\n\t\tPoller:      poller,\n\t\tSynchronous: false,\n\t\t// Genrally, issues are tackled inside each state, only fatal error should be returned to framework.\n\t\t// onError will terminate current session and log to terminal.\n\t\tOnError: onError,\n\t}\n\tlog.WithField(\"token\", msbconf.BotToken).Info(\"Attempting to initialize...\")\n\tb, err := tele.NewBot(pref)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treturn b\n}\n\nfunc initWorkspace(b *tele.Bot) {\n\tbotName = b.Me.Username\n\tif msbconf.DataDir != \"\" {\n\t\tdataDir = msbconf.DataDir\n\t} else {\n\t\tdataDir = botName + \"_data\"\n\t}\n\tusers = Users{data: make(map[int64]*UserData)}\n\terr := os.MkdirAll(dataDir, 0755)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tif msbconf.DbAddr != \"\" {\n\t\tdbName := botName + \"_db\"\n\t\terr = initDB(dbName)\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"Error initializing database!!\", err)\n\t\t}\n\t} else {\n\t\tlog.Warn(\"Database not enabled because --db_addr is not set.\")\n\t}\n}\n\n// This gocron is intended to do periodic cleanups.\nfunc initGoCron() {\n\t// Delay start.\n\ttime.Sleep(15 * time.Second)\n\tcronScheduler = gocron.NewScheduler(time.UTC)\n\tcronScheduler.Every(1).Days().Do(purgeOutdatedStorageData)\n\tif msbconf.DbAddr != \"\" {\n\t\tcronScheduler.Every(1).Weeks().Do(curateDatabase)\n\t}\n\tcronScheduler.StartBlocking()\n}\n\nfunc initLogrus() {\n\tlog.SetFormatter(&log.TextFormatter{\n\t\tForceColors:            true,\n\t\tDisableLevelTruncation: true,\n\t})\n\n\tlevel, err := log.ParseLevel(msbconf.LogLevel)\n\tif err != nil {\n\t\tprintln(\"Error parsing log_level! Defaulting to DEBUG level.\\n\")\n\t\tlog.SetLevel(log.DebugLevel)\n\t}\n\tlog.SetLevel(level)\n\n\tfmt.Printf(\"Log level is set to: %s\\n\", log.GetLevel())\n\tlog.Debug(\"Warning: Log level below DEBUG might print sensitive information, including passwords.\")\n}\n"
  },
  {
    "path": "core/message.go",
    "content": "package core\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\ttele \"gopkg.in/telebot.v3\"\n)\n\nfunc sendStartMessage(c tele.Context) error {\n\tmessage := `\nHi! I'm <a href=\"https://github.com/star-39/moe-sticker-bot\">moe_sticker_bot</a>! Please:\n• Send <b>LINE/Kakao sticker share link</b> to import or download.\n• Send <b>Telegram sticker/link/GIF</b> to download or export to WhatsApp.\n• Send <b>keywords</b> to search sticker sets.\n• Tap to <b>/create</b> or <b>/manage</b> sticker set and CustomEmoji.\n• Tap to check all available <b>/command_list</b>.\n\n你好! 歡迎使用<a href=\"https://github.com/star-39/moe-sticker-bot\">萌萌貼圖BOT</a>! 請：\n• 傳送<b>LINE/kakao貼圖包的分享連結</b>來匯入或下載.\n• 傳送<b>Telegram貼圖/連結/GIF</b>來下載.\n• 傳送<b>關鍵字</b>來搜尋貼圖包.\n• 傳送 <b>/create</b> 或 <b>/manage</b> 來創建或管理貼圖包和表情貼。\n• 傳送 <b>/command_list</b> 檢視所有可用指令.\n`\n\treturn c.Send(message, tele.ModeHTML, tele.NoPreview)\n}\n\nfunc sendCommandList(c tele.Context) error {\n\tmessage := `\n<b>/import</b>  <b>/search</b> LINE/Kakao stickers.<code>\n匯入或搜尋LINE/Kaka貼圖包.</code>\n<b>/download</b>  <b>/create</b>  <b>/manage</b> Telegram stickers.<code>\n下載、創建、管理Telegram貼圖包.</code>\n<b>/faq  /about  /changelog  /privacy</b><code>\n常見問題/關於/更新紀錄/私隱</code>\n`\n\n\treturn c.Send(message, tele.ModeHTML, tele.NoPreview)\n}\n\nfunc sendAboutMessage(c tele.Context) {\n\tc.Send(fmt.Sprintf(`\n<b>Please star for this project on Github if you like this bot!\n如果您喜歡這個bot, 歡迎在Github給本專案標Star喔!\nhttps://github.com/star-39/moe-sticker-bot</b>\nThank you @StickerGroup for feedbacks and advices!\n<code>\nThis free(as in freedom) software is released under the GPLv3 License.\nComes with ABSOLUTELY NO WARRANTY! All rights reserved.\n本BOT為免費提供的自由軟體, 您可以自由使用/分發, 惟無任何保用(warranty)!\t\n本軟體授權於通用公眾授權條款(GPL)v3, 保留所有權利.\n</code><b>\nPlease send /start to start using\n請傳送 /start 來開始\n</b><code>\nVersion:版本: %s\n</code>\n`, BOT_VERSION), tele.ModeHTML)\n}\n\nfunc sendFAQ(c tele.Context) {\n\tc.Send(fmt.Sprintf(`\n<b>Please hit Star for this project on Github if you like this bot!\n如果您喜歡這個bot, 歡迎在Github給本專案標Star喔!\nhttps://github.com/star-39/moe-sticker-bot</b>\n------------------------------------\n<b>Q: I got stucked! I can't quit from command!\n我卡住了! 我沒辦法從指令中退出!</b>\nA: Please send /quit to interrupt.\n請傳送 /quit 來中斷.\n\n<b>Q: Why ID has suffix: _by_%s ?\n為甚麼ID的末尾有: _by_%s ?</b>\nA: It's forced by Telegram, bot created sticker set must have its name in ID suffix.\n因為這個是Telegram的強制要求, 由bot創造的貼圖ID末尾必須有bot名字.\n\n<b>Q: Who owns the sticker sets the bot created?\n    BOT創造的貼圖包由誰所有?</b>\nA: It's you of course. You can manage them through /manage or Telegram's official @Stickers bot.\n    當然是您. 您可以通過 /manage 指令或者Telegram官方的 @Stickers 管理您的貼圖包.\n`, botName, botName), tele.ModeHTML)\n}\n\nfunc sendChangelog(c tele.Context) error {\n\treturn c.Send(`\nDetails: 詳細:\nhttps://github.com/star-39/moe-sticker-bot#changelog\nv2.5.0-RC1(20240528)\n* Support mix-typed sticker set.\n* You can add video to static set and vice versa.\n* Removed WhatsApp export temporarily .\n* 支援混合貼圖包。\n* 貼圖包可以同時存入靜態與動態貼圖。\n* 暫時移除WhatsApp匯出功能。\n\nv2.4.0-RC1-RC4(20240304)\n* Support Importing LINE Emoji into CustomEmoji.\n* Support creating CustomEmoji.\n* Support editing sticker emoji and title.\n* 支援LINE表情貼匯入。\n* 支援創建表情貼。\n* 支援修改貼圖Emoji/貼圖包標題。\n\nv2.3.13-v2.3.15(20230228)\n* Support region locked LINE Message sticker.\n* Support TGS(Animated) sticker export.\n* Fix TGS(Animated) sticker download. \n* 支援有區域鎖的line訊息貼圖。\n* 支援TGS貼圖匯出。\n* 修復TGS(動態)貼圖下載問題.\n\nv2.3.10(20230217)\n  * Fix kakao import fatal, support more animated kakao.\n  * 修復KAKAO匯入錯誤, 支援更多KAKAO動態貼圖.\n\nv2.3.x (20230216)\n  * Fix flood limit error during import.\n  * Fix animated kakao treated as static.\n  * Improved static kakao quality.\n  * Support changing sticker title.\n  * 修復匯入貼圖時flood limit錯誤。\n  * 修復動態KAKAO被當作靜態.\n  * 提升靜態KAKAO畫質.\n  * 支援修改貼圖包標題.\n  \nv2.2.0 (20230131)\n  * Support animated kakao sticker.\n  * 支援動態kakao貼圖。\n\nv2.1.0 (20230129)\n  * Support exporting sticker to WhatsApp.\n  * 支援匯出貼圖到WhatsApp\n\nv2.0.0 (20230105)\n  * Use new WebApp from /manage command to edit sticker set with ease.\n  * Send text or use /search command to search imported LINE/kakao sticker sets by all users.\n  * Auto import now happens on backgroud.\n  * Downloading sticker set is now lot faster.\n  * Fix many LINE import issues.\n  * 通過 /manage 指令使用新的WebApp輕鬆管理貼圖包.\n  * 直接傳送文字或使用 /search 指令來搜尋所有用戶匯入的LINE/KAKAO貼圖包.\n  * 自動匯入現在會在背景處理.\n  * 下載整個貼圖包的速度現在會快許多.\n  * 修復了許多LINE貼圖匯入的問題.\n\t`, tele.NoPreview)\n}\n\nfunc sendPrivacy(c tele.Context) error {\n\treturn c.Send(`\n<b>Privacy Notice:</b>\nNone of your usage or behaviour will be stored or analyzed.\nNone of your user identifier or information will be collected or stored if you did not use /import or /create command and succeded.\n\nIf you used /create or /import feature and upon success,\nyour user identifier will be associated to the sticker set you create and stored to database on the bot server,\nwhich will only be used to tell which sticker set is owned by you only when you use /manage command.\nNo one else could see or use the stored user identifier.\n\nAll the data being stored is encrypted.\nThis bot will never share any of the stored data to anyone or to anywhere else.\nThe bot server is physically located at Osaka,Japan. Local laws might apply.\nThis bot is free and open source software, you can see https://github.com/star-39/moe-sticker-bot/blob/master/core/database.go\nto investigate how the bot store and process the stored data.\n\n<b>私隱聲明:</b>\n本bot不會存儲或分析您的使用情況或行為。\n本bot不會採集或儲存任何用戶資訊，除非您使用了 /import 或 /create 指令且成功完成。\n\n如果您使用了 /import 或 /create 指令並且成功完成，\n您的用戶識別子(user_id)會與您創建的貼圖包關聯並存檔入bot伺服器的資料庫中。\n此識別子只會用來讓您本人通過 /manage 指令查詢您所擁有的貼圖包，不作其他任何用途。\n任何其他用戶無法看見或使用此識別子。\n\n本bot儲存的所有資訊均經過加密。\n本bot不會分享任何儲存的資訊給任何人或實體或到任何地方。\n本bot伺服器的物理位置位於日本大阪。 當地法律可能適用。\n本bot為自由開放原始碼軟體，請參閱 https://github.com/star-39/moe-sticker-bot/blob/master/core/database.go\n來了解bot如何儲存和處理儲存的資訊。\n`, tele.ModeHTML, tele.NoPreview)\n}\n\nfunc sendAskEmoji(c tele.Context) error {\n\tselector := &tele.ReplyMarkup{}\n\tbtnManu := selector.Data(\"Assign separately/分別設定\", \"manual\")\n\tbtnRand := selector.Data(`Batch assign as/一併設定為 \"⭐\"`, \"random\")\n\tselector.Inline(selector.Row(btnManu), selector.Row(btnRand))\n\n\treturn c.Send(`\nTelegram requires emoji to and keywords for each sticker:\n• Press \"Assign separately\" to assign emoji and keywords one by one.\n• Send an emoji to do batch assign.\nTelegram要求為每張貼圖分別設定emoji和關鍵字:\n• 按下\"分別設定\"來為每個貼圖分別設定相應的emoji和關鍵字.\n• 傳送一個emoji來為全部貼圖設定成一樣的.\n`, selector)\n}\n\nfunc sendConfirmExportToWA(c tele.Context, sn string, hex string) error {\n\tselector := &tele.ReplyMarkup{}\n\tbaseUrl, _ := url.JoinPath(msbconf.WebappUrl, \"export\")\n\twebAppUrl := fmt.Sprintf(\"%s?sn=%s&hex=%s\", baseUrl, sn, hex)\n\tlog.Debugln(\"webapp export link is:\", webAppUrl)\n\twebapp := tele.WebApp{URL: webAppUrl}\n\tbtnExport := selector.WebApp(\"Continue export/繼續匯出 →\", &webapp)\n\tselector.Inline(selector.Row(btnExport))\n\n\treturn c.Reply(`\nExporting to WhatsApp requires <a href=\"https://github.com/star-39/msb_app\">Msb App</a> due to their restrictions, then press \"Continue export\".\n匯出到WhatsApp需要手機上安裝<a href=\"https://github.com/star-39/msb_app\">Msb App</a>, 然後按下\"繼續匯出\".\n\nDownload:下載:\n<b>iPhone:</b> AppStore(N/A.暫無), <a href=\"https://github.com/star-39/msb_app/releases/latest/download/msb_app.ipa\">IPA</a>\n<b>Android:</b> GooglePlay(N/A.暫無), <a href=\"https://github.com/star-39/msb_app/releases/latest/download/msb_app.apk\">APK</a>\n`, tele.ModeHTML, tele.NoPreview, selector)\n}\n\nfunc genSDnMnEInline(canManage bool, isTGS bool, sn string) *tele.ReplyMarkup {\n\tselector := &tele.ReplyMarkup{}\n\tbtnSingle := selector.Data(\"Download this sticker/下載這張貼圖\", CB_DN_SINGLE)\n\tbtnAll := selector.Data(\"Download sticker set/下載整個貼圖包\", CB_DN_WHOLE)\n\tbtnMan := selector.Data(\"Manage sticker set/管理這個貼圖包\", CB_MANAGE)\n\t// btnExport := selector.Data(\"Export to WhatsApp/匯出到WhatsApp\", CB_EXPORT_WA)\n\tif canManage {\n\t\tselector.Inline(selector.Row(btnSingle), selector.Row(btnAll), selector.Row(btnMan))\n\t} else {\n\t\tif isTGS {\n\t\t\t//If is TGS, do not support export to WA.\n\t\t\tselector.Inline(selector.Row(btnSingle), selector.Row(btnAll))\n\t\t} else {\n\t\t\tselector.Inline(selector.Row(btnSingle), selector.Row(btnAll))\n\t\t}\n\t}\n\treturn selector\n}\n\nfunc sendAskSDownloadChoice(c tele.Context, s *tele.Sticker) error {\n\tselector := genSDnMnEInline(false, s.Animated, s.SetName)\n\treturn c.Reply(`\nYou can download this sticker or the whole sticker set, please select below.\n您可以下載這個貼圖或者其所屬的整個貼圖包, 請選擇:\n`, selector)\n}\n\nfunc sendAskSChoice(c tele.Context, sn string) error {\n\tselector := genSDnMnEInline(true, false, sn)\n\treturn c.Reply(`\nYou own this sticker set. You can download or manage this sticker set, please select below.\n您擁有這個貼圖包. 您可以下載或者管理這個貼圖包, 請選擇:\n`, selector)\n}\n\nfunc sendAskTGLinkChoice(c tele.Context) error {\n\tselector := &tele.ReplyMarkup{}\n\tbtnManu := selector.Data(\"Download sticker set/下載整個貼圖包\", CB_DN_WHOLE)\n\tbtnMan := selector.Data(\"Manage sticker set/管理這個貼圖包\", CB_MANAGE)\n\tselector.Inline(selector.Row(btnManu), selector.Row(btnMan))\n\treturn c.Reply(`\nYou own this sticker set. You can download or manage this sticker set, please select below.\n您擁有這個貼圖包. 您可以下載或者管理這個貼圖包, 請選擇:\n`, selector)\n}\n\nfunc sendAskWantSDown(c tele.Context) error {\n\tselector := &tele.ReplyMarkup{}\n\tbtn1 := selector.Data(\"Yes\", CB_DN_WHOLE)\n\tbtnNo := selector.Data(\"No\", CB_BYE)\n\tselector.Inline(selector.Row(btn1), selector.Row(btnNo))\n\treturn c.Reply(`\nYou can download this sticker set. Press Yes to continue.\n您可以下載這個貼圖包, 按下Yes來繼續.\n`, selector)\n}\n\nfunc sendAskWantImportOrDownload(c tele.Context, avalAsEmoji bool) error {\n\tmsg := \"\"\n\tselector := &tele.ReplyMarkup{}\n\tbtnImportSticker := selector.Data(\"Import as sticker set/作為普通貼圖包匯入\", CB_OK_IMPORT)\n\tbtnImportEmoji := selector.Data(\"Import as CustomEmoji/作為表情貼匯入\", CB_OK_IMPORT_EMOJI)\n\tbtnDownload := selector.Data(\"Download/下載\", CB_OK_DN)\n\tif avalAsEmoji {\n\t\tselector.Inline(selector.Row(btnImportSticker), selector.Row(btnImportEmoji), selector.Row(btnDownload))\n\t\tmsg = `\nYou can import this sticker set to Telegram or download it.\nImport as Custom Emoji is also available, however you will need Telegram Premium to send them.\n您可以下載或匯入這個貼圖包到Telegram.\n也可以作為表情貼匯入，但是傳送需要Telegram會員。`\n\t} else {\n\t\tselector.Inline(selector.Row(btnImportSticker), selector.Row(btnDownload))\n\t\tmsg = `\nYou can import this sticker set to Telegram or download it.\n您可以下載或匯入這個貼圖包到Telegram.`\n\t}\n\n\treturn c.Reply(msg, selector)\n}\n\nfunc sendAskWhatToDownload(c tele.Context) error {\n\treturn c.Send(\"Please send a sticker that you want to download, or its share link(can be either Telegram or LINE ones)\\n\" +\n\t\t\"請傳送想要下載的貼圖, 或者是貼圖包的分享連結(可以是Telegram或LINE連結).\")\n}\n\nfunc sendAskTitle_Import(c tele.Context) error {\n\tld := users.data[c.Sender().ID].lineData\n\tld.TitleWg.Wait()\n\tlog.Debug(\"titles are::\")\n\tlog.Debugln(ld.I18nTitles)\n\tselector := &tele.ReplyMarkup{}\n\n\tvar titleButtons []tele.Row\n\tvar titleText string\n\tfor i, t := range ld.I18nTitles {\n\t\tif t == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\ttitle := escapeTagMark(t) + \" @\" + botName\n\t\tbtn := selector.Data(title, strconv.Itoa(i))\n\t\trow := selector.Row(btn)\n\t\ttitleButtons = append(titleButtons, row)\n\t\ttitleText = titleText + \"\\n<code>\" + title + \"</code>\"\n\t}\n\n\tif len(titleButtons) == 0 {\n\t\tbtnDefault := selector.Data(escapeTagMark(ld.Title)+\" @\"+botName, CB_DEFAULT_TITLE)\n\t\ttitleButtons = []tele.Row{selector.Row(btnDefault)}\n\t}\n\tselector.Inline(titleButtons...)\n\n\treturn c.Send(\"Please send a title for this sticker set. You can also select an original title below:\\n\"+\n\t\t\"請傳送貼圖包的標題.您也可以按下面的按鈕自動填上合適的原版標題:\\n\"+\n\t\ttitleText, selector, tele.ModeHTML)\n}\n\nfunc sendAskTitle(c tele.Context) error {\n\treturn c.Send(\"Please send a title for this sticker set.\\n\" +\n\t\t\"請傳送貼圖包的標題.\")\n}\n\nfunc sendAskID(c tele.Context) error {\n\tselector := &tele.ReplyMarkup{}\n\tbtnAuto := selector.Data(\"Auto Generate/自動生成\", \"auto\")\n\tselector.Inline(selector.Row(btnAuto))\n\treturn c.Send(`\nPlease send an ID for sticker set, used in share link.\nCan contain alphanum and underscore only.\n請設定貼圖包的ID, 用於分享連結.\n只可以含有英文,數字,下劃線.\nFor example: 例如:\n<code>My_favSticker21</code>\n\nID is usually not important, you can press Auto Generate.\nID通常不重要, 您可以按下\"自動生成\".`, selector, tele.ModeHTML)\n}\n\nfunc sendAskImportLink(c tele.Context) error {\n\treturn c.Send(`\nPlease send LINE/kakao store link of the sticker set. You can obtain this link from App by going to sticker store and tapping Share->Copy Link.\n請傳送貼圖包的LINE/kakao Store連結. 您可以在App裡的貼圖商店按右上角的分享->複製連結來取得連結.\nFor example: 例如:\n<code>https://store.line.me/stickershop/product/7673/ja</code>\n<code>https://e.kakao.com/t/pretty-all-friends</code>\n<code>https://emoticon.kakao.com/items/lV6K2fWmU7CpXlHcP9-ysQJx9rg=?referer=share_link</code>\n`, tele.ModeHTML)\n}\n\nfunc sendNotifySExist(c tele.Context, lineID string) bool {\n\tlines := queryLineS(lineID)\n\tif len(lines) == 0 {\n\t\treturn false\n\t}\n\tmessage := \"This sticker set exists in our database, you can continue import or just use them if you want.\\n\" +\n\t\t\"此套貼圖包已經存在於資料庫中, 您可以繼續匯入, 或者使用下列現成的貼圖包\\n\\n\"\n\n\tvar entries []string\n\tfor _, l := range lines {\n\t\tif l.Ae {\n\t\t\tentries = append(entries, fmt.Sprintf(`<a href=\"%s\">%s</a>`, \"https://t.me/addstickers/\"+l.Tg_id, l.Tg_title))\n\t\t} else {\n\t\t\t// append to top\n\t\t\tentries = append([]string{fmt.Sprintf(`★ <a href=\"%s\">%s</a>`, \"https://t.me/addstickers/\"+l.Tg_id, l.Tg_title)}, entries...)\n\t\t}\n\t}\n\tif len(entries) > 5 {\n\t\tentries = entries[:5]\n\t}\n\tmessage += strings.Join(entries, \"\\n\")\n\tc.Send(message, tele.ModeHTML)\n\treturn true\n}\n\nfunc sendSearchResult(entriesWant int, lines []LineStickerQ, c tele.Context) error {\n\tvar entries []string\n\tmessage := \"Search Results: 搜尋結果：\\n\"\n\n\tfor _, l := range lines {\n\t\tl.Tg_title = strings.TrimSuffix(l.Tg_title, \" @\"+botName)\n\t\tif l.Ae {\n\t\t\tentries = append(entries, fmt.Sprintf(`<a href=\"%s\">%s</a>`, \"https://t.me/addstickers/\"+l.Tg_id, l.Tg_title))\n\t\t} else {\n\t\t\t// append to top\n\t\t\tentries = append([]string{fmt.Sprintf(`★ <a href=\"%s\">%s</a>`, \"https://t.me/addstickers/\"+l.Tg_id, l.Tg_title)}, entries...)\n\t\t}\n\t}\n\n\tif entriesWant == -1 && len(entries) > 120 {\n\t\tc.Send(\"Too many results, please narrow your keyword, truncated to 120 entries.\\n\" +\n\t\t\t\"搜尋結果過多，已縮減到120個，請使用更準確的搜尋關鍵字。\")\n\t\tentries = entries[:120]\n\t}\n\tif entriesWant != -1 && len(entries) > entriesWant {\n\t\tentries = entries[:entriesWant]\n\t}\n\tif len(entries) > 30 {\n\t\teChunks := chunkSlice(entries, 30)\n\t\tfor _, eChunk := range eChunks {\n\t\t\tmsgToSend := message + strings.Join(eChunk, \"\\n\")\n\t\t\tc.Send(msgToSend, tele.ModeHTML)\n\t\t}\n\t} else {\n\t\tmessage += strings.Join(entries, \"\\n\")\n\t\tc.Send(message, tele.ModeHTML)\n\t}\n\n\treturn nil\n}\n\nfunc sendAskStickerFile(c tele.Context) error {\n\treturn c.Send(\"Please send images/photos/stickers(less than 120 in total),\\n\" +\n\t\t\"or send an archive containing image files,\\n\" +\n\t\t\"wait until upload complete, then tap 'Done adding'.\\n\\n\" +\n\t\t\"請傳送任意格式的圖片/影片/貼圖(少於120張)\\n\" +\n\t\t\"或者傳送內有貼圖檔案的歸檔,\\n\" +\n\t\t\"等候所有檔案上載完成, 然後按下「停止增添」\\n\")\n}\n\nfunc sendInStateWarning(c tele.Context) error {\n\tcommand := users.data[c.Sender().ID].command\n\tstate := users.data[c.Sender().ID].state\n\n\treturn c.Send(fmt.Sprintf(`\nPlease send content according to instructions.\n請按照bot提示傳送相應內容.\nCurrent command: %s\nCurrent state: %s\nYou can also send /quit to terminate session.\n您也可以傳送 /quit 來中斷對話.\n`, command, state))\n}\n\nfunc sendNoSessionWarning(c tele.Context) error {\n\treturn c.Send(\"Please use /start or send LINE/kakao/Telegram links or stickers.\\n請使用 /start 或者傳送LINE/kakao/Telegram連結或貼圖.\")\n}\n\nfunc sendAskSTypeToCreate(c tele.Context) error {\n\tselector := &tele.ReplyMarkup{}\n\tbtnRegular := selector.Data(\"Regular sticker set/普通貼圖包\", CB_REGULAR_STICKER)\n\tbtnCustomEmoji := selector.Data(\"Custom Emoji/表情貼\", CB_CUSTOM_EMOJI)\n\n\tselector.Inline(selector.Row(btnRegular), selector.Row(btnCustomEmoji))\n\treturn c.Send(\"What kind of sticker set you want to create?\\nNote that custom emoji can only be sent by Telegram Premium member.\"+\n\t\t\"您想要創建何種類型的貼圖包?\\n請注意只有Telgram會員可以傳送表情貼。\", selector)\n}\n\nfunc sendAskEmojiAssign(c tele.Context) error {\n\tsd := users.data[c.Sender().ID].stickerData\n\tsf := sd.stickers[sd.pos]\n\tsf.wg.Wait()\n\tcaption := fmt.Sprintf(`\nSend emoji(s) representing this sticker.\n請傳送代表這個貼圖的emoji(可以多個).\n\n%d of %d\n`, sd.pos+1, sd.lAmount)\n\n\tif sf.fileID != \"\" {\n\t\tmsg, _ := c.Bot().Send(c.Sender(), &tele.Sticker{\n\t\t\tFile: tele.File{FileID: sf.fileID},\n\t\t})\n\t\t_, err := c.Bot().Reply(msg, caption)\n\t\treturn err\n\t}\n\n\terr := c.Send(&tele.Video{\n\t\tFile:    tele.FromDisk(sf.oPath),\n\t\tCaption: caption,\n\t})\n\tif err != nil {\n\t\terr2 := c.Send(&tele.Video{\n\t\t\tFile:    tele.FromDisk(sd.stickers[sd.pos].oPath),\n\t\t\tCaption: caption,\n\t\t})\n\t\tif err2 != nil {\n\t\t\terr3 := c.Send(&tele.Document{\n\t\t\t\tFile:     tele.FromDisk(sd.stickers[sd.pos].oPath),\n\t\t\t\tFileName: filepath.Base(sd.stickers[sd.pos].oPath),\n\t\t\t\tCaption:  caption,\n\t\t\t})\n\t\t\tif err3 != nil {\n\t\t\t\terr4 := c.Send(&tele.Sticker{File: tele.File{FileID: sd.stickers[sd.pos].oPath}})\n\t\t\t\tif err4 != nil {\n\t\t\t\t\treturn err4\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc sendFatalError(err error, c tele.Context) {\n\tif c == nil {\n\t\treturn\n\t}\n\tvar errMsg string\n\tif err != nil {\n\t\terrMsg = err.Error()\n\t\terrMsg = strings.ReplaceAll(errMsg, msbconf.BotToken, \"***\")\n\t\tif strings.Contains(errMsg, \"500\") {\n\t\t\terrMsg += \"\\nThis is an internal error of Telegram server, we could do nothing but wait for its recover. Please try again later.\\n\" +\n\t\t\t\t\"此錯誤為Telegram伺服器之內部錯誤, 無法由bot解決, 只能等候官方修復. 建議您稍後再嘗試一次.\\n\"\n\t\t}\n\t}\n\n\tc.Send(\"<b>Fatal error encounterd. Please try again. /start\\n\"+\n\t\t\"發生嚴重錯誤. 請您從頭再試一次. /start </b>\\n\\n\"+\n\t\t\"You can report this error to https://github.com/star-39/moe-sticker-bot/issues\\n\\n\"+\n\t\t\"<code>\"+errMsg+\"</code>\", tele.ModeHTML, tele.NoPreview)\n}\n\nfunc sendExecEmojiAssignFinished(c tele.Context) error {\n\tud := users.data[c.Sender().ID]\n\tmsg := fmt.Sprintf(`\nLINE Cat: <code>%s</code>\nLINE ID: <code>%s</code>\nTG ID: <code>%s</code>\nTG Title: <a href=\"%s\">%s</a>\n\nSuccess. 成功完成. /start\n\t`, ud.lineData.Category,\n\t\tud.lineData.Id,\n\t\tud.stickerData.id,\n\t\t\"https://t.me/addstickers/\"+ud.stickerData.id,\n\t\tescapeTagMark(ud.stickerData.title),\n\t)\n\treturn c.Send(msg, tele.ModeHTML)\n}\n\n// Return:\n// string: Text of the message.\n// *tele.Message: The pointer of the message.\n// error: error\nfunc sendProcessStarted(ud *UserData, c tele.Context, optMsg string) (string, *tele.Message, error) {\n\tmessage := fmt.Sprintf(`\nPreparing stickers, please wait...\n正在準備貼圖, 請稍後...\n\nLINE Cat: <code>%s</code>\nLINE ID: <code>%s</code>\nTG ID: <code>%s</code>\nTG TYPE: <code>%s</code>\nTG Title: <a href=\"%s\">%s</a>\n\n<b>Progress / 進展</b>\n<code>%s</code>\n`, ud.lineData.Category,\n\t\tud.lineData.Id,\n\t\tud.stickerData.id,\n\t\tud.stickerData.stickerSetType,\n\t\t\"https://t.me/addstickers/\"+ud.stickerData.id,\n\t\tescapeTagMark(ud.stickerData.title),\n\t\toptMsg)\n\tud.progress = message\n\n\tteleMsg, err := c.Bot().Send(c.Recipient(), message, tele.ModeHTML)\n\tud.progressMsg = teleMsg\n\treturn message, teleMsg, err\n}\n\n// if progressText is empty, a progress bar will be generated based on cur and total.\nfunc editProgressMsg(cur int, total int, progressText string, originalText string, teleMsg *tele.Message, c tele.Context) error {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.Errorln(\"editProgressMsg encountered panic! ignoring...\", string(debug.Stack()))\n\t\t}\n\t}()\n\n\theader := originalText[:strings.LastIndex(originalText, \"<code>\")]\n\tprog := \"\"\n\n\tif progressText != \"\" {\n\t\tprog = progressText\n\t\tgoto SEND\n\t}\n\tcur = cur + 1\n\tif cur == 1 {\n\t\tprog = fmt.Sprintf(\"<code>[=>                  ]\\n       %d of %d</code>\", cur, total)\n\t} else if cur == int(float64(0.25)*float64(total)) {\n\t\tprog = fmt.Sprintf(\"<code>[====>               ]\\n       %d of %d</code>\", cur, total)\n\t} else if cur == int(float64(0.5)*float64(total)) {\n\t\tprog = fmt.Sprintf(\"<code>[=========>          ]\\n       %d of %d</code>\", cur, total)\n\t} else if cur == int(float64(0.75)*float64(total)) {\n\t\tprog = fmt.Sprintf(\"<code>[==============>     ]\\n       %d of %d</code>\", cur, total)\n\t} else if cur == total {\n\t\tprog = fmt.Sprintf(\"<code>[====================]\\n       %d of %d</code>\", cur, total)\n\t} else {\n\t\treturn nil\n\t}\nSEND:\n\tmessageText := header + prog\n\tc.Bot().Edit(teleMsg, messageText, tele.ModeHTML)\n\treturn nil\n}\n\nfunc sendAskSToManage(c tele.Context) error {\n\treturn c.Send(\"Send a sticker from the sticker set that want to edit,\\n\" +\n\t\t\"or send its share link.\\n\\n\" +\n\t\t\"您想要修改哪個貼圖包? 請傳送那個貼圖包內任意一張貼圖,\\n\" +\n\t\t\"或者是它的分享連結.\")\n}\n\nfunc sendUserOwnedS(c tele.Context) error {\n\tusq := queryUserS(c.Sender().ID)\n\tif usq == nil {\n\t\treturn errors.New(\"no sticker owned\")\n\t}\n\n\tvar entries []string\n\n\tfor _, us := range usq {\n\t\tdate := time.Unix(us.timestamp, 0).Format(\"2006-01-02 15:04\")\n\t\ttitle := strings.TrimSuffix(us.tg_title, \" @\"+botName)\n\t\t//workaround for empty title.\n\t\tif title == \"\" || title == \" \" {\n\t\t\ttitle = \"_\"\n\t\t}\n\t\tentry := fmt.Sprintf(`<a href=\"https://t.me/addstickers/%s\">%s</a>`, us.tg_id, title)\n\t\tentry += \" | \" + date\n\t\tentries = append(entries, entry)\n\t}\n\n\tif len(entries) > 30 {\n\t\teChunks := chunkSlice(entries, 30)\n\t\tfor _, eChunk := range eChunks {\n\t\t\tmessage := \"You own following stickers:\\n\"\n\t\t\tmessage += strings.Join(eChunk, \"\\n\")\n\t\t\tc.Send(message, tele.ModeHTML)\n\t\t}\n\t} else {\n\t\tmessage := \"You own following stickers:\\n\"\n\t\tmessage += strings.Join(entries, \"\\n\")\n\t\tc.Send(message, tele.ModeHTML)\n\t}\n\treturn nil\n}\n\nfunc sendAskEditChoice(c tele.Context) error {\n\tud := users.data[c.Sender().ID]\n\tselector := &tele.ReplyMarkup{}\n\tbtnAdd := selector.Data(\"Add sticker/增添貼圖\", CB_ADD_STICKER)\n\tbtnDel := selector.Data(\"Delete sticker/刪除貼圖\", CB_DELETE_STICKER)\n\tbtnDelset := selector.Data(\"Delete sticker set/刪除貼圖包\", CB_DELETE_STICKER_SET)\n\tbtnChangeTitle := selector.Data(\"Change title/修改標題\", CB_CHANGE_TITLE)\n\tbtnExit := selector.Data(\"Exit/退出\", \"bye\")\n\n\tif msbconf.WebappUrl != \"\" {\n\t\tbaseUrl, _ := url.JoinPath(msbconf.WebappUrl, \"edit\")\n\t\turl := fmt.Sprintf(\"%s?ss=%s&dt=%d\",\n\t\t\tbaseUrl,\n\t\t\tud.stickerData.id,\n\t\t\ttime.Now().Unix())\n\t\tlog.Debugln(\"WebApp URL is : \", url)\n\t\twebApp := &tele.WebApp{\n\t\t\tURL: url,\n\t\t}\n\t\tbtnEdit := selector.WebApp(\"Change order or emoji/修改順序或Emoji\", webApp)\n\t\tselector.Inline(\n\t\t\tselector.Row(btnAdd), selector.Row(btnDel), selector.Row(btnDelset), selector.Row(btnEdit), selector.Row(btnChangeTitle), selector.Row(btnExit))\n\t} else {\n\t\tselector.Inline(\n\t\t\tselector.Row(btnAdd), selector.Row(btnDel), selector.Row(btnDelset), selector.Row(btnChangeTitle), selector.Row(btnExit))\n\t}\n\n\treturn c.Send(fmt.Sprintf(`\nID: <code>%s</code>\nTitle: <a href=\"https://t.me/addstickers/%s\">%s</a>\n\nWhat do you want to edit? Please select below:\n您想要修改貼圖包的甚麼內容? 請選擇:`,\n\t\tusers.data[c.Sender().ID].stickerData.id,\n\t\tud.stickerData.id,\n\t\tud.stickerData.title),\n\t\tselector, tele.ModeHTML)\n}\n\nfunc sendAskSDel(c tele.Context) error {\n\treturn c.Send(\"Which sticker do you want to delete? Please send it.\\n\" +\n\t\t\"您想要刪除哪一個貼圖? 請傳送那個貼圖\")\n}\n\nfunc sendConfirmDelset(c tele.Context) error {\n\tselector := &tele.ReplyMarkup{}\n\tbtnYes := selector.Data(\"Yes\", CB_YES)\n\tbtnNo := selector.Data(\"No\", CB_NO)\n\tselector.Inline(selector.Row(btnYes), selector.Row(btnNo))\n\n\treturn c.Send(\"You are attempting to delete the whole sticker set, please confirm.\\n\"+\n\t\t\"您將要刪除整個貼圖包, 請確認.\", selector)\n}\n\nfunc sendSFromSS(c tele.Context, ssid string, reply *tele.Message) error {\n\tss, _ := c.Bot().StickerSet(ssid)\n\tif reply != nil {\n\t\tc.Bot().Reply(reply, &ss.Stickers[0])\n\t} else {\n\t\tc.Send(&ss.Stickers[0])\n\t}\n\treturn nil\n}\n\nfunc sendFLWarning(c tele.Context) error {\n\treturn c.Send(`\nIt might take longer to process this sticker set (2-8 minutes)... \nThis warning indicates that you might triggered Telegram's flood limit, and bot is trying to re-submit.\nDue to this mechanism, resulted sticker set might contains duplicate or missing sticker, please check manually after done.\n\n此貼圖包可能需要更長時間處理(2-8分鐘)...\n看到這一條警告表示Telegram可能限制了您創建貼圖包的頻度, 且bot正在自動嘗試重新製作, 因此得出的貼圖包可能會重複或缺失貼圖, 請在完成製作後再檢查一下.\n`)\n}\n\nfunc sendTooManyFloodLimits(c tele.Context) error {\n\treturn c.Send(\"Sorry, it seems that you have triggered Telegram's flood limit for too many times, it's recommended try again after a while.\\n\" +\n\t\t\"抱歉, 您似乎觸發了Telegram的貼圖製作次數限制, 建議您過一段時間後再試一次.\")\n}\n\nfunc sendNoCbWarn(c tele.Context) error {\n\treturn c.Send(\"Please press a button! /quit\\n請選擇按鈕!\")\n}\n\nfunc sendBadIDWarn(c tele.Context) error {\n\treturn c.Send(`\nBad ID. try again or press Auto Generate. /quit\nCan contain alphanum and underscore only, must begin with alphabet, must not contain consecutive underscores.\n只可以含有英文,數字,下劃線, 必須由英文字母開頭，不可以有連續下劃線.\nID錯誤, 請試多一次或按下'自動生成'按鈕. /quit`)\n}\n\nfunc sendIDOccupiedWarn(c tele.Context) error {\n\treturn c.Send(\"ID already occupied! try another one. ID已經被占用, 請試試另一個.\")\n}\n\nfunc sendBadImportLinkWarn(c tele.Context) error {\n\treturn c.Send(\"Invalid import link, make sure its a LINE Store link or kakao store link. Try again or /quit\\n\"+\n\t\t\"無效的連結, 請檢視是否為LINE貼圖商店的連結, 或是kakao emoticon的連結.\\n\\n\"+\n\t\t\"For example: 例如:\\n\"+\n\t\t\"<code>https://store.line.me/stickershop/product/7673/ja</code>\\n\"+\n\t\t\"<code>https://e.kakao.com/t/pretty-all-friends</code>\", tele.ModeHTML)\n}\n\nfunc sendNoSToManage(c tele.Context) error {\n\treturn c.Send(\"Sorry, you have not created any sticker set yet. You can use /import or /create .\\n\" +\n\t\t\"抱歉, 您還未創建過貼圖包, 您可以使用 /create 或 /import .\")\n}\n\nfunc sendPromptStopAdding(c tele.Context) error {\n\tselector := &tele.ReplyMarkup{}\n\tbtnDone := selector.Data(\"Done adding/停止添加\", CB_DONE_ADDING)\n\tselector.Inline(selector.Row(btnDone))\n\treturn c.Send(\"Continue sending files or press button below to stop adding.\\n\"+\n\t\t\"請繼續傳送檔案. 或者按下方按鈕來停止增添.\", selector)\n}\n\nfunc replySFileOK(c tele.Context, count int) error {\n\tselector := &tele.ReplyMarkup{}\n\tbtnDone := selector.Data(\"Done adding/停止添加\", CB_DONE_ADDING)\n\tselector.Inline(selector.Row(btnDone))\n\treturn c.Reply(\n\t\tfmt.Sprintf(\"File OK. Got %d stickers. Continue sending files or press button below to stop adding.\\n\"+\n\t\t\t\"檔案OK. 已收到%d份貼圖. 請繼續傳送檔案. 或者按下方按鈕來停止增添.\", count, count), selector)\n}\n\nfunc sendSEditOK(c tele.Context) error {\n\treturn c.Send(\n\t\t\"Successfully edited sticker set. /start\\n\" +\n\t\t\t\"成功修改貼圖包. /start\")\n}\n\nfunc sendStickerSetFullWarning(c tele.Context) error {\n\treturn c.Send(\n\t\t\"Warning: Your sticker set is already full. You cannot add new sticker.\\n\" +\n\t\t\t\"提示：當前貼圖包已滿，您將不能增添貼圖。\")\n}\n\n// func sendEditingEmoji(c tele.Context) error {\n// \treturn c.Send(\"Commiting changes...\\n正在套用變更，請稍候...\")\n// }\n\nfunc sendAskSearchKeyword(c tele.Context) error {\n\treturn c.Send(\"Please send a word that you want to search\\n請傳送想要搜尋的內容\")\n}\n\nfunc sendSearchNoResult(c tele.Context) error {\n\tmessage := \"Sorry, no result.\\n抱歉, 搜尋沒有結果.\"\n\tif c.Chat().Type == tele.ChatPrivate {\n\t\tmessage += \"\\nTry again or /quit\\n請試試別的關鍵字或 /quit\"\n\t}\n\treturn c.Send(message)\n}\n\nfunc sendNotifyNoSessionSearch(c tele.Context) error {\n\treturn c.Send(\"Here are some search results, use /search to dig deeper or /start to see available commands.\\n\" +\n\t\t\"這些是貼圖包搜尋結果，使用 /search 詳細搜尋或 /start 來看看可用的指令。\")\n}\n\nfunc sendUnsupportedCommandForGroup(c tele.Context) error {\n\treturn c.Send(\"This command is not supported in group chat, please chat with bot directly.\\n\" +\n\t\t\"此指令無法於群組內使用, 請與bot直接私訊.\")\n}\n\nfunc sendBadSearchKeyword(c tele.Context) error {\n\treturn c.Send(fmt.Sprintf(`\nPlease specify keyword\n請指定搜尋關鍵字.\n\nExample: 例如:\n/search@%s keyword1 keyword2 ...\n/search@%s nekomimi mia\n`, botName, botName))\n}\n\nfunc sendPreferKakaoShareLinkWarning(c tele.Context) error {\n\tmsg := `\nThe link you sent is a Kakao store link.\nUse a share link for improved image quality and animated sticker support,\nyou can obtain it from KakaoTalk app by tapping share->copy link in sticker store.\n\n您傳送的是Kakao商店的連結.\n使用分享連結才能支援動態貼圖, 靜態貼圖的畫質也更高。\n您可以在KakaoTalk App內的貼圖商店點選 分享->複製連結 來取得分享連結。\n\neg:例如: <code>https://emoticon.kakao.com/items/lV6K2fWmU7CpXlHcP9-ysQJx9rg=?referer=share_link</code>\n`\n\terr := c.Reply(&tele.Photo{\n\t\tFile:    tele.File{FileID: FID_KAKAO_SHARE_LINK},\n\t\tCaption: msg,\n\t}, tele.ModeHTML)\n\tif err != nil {\n\t\tc.Reply(msg, tele.ModeHTML)\n\t}\n\treturn nil\n}\n\nfunc sendUseCommandToImport(c tele.Context) error {\n\treturn c.Send(\"Please use /create to create sticker set using your own photos and videos. /start\\n\" +\n\t\t\"請使用 /create 指令來使用自己的圖片和影片和創建貼圖包. /start\")\n}\n\nfunc sendOneStickerFailedToAdd(c tele.Context, pos int, err error) error {\n\treturn c.Reply(fmt.Sprintf(`\nFailed to add one sticker.\n一張貼圖添加失敗.\nIndex: %d\nError: %s\n`, pos, err.Error()))\n}\n\nfunc sendBadSNWarn(c tele.Context) error {\n\treturn c.Reply(\"Wrong sticker or link!\\n貼圖或連結錯誤!\")\n}\n\nfunc sendSSTitleChanged(c tele.Context) error {\n\tmsg := `\nSuccessfully changed title.\n新標題設定完成`\n\treturn c.Reply(msg, tele.ModeHTML)\n}\nfunc sendSSTitleFailedToChanged(c tele.Context) error {\n\tmsg := `\nFailed to change title, please try again.\n新標題設定失敗，請再試一次。`\n\treturn c.Reply(msg, tele.ModeHTML)\n}\n\n// func sendInvalidEmojiWarn(c tele.Context) error {\n// \treturn c.Reply(`\n// Sorry, this emoji is invalid, it has been defaulted to ⭐️, you can edit it after done by using /manage command.\n// 抱歉，這個emoji無效，並且已默認設定為⭐️，你可以在完成製作後使用 /manage 來修改。\n// \t`)\n// }\n\nfunc sendProcessingStickers(c tele.Context) error {\n\treturn c.Send(`\nProcessing stickers, please wait a while...\n正在製作貼圖，請稍等...\n`)\n}\n"
  },
  {
    "path": "core/os_util.go",
    "content": "package core\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc purgeOutdatedStorageData() {\n\tdirEntries, _ := os.ReadDir(dataDir)\n\tfor _, f := range dirEntries {\n\t\tif !f.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tfInfo, _ := f.Info()\n\t\tfMTime := fInfo.ModTime().Unix()\n\t\tfPath := filepath.Join(dataDir, f.Name())\n\t\t// 1 Day\n\t\tif fMTime < (time.Now().Unix() - 86400) {\n\t\t\tos.RemoveAll(fPath)\n\t\t\tusers.mu.Lock()\n\t\t\tfor uid, ud := range users.data {\n\t\t\t\tif ud.sessionID == f.Name() {\n\t\t\t\t\tlog.Warnf(\"Found outdated user data. Purging from map as well. SID:%s, UID:%d\", ud.sessionID, uid)\n\t\t\t\t\tdelete(users.data, uid)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tusers.mu.Unlock()\n\t\t\tlog.Infoln(\"Purged outdated user dir:\", fPath)\n\t\t}\n\t}\n\n\tif msbconf.WebappDataDir != \"\" {\n\t\twebappDataDirEntries, _ := os.ReadDir(msbconf.WebappDataDir)\n\t\tfor _, f := range webappDataDirEntries {\n\t\t\tif !f.IsDir() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfInfo, _ := f.Info()\n\t\t\tfMTime := fInfo.ModTime().Unix()\n\t\t\tfPath := filepath.Join(msbconf.WebappDataDir, f.Name())\n\t\t\t// 2 Days\n\t\t\tif fMTime < (time.Now().Unix() - 172800) {\n\t\t\t\tos.RemoveAll(fPath)\n\t\t\t\tlog.Infoln(\"Purged outdated webapp data dir:\", fPath)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "core/states.go",
    "content": "package core\n\nimport (\n\t\"errors\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/star-39/moe-sticker-bot/pkg/msbimport\"\n\ttele \"gopkg.in/telebot.v3\"\n)\n\n// Handle conversation state during a command.\nfunc handleMessage(c tele.Context) error {\n\tvar err error\n\tcommand, state := getState(c)\n\tif command == \"\" {\n\t\treturn handleNoSession(c)\n\t}\n\tswitch command {\n\tcase \"import\":\n\t\tswitch state {\n\t\tcase \"waitSTitle\":\n\t\t\terr = waitSTitle(c)\n\t\tcase \"waitEmojiChoice\":\n\t\t\terr = waitEmojiChoice(c)\n\t\tcase \"process\":\n\t\t\terr = stateProcessing(c)\n\t\tcase \"waitSEmojiAssign\":\n\t\t\terr = waitSEmojiAssign(c)\n\t\t}\n\tcase \"create\":\n\t\tswitch state {\n\t\tcase \"waitSType\":\n\t\t\terr = waitSType(c)\n\t\tcase \"waitSTitle\":\n\t\t\terr = waitSTitle(c)\n\t\tcase \"waitSID\":\n\t\t\terr = waitSID(c)\n\t\tcase \"waitSFile\":\n\t\t\terr = waitSFile(c)\n\t\tcase \"waitEmojiChoice\":\n\t\t\terr = waitEmojiChoice(c)\n\t\tcase \"waitSEmojiAssign\":\n\t\t\terr = waitSEmojiAssign(c)\n\t\tcase \"process\":\n\t\t\terr = stateProcessing(c)\n\t\t}\n\tcase \"manage\":\n\t\tswitch state {\n\t\tcase \"waitCbEditChoice\":\n\t\t\terr = waitCbEditChoice(c)\n\t\tcase \"waitSFile\":\n\t\t\terr = waitSFile(c)\n\t\tcase \"waitEmojiChoice\":\n\t\t\terr = waitEmojiChoice(c)\n\t\tcase \"waitSEmojiAssign\":\n\t\t\terr = waitSEmojiAssign(c)\n\t\tcase \"waitSTitle\":\n\t\t\terr = waitSTitle(c)\n\t\tcase \"waitSDel\":\n\t\t\terr = waitSDel(c)\n\t\tcase \"waitCbDelset\":\n\t\t\terr = waitCbDelset(c)\n\t\tcase \"process\":\n\t\t\terr = stateProcessing(c)\n\t\t}\n\tcase \"search\":\n\t\tswitch state {\n\t\tcase \"waitSearchKW\":\n\t\t\terr = waitSearchKeyword(c)\n\t\t}\n\tcase \"getfid\":\n\t\terr = cmdGetFID(c)\n\t}\n\treturn err\n}\n\n// Received bare message without using a command.\nfunc handleNoSession(c tele.Context) error {\n\tlog.Debugf(\"user %d entered no session with message: %s\", c.Sender().ID, c.Message().Text)\n\n\t//During previous stage, bot will reply to a message with callback buttons.\n\t//Now we react to user's choice.\n\tif c.Callback() != nil && c.Message().ReplyTo != nil {\n\t\tswitch c.Callback().Data {\n\t\tcase CB_DN_SINGLE:\n\t\t\treturn downloadStickersAndSend(c.Message().ReplyTo.Sticker, \"\", c)\n\t\tcase CB_DN_WHOLE:\n\t\t\tid := getSIDFromMessage(c.Message().ReplyTo)\n\t\t\treturn downloadStickersAndSend(nil, id, c)\n\t\tcase CB_MANAGE:\n\t\t\treturn statePrepareSManage(c)\n\t\tcase CB_OK_IMPORT:\n\t\t\treturn confirmImport(c, false)\n\t\tcase CB_OK_IMPORT_EMOJI:\n\t\t\treturn confirmImport(c, true)\n\t\tcase CB_OK_DN:\n\t\t\tud := initUserData(c, \"download\", \"process\")\n\t\t\tc.Send(\"Please wait...\")\n\t\t\tmsbimport.ParseImportLink(findLink(c.Message().ReplyTo.Text), ud.lineData)\n\t\t\treturn downloadLineSToZip(c, ud)\n\t\tcase CB_EXPORT_WA:\n\t\t\thex := secHex(6)\n\t\t\tid := getSIDFromMessage(c.Message().ReplyTo)\n\t\t\tss, _ := c.Bot().StickerSet(id)\n\t\t\tgo prepareWebAppExportStickers(ss, hex)\n\t\t\treturn sendConfirmExportToWA(c, id, hex)\n\t\tcase CB_BYE:\n\t\t\treturn c.Send(\"Bye. /start\")\n\t\t}\n\t}\n\n\t// bare sticker, ask user's choice.\n\tif c.Message().Sticker != nil {\n\t\tif matchUserS(c.Sender().ID, c.Message().Sticker.SetName) {\n\t\t\treturn sendAskSChoice(c, c.Message().Sticker.SetName)\n\t\t} else {\n\t\t\treturn sendAskSDownloadChoice(c, c.Message().Sticker)\n\t\t}\n\t}\n\n\t//Animation is MP4 video with no sound.\n\tif c.Message().Animation != nil {\n\t\treturn downloadGifToZip(c)\n\t}\n\n\tif c.Message().Photo != nil || c.Message().Document != nil {\n\t\treturn sendUseCommandToImport(c)\n\t}\n\n\t// bare text message, expect a link, if no link, search keyword.\n\tlink, tp := findLinkWithType(c.Message().Text)\n\n\tswitch tp {\n\tcase LINK_TG:\n\t\tif matchUserS(c.Sender().ID, path.Base(link)) {\n\t\t\treturn sendAskTGLinkChoice(c)\n\t\t} else {\n\t\t\treturn sendAskWantSDown(c)\n\t\t}\n\tcase LINK_IMPORT:\n\t\tld := &msbimport.LineData{}\n\t\twarn, err := msbimport.ParseImportLink(link, ld)\n\t\tif err != nil {\n\t\t\treturn sendBadImportLinkWarn(c)\n\t\t}\n\t\tif warn != \"\" {\n\t\t\tswitch warn {\n\t\t\tcase msbimport.WARN_KAKAO_PREFER_SHARE_LINK:\n\t\t\t\tsendPreferKakaoShareLinkWarning(c)\n\t\t\t}\n\t\t}\n\t\tsendNotifySExist(c, ld.Id)\n\t\treturn sendAskWantImportOrDownload(c, ld.IsEmoji)\n\n\tdefault:\n\t\tif c.Message().Text == \"\" {\n\t\t\treturn sendNoSessionWarning(c)\n\t\t}\n\t\t// User sent plain text, attempt to search.\n\t\tif trySearchKeyword(c) {\n\t\t\treturn sendNotifyNoSessionSearch(c)\n\t\t} else {\n\t\t\treturn sendNoSessionWarning(c)\n\t\t}\n\t}\n}\n\nfunc confirmImport(c tele.Context, wantEmoji bool) error {\n\tud := initUserData(c, \"import\", \"waitSTitle\")\n\t_, err := msbimport.ParseImportLink(findLink(c.Message().ReplyTo.Text), ud.lineData)\n\tif err != nil {\n\t\treturn err\n\t}\n\tud.stickerData.id = checkGnerateSIDFromLID(ud.lineData)\n\tworkDir := filepath.Join(ud.workDir, ud.lineData.Id)\n\tsendAskTitle_Import(c)\n\tud.wg.Add(1)\n\terr = msbimport.PrepareImportStickers(ud.ctx, ud.lineData, workDir, true, wantEmoji)\n\tud.wg.Done()\n\tif err != nil {\n\t\treturn err\n\t}\n\tud.stickerData.lAmount = ud.lineData.Amount\n\tud.stickerData.isVideo = ud.lineData.IsAnimated\n\tif ud.lineData.IsEmoji && wantEmoji {\n\t\tud.stickerData.stickerSetType = tele.StickerCustomEmoji\n\t\tud.stickerData.isCustomEmoji = true\n\t} else {\n\t\tud.stickerData.stickerSetType = tele.StickerRegular\n\t}\n\n\t//After PrepareImportStickers returns, individual LineFile might not be ready yet.\n\t//When transfering data to ud.stickerData.stickers, make sure to transfer finished data only.\n\tfor range ud.lineData.Files {\n\t\tsf := &StickerFile{}\n\t\tsf.wg.Add(1)\n\t\tud.stickerData.stickers = append(ud.stickerData.stickers, sf)\n\t}\n\tfor i, lf := range ud.lineData.Files {\n\t\tlf.Wg.Wait()\n\t\tud.stickerData.stickers[i].wg.Done()\n\t\tud.stickerData.stickers[i].oPath = lf.OriginalFile\n\t\tud.stickerData.stickers[i].cPath = lf.ConvertedFile\n\t}\n\treturn nil\n}\n\nfunc trySearchKeyword(c tele.Context) bool {\n\tkeywords := strings.Split(c.Text(), \" \")\n\tif len(keywords) == 0 {\n\t\treturn false\n\t}\n\tlines := searchLineS(keywords)\n\tif len(lines) == 0 {\n\t\treturn false\n\t}\n\tsendSearchResult(20, lines, c)\n\treturn true\n}\n\nfunc stateProcessing(c tele.Context) error {\n\tif c.Callback() != nil {\n\t\tif c.Callback().Data == \"bye\" {\n\t\t\treturn cmdQuit(c)\n\t\t}\n\t}\n\treturn c.Send(\"Processing, please wait... 作業中, 請稍後... /quit\")\n}\n\nfunc statePrepareSManage(c tele.Context) error {\n\tvar ud *UserData\n\tif c.Message().ReplyTo == nil {\n\t\treturn errors.New(\"unknown error: no reply to\")\n\t}\n\n\tud = initUserData(c, \"manage\", \"waitCbEditChoice\")\n\tid := getSIDFromMessage(c.Message().ReplyTo)\n\tud.stickerData.id = id\n\n\tud.lastContext = c\n\t// Allow admin to manage all sticker sets.\n\tif c.Sender().ID == msbconf.AdminUid {\n\t\tgoto NEXT\n\t}\n\tif !matchUserS(c.Sender().ID, ud.stickerData.id) {\n\t\treturn c.Send(\"Sorry, this sticker set cannot be edited. try another or /quit\")\n\t}\n\nNEXT:\n\terr := retrieveSSDetails(c, ud.stickerData.id, ud.stickerData)\n\tif err != nil {\n\t\treturn c.Send(\"bad sticker set! try again or /quit\")\n\t}\n\terr = prepareWebAppEditStickers(users.data[c.Sender().ID])\n\tif err != nil {\n\t\treturn c.Send(\"error preparing stickers for webapp /quit\")\n\t}\n\tif ud.stickerData.cAmount == 120 {\n\t\tsendStickerSetFullWarning(c)\n\t}\n\tsetState(c, \"waitCbEditChoice\")\n\treturn sendAskEditChoice(c)\n}\n\nfunc waitCbEditChoice(c tele.Context) error {\n\tif c.Callback() == nil {\n\t\treturn sendNoCbWarn(c)\n\t}\n\n\tswitch c.Callback().Data {\n\tcase CB_ADD_STICKER:\n\t\tsetState(c, \"waitSFile\")\n\t\treturn sendAskStickerFile(c)\n\tcase CB_DELETE_STICKER:\n\t\tsetState(c, \"waitSDel\")\n\t\treturn sendAskSDel(c)\n\tcase CB_DELETE_STICKER_SET:\n\t\tsetState(c, \"waitCbDelset\")\n\t\treturn sendConfirmDelset(c)\n\tcase CB_CHANGE_TITLE:\n\t\tsetState(c, \"waitSTitle\")\n\t\treturn sendAskTitle(c)\n\tcase CB_BYE:\n\t\tendManageSession(c)\n\t\tterminateSession(c)\n\tdefault:\n\t\treturn sendInStateWarning(c)\n\t}\n\treturn nil\n}\n\nfunc waitSDel(c tele.Context) error {\n\tud := users.data[c.Sender().ID]\n\tif c.Message().Sticker == nil {\n\t\treturn c.Send(\"send sticker! try again or /quit\")\n\t}\n\tif c.Message().Sticker.SetName != ud.stickerData.id {\n\t\treturn c.Send(\"wrong sticker! try again or /quit\")\n\t}\n\n\terr := c.Bot().DeleteSticker(c.Message().Sticker.FileID)\n\tif err != nil {\n\t\tc.Send(\"error deleting sticker! try another one or /quit\")\n\t\treturn err\n\t}\n\tc.Send(\"Delete OK. 成功刪除一張貼圖。\")\n\tud.stickerData.cAmount--\n\tif ud.stickerData.cAmount == 0 {\n\t\tdeleteUserS(ud.stickerData.id)\n\t\tdeleteLineS(ud.stickerData.id)\n\t\tterminateSession(c)\n\t\treturn nil\n\t} else {\n\t\tsetState(c, \"waitCbEditChoice\")\n\t\treturn sendAskEditChoice(c)\n\t}\n}\n\nfunc waitCbDelset(c tele.Context) error {\n\tif c.Callback() == nil {\n\t\tsetState(c, \"waitCbEditChoice\")\n\t\treturn sendAskEditChoice(c)\n\t}\n\tif c.Callback().Data != CB_YES {\n\t\tsetState(c, \"waitCbEditChoice\")\n\t\treturn sendAskEditChoice(c)\n\t}\n\tud := users.data[c.Sender().ID]\n\tsetState(c, \"process\")\n\tc.Send(\"please wait...\")\n\n\tss, _ := c.Bot().StickerSet(ud.stickerData.id)\n\tfor _, s := range ss.Stickers {\n\t\tc.Bot().DeleteSticker(s.FileID)\n\t}\n\tdeleteUserS(ud.stickerData.id)\n\tdeleteLineS(ud.stickerData.id)\n\tc.Send(\"Delete set OK. bye\")\n\tendManageSession(c)\n\tterminateSession(c)\n\treturn nil\n}\n\nfunc waitSType(c tele.Context) error {\n\tif c.Callback() == nil {\n\t\treturn c.Send(\"Please press a button. /quit\")\n\t}\n\n\tud := users.data[c.Sender().ID]\n\tif strings.Contains(c.Callback().Data, CB_CUSTOM_EMOJI) {\n\t\tud.stickerData.stickerSetType = tele.StickerCustomEmoji\n\t\tud.stickerData.isCustomEmoji = true\n\t} else {\n\t\tud.stickerData.stickerSetType = tele.StickerRegular\n\t\tud.stickerData.isCustomEmoji = false\n\t}\n\n\tsendAskTitle(c)\n\tsetState(c, \"waitSTitle\")\n\treturn nil\n}\n\nfunc waitSFile(c tele.Context) error {\n\tif c.Callback() != nil {\n\t\tswitch c.Callback().Data {\n\t\tcase CB_DONE_ADDING:\n\t\t\tgoto NEXT\n\t\tcase CB_BYE:\n\t\t\tterminateSession(c)\n\t\t\treturn nil\n\t\tdefault:\n\t\t\treturn sendPromptStopAdding(c)\n\t\t}\n\t}\n\tif c.Message().Media() != nil {\n\t\terr := appendMedia(c)\n\t\tif err != nil {\n\t\t\tc.Reply(\"Failed processing this file. 處理此檔案時錯誤:\\n\" + err.Error())\n\t\t}\n\t\treturn nil\n\t} else {\n\t\treturn sendPromptStopAdding(c)\n\t}\nNEXT:\n\tif len(users.data[c.Sender().ID].stickerData.stickers) == 0 {\n\t\treturn c.Send(\"No image received. try again or /quit\")\n\t}\n\n\tsetState(c, \"waitEmojiChoice\")\n\tsendAskEmoji(c)\n\n\treturn nil\n}\n\nfunc waitSTitle(c tele.Context) error {\n\tud := users.data[c.Sender().ID]\n\tcommand := ud.command\n\n\t// User sent text instead of clicking button.\n\tif c.Callback() == nil {\n\t\tif command == \"create\" || command == \"import\" {\n\t\t\tud.stickerData.title = c.Message().Text\n\t\t} else if command == \"manage\" {\n\t\t\terr := c.Bot().SetStickerSetTitle(c.Recipient(), c.Message().Text, ud.stickerData.id)\n\t\t\tsetState(c, \"waitCbEditChoice\")\n\t\t\tif err != nil {\n\t\t\t\tlog.Warnln(err)\n\t\t\t\treturn sendSSTitleFailedToChanged(c)\n\t\t\t} else {\n\t\t\t\treturn sendSSTitleChanged(c)\n\t\t\t}\n\t\t} else {\n\t\t\treturn nil\n\t\t}\n\t\t// User clicked a button, only command \"import\" is allowed.\n\t} else {\n\t\t//Reject.\n\t\tif command != \"import\" {\n\t\t\treturn nil\n\t\t}\n\t\ttitleIndex, atoiErr := strconv.Atoi(c.Callback().Data)\n\t\tif atoiErr == nil && titleIndex != -1 {\n\t\t\tud.stickerData.title = ud.lineData.I18nTitles[titleIndex] + \" @\" + botName\n\t\t} else {\n\t\t\tud.stickerData.title = ud.lineData.Title + \" @\" + botName\n\t\t}\n\t}\n\n\tif !checkTitle(ud.stickerData.title) {\n\t\treturn c.Send(\"bad title! try again or /quit\")\n\t}\n\n\tswitch command {\n\tcase \"import\":\n\t\tsetState(c, \"waitEmojiChoice\")\n\t\treturn sendAskEmoji(c)\n\tcase \"create\":\n\t\tsetState(c, \"waitSID\")\n\t\tsendAskID(c)\n\t}\n\n\treturn nil\n}\n\nfunc waitSID(c tele.Context) error {\n\tvar id string\n\tif c.Callback() != nil {\n\t\tif c.Callback().Data == \"auto\" {\n\t\t\tusers.data[c.Sender().ID].stickerData.id = \"sticker_\" + secHex(4) + \"_by_\" + botName\n\t\t\tgoto NEXT\n\t\t}\n\t}\n\n\tid = regexAlphanum.FindString(c.Message().Text)\n\tif !checkID(id) {\n\t\treturn sendBadIDWarn(c)\n\t}\n\tid = id + \"_by_\" + botName\n\tif _, err := c.Bot().StickerSet(id); err == nil {\n\t\treturn sendIDOccupiedWarn(c)\n\t}\n\tusers.data[c.Sender().ID].stickerData.id = id\n\nNEXT:\n\tsetState(c, \"waitSFile\")\n\treturn sendAskStickerFile(c)\n}\n\nfunc waitEmojiChoice(c tele.Context) error {\n\tud := users.data[c.Sender().ID]\n\tif c.Callback() != nil {\n\t\tswitch c.Callback().Data {\n\t\tcase \"random\":\n\t\t\tusers.data[c.Sender().ID].stickerData.emojis = []string{\"⭐\"}\n\t\tcase \"manual\":\n\t\t\tsendProcessStarted(ud, c, \"preparing...\")\n\t\t\tsetState(c, ST_PROCESSING)\n\t\t\tud.wg.Wait()\n\t\t\tfor range ud.stickerData.stickers {\n\t\t\t\tud.commitChans = append(ud.commitChans, make(chan bool))\n\t\t\t}\n\t\t\tsetState(c, \"waitSEmojiAssign\")\n\t\t\treturn sendAskEmojiAssign(c)\n\t\tdefault:\n\t\t\treturn nil\n\t\t}\n\t} else {\n\t\temojis := findEmojis(c.Message().Text)\n\t\tif emojis == \"\" {\n\t\t\treturn c.Reply(\"Send emoji or press button a button.\\n請傳送emoji或點選按鈕。 /quit\")\n\t\t}\n\t\tusers.data[c.Sender().ID].stickerData.emojis = []string{emojis}\n\t}\n\n\tsetState(c, ST_PROCESSING)\n\n\terr := submitStickerSetAuto(!(ud.command == \"manage\"), c)\n\tendSession(c)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc waitSEmojiAssign(c tele.Context) error {\n\temojiList := findEmojiList(c.Message().Text)\n\tif len(emojiList) == 0 {\n\t\treturn c.Reply(\"Please send emoji and keywords(optional).\\n請傳送emoji和 關鍵字(可選)。\\ntry again or /quit\")\n\t}\n\tkeywords := stripEmoji(c.Message().Text)\n\tkeywordList := []string{}\n\tif len(keywords) > 0 {\n\t\tkeywordList = strings.Split(keywords, \" \")\n\t}\n\n\tud := users.data[c.Sender().ID]\n\tsetState(c, ST_PROCESSING)\n\n\terr := submitStickerManual(!(users.data[c.Sender().ID].command == \"manage\"), ud.stickerData.pos, emojiList, keywordList, c)\n\tif err != nil {\n\t\treturn err\n\t}\n\tud.stickerData.pos += 1\n\tif ud.stickerData.pos == ud.stickerData.lAmount {\n\t\treturn sendProcessingStickers(c)\n\t} else {\n\t\tsendAskEmojiAssign(c)\n\t\tsetState(c, \"waitSEmojiAssign\")\n\t\treturn nil\n\t}\n}\n\nfunc waitSearchKeyword(c tele.Context) error {\n\tkeywords := strings.Split(c.Text(), \" \")\n\tlines := searchLineS(keywords)\n\tif len(lines) == 0 {\n\t\treturn sendSearchNoResult(c)\n\t}\n\tsendSearchResult(-1, lines, c)\n\tterminateSession(c)\n\treturn nil\n}\n"
  },
  {
    "path": "core/sticker.go",
    "content": "package core\n\nimport (\n\t\"errors\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/star-39/moe-sticker-bot/pkg/msbimport\"\n\ttele \"gopkg.in/telebot.v3\"\n)\n\n//TODO: Shrink oversized function.\n\n// Final stage of automated sticker submission.\n// Automated means all emojis are same.\nfunc submitStickerSetAuto(createSet bool, c tele.Context) error {\n\tuid := c.Sender().ID\n\tud := users.data[uid]\n\tpText, teleMsg, _ := sendProcessStarted(ud, c, \"Waiting...\")\n\tud.wg.Wait()\n\n\tdefer cleanUserData(uid)\n\n\tif len(ud.stickerData.stickers) == 0 {\n\t\tlog.Error(\"No sticker to commit!\")\n\t\treturn errors.New(\"no sticker available\")\n\t}\n\n\tlog.Debugln(\"stickerData summary:\")\n\tlog.Debugln(ud.stickerData)\n\tcommittedStickers := 0\n\terrorCount := 0\n\tflCount := &ud.stickerData.flCount\n\tssName := ud.stickerData.id\n\tssTitle := ud.stickerData.title\n\tssType := ud.stickerData.stickerSetType\n\n\t//Set emojis and keywords in batch.\n\tfor _, s := range ud.stickerData.stickers {\n\t\ts.emojis = ud.stickerData.emojis\n\t\ts.keywords = MSB_DEFAULT_STICKER_KEYWORDS\n\t}\n\n\t//Try batch create.\n\tvar batchCreateSuccess bool\n\tif createSet {\n\t\terr := createStickerSetBatch(ud.stickerData.stickers, c, ssName, ssTitle, ssType)\n\t\tif err != nil {\n\t\t\tlog.Warnln(\"sticker.go: Error batch create:\", err.Error())\n\t\t} else {\n\t\t\tlog.Debugln(\"sticker.go: Batch create success.\")\n\t\t\tbatchCreateSuccess = true\n\t\t\tif len(ud.stickerData.stickers) < 51 {\n\t\t\t\tcommittedStickers = len(ud.stickerData.stickers)\n\t\t\t} else {\n\t\t\t\tcommittedStickers = 50\n\t\t\t}\n\t\t}\n\t}\n\n\t//One by one commit.\n\tfor index, sf := range ud.stickerData.stickers {\n\t\tvar err error\n\n\t\t//Sticker set already finished.\n\t\tif batchCreateSuccess && len(ud.stickerData.stickers) < 51 {\n\t\t\tgo editProgressMsg(len(ud.stickerData.stickers), len(ud.stickerData.stickers), \"\", pText, teleMsg, c)\n\t\t\tbreak\n\t\t}\n\t\t//Sticker set is larger than 50 and batch succeeded.\n\t\t//Skip first 50 stickers.\n\t\tif batchCreateSuccess && len(ud.stickerData.stickers) > 50 {\n\t\t\tif index < 50 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\t//Batch creation failed, run normal creation procedure if createSet is true.\n\t\tif createSet && index == 0 {\n\t\t\terr = createStickerSet(false, sf, c, ssName, ssTitle, ssType)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorln(\"create sticker set failed!. \", err)\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\tcommittedStickers += 1\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tgo editProgressMsg(index, len(ud.stickerData.stickers), \"\", pText, teleMsg, c)\n\n\t\terr = commitSingleticker(index, flCount, false, sf, c, ssName, ssType)\n\t\tif err != nil {\n\t\t\tlog.Warnln(\"execAutoCommit: a sticker failed to add.\", err)\n\t\t\tsendOneStickerFailedToAdd(c, index, err)\n\t\t\terrorCount += 1\n\t\t} else {\n\t\t\tlog.Debugln(\"one sticker commited. count: \", committedStickers)\n\t\t\tcommittedStickers += 1\n\t\t}\n\t\t// If encountered flood limit more than once, set a interval.\n\t\tif *flCount == 1 {\n\t\t\tsleepTime := 10 + rand.Intn(10)\n\t\t\ttime.Sleep(time.Duration(sleepTime) * time.Second)\n\t\t} else if *flCount > 1 {\n\t\t\tsleepTime := 60 + rand.Intn(10)\n\t\t\ttime.Sleep(time.Duration(sleepTime) * time.Second)\n\t\t}\n\t}\n\n\t// Tolerate at most 3 errors when importing sticker set.\n\tif ud.command == \"import\" && errorCount > 3 {\n\t\treturn errors.New(\"too many errors importing\")\n\t}\n\n\tif createSet {\n\t\tif ud.command == \"import\" {\n\t\t\tinsertLineS(ud.lineData.Id, ud.lineData.Link, ud.stickerData.id, ud.stickerData.title, true)\n\t\t\t// Only verify for import.\n\t\t\t// User generated sticker set might intentionally contain same stickers.\n\t\t\tif *flCount > 1 {\n\t\t\t\tverifyFloodedStickerSet(c, *flCount, errorCount, ud.lineData.Amount, ud.stickerData.id)\n\t\t\t}\n\t\t}\n\t\tinsertUserS(c.Sender().ID, ud.stickerData.id, ud.stickerData.title, time.Now().Unix())\n\t}\n\teditProgressMsg(0, 0, \"Success! /start\", pText, teleMsg, c)\n\tsendSFromSS(c, ud.stickerData.id, teleMsg)\n\treturn nil\n}\n\n// Only fatal error should be returned.\nfunc submitStickerManual(createSet bool, pos int, emojis []string, keywords []string, c tele.Context) error {\n\tud := users.data[c.Sender().ID]\n\tvar err error\n\tname := ud.stickerData.id\n\ttitle := ud.stickerData.title\n\tssType := ud.stickerData.stickerSetType\n\n\tif len(ud.stickerData.stickers) == 0 {\n\t\tlog.Error(\"No sticker to commit!!\")\n\t\treturn errors.New(\"no sticker available\")\n\t}\n\n\tsf := ud.stickerData.stickers[pos]\n\tsf.emojis = emojis\n\tsf.keywords = keywords\n\n\t//Do not submit to goroutine when creating sticker set.\n\tif createSet && pos == 0 {\n\t\tdefer close(ud.commitChans[pos])\n\t\terr = createStickerSet(false, sf, c, name, title, ssType)\n\t\tif err != nil {\n\t\t\tlog.Errorln(\"create failed. \", err)\n\t\t\treturn err\n\t\t} else {\n\t\t\tud.stickerData.cAmount += 1\n\t\t}\n\t\tif ud.stickerData.lAmount == 1 {\n\t\t\treturn finalizeSubmitStickerManual(c, createSet, ud)\n\t\t}\n\t} else {\n\t\tgo func() {\n\t\t\t//wait for the previous commit to be done.\n\t\t\tif pos > 0 {\n\t\t\t\t<-ud.commitChans[pos-1]\n\t\t\t}\n\n\t\t\terr = commitSingleticker(pos, &ud.stickerData.flCount, false, sf, c, name, ssType)\n\t\t\tif err != nil {\n\t\t\t\tsendOneStickerFailedToAdd(c, pos, err)\n\t\t\t\tlog.Warnln(\"execEmojiAssign: a sticker failed to add: \", err)\n\t\t\t} else {\n\t\t\t\tud.stickerData.cAmount += 1\n\t\t\t}\n\n\t\t\tif pos+1 == ud.stickerData.lAmount {\n\t\t\t\tfinalizeSubmitStickerManual(c, createSet, ud)\n\t\t\t}\n\t\t\tclose(ud.commitChans[pos])\n\t\t}()\n\t}\n\treturn nil\n}\n\nfunc finalizeSubmitStickerManual(c tele.Context, createSet bool, ud *UserData) error {\n\tif createSet {\n\t\tif ud.command == \"import\" {\n\t\t\tinsertLineS(ud.lineData.Id, ud.lineData.Link, ud.stickerData.id, ud.stickerData.title, false)\n\t\t}\n\t\tinsertUserS(c.Sender().ID, ud.stickerData.id, ud.stickerData.title, time.Now().Unix())\n\t}\n\tsendExecEmojiAssignFinished(c)\n\t// c.Send(\"Success! /start\")\n\tsendSFromSS(c, ud.stickerData.id, nil)\n\tendSession(c)\n\treturn nil\n}\n\n// Create sticker set if needed.\nfunc createStickerSet(safeMode bool, sf *StickerFile, c tele.Context, name string, title string, ssType string) error {\n\tvar file string\n\tvar isCustomEmoji bool\n\tif ssType == tele.StickerCustomEmoji {\n\t\tisCustomEmoji = true\n\t}\n\n\tsf.wg.Wait()\n\n\tif safeMode {\n\t\tfile, _ = msbimport.FFToWebmSafe(sf.oPath, isCustomEmoji)\n\t} else {\n\t\tfile = sf.cPath\n\t}\n\n\tlog.Debugln(\"createStickerSet: attempting, sticker file path:\", sf.cPath)\n\n\tinput := tele.InputSticker{\n\t\tEmojis:   sf.emojis,\n\t\tKeywords: sf.keywords,\n\t}\n\tif sf.fileID != \"\" {\n\t\tinput.Sticker = sf.fileID\n\t\tinput.Format = sf.format\n\t} else {\n\t\tinput.Sticker = \"file://\" + file\n\t\tinput.Format = guessInputStickerFormat(file)\n\t}\n\n\terr := c.Bot().CreateStickerSet(c.Recipient(), []tele.InputSticker{input}, name, title, ssType)\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tlog.Errorf(\"createStickerSet error:%s for set:%s.\", err, name)\n\n\t// Only handle video_long error here, return all other error types.\n\tif strings.Contains(strings.ToLower(err.Error()), \"video_long\") {\n\t\t// Redo with safe mode on.\n\t\t// This should happen only one time.\n\t\t// So if safe mode is on and this error still occurs, return err.\n\t\tif safeMode {\n\t\t\tlog.Error(\"safe mode DID NOT resolve video_long problem.\")\n\t\t\treturn err\n\t\t} else {\n\t\t\tlog.Warnln(\"returned video_long, attempting safe mode.\")\n\t\t\treturn createStickerSet(true, sf, c, name, title, ssType)\n\t\t}\n\t} else {\n\t\treturn err\n\t}\n}\n\n// Create sticker set with multiple StickerFile.\n// API 7.2 feature, consider it experimental.\n// If it failed, no retry, just return error and we try conventional way.\nfunc createStickerSetBatch(sfs []*StickerFile, c tele.Context, name string, title string, ssType string) error {\n\tvar inputs []tele.InputSticker\n\tlog.Debugln(\"createStickerSetBatch: attempting, batch creation:\", name)\n\n\tfor i, sf := range sfs {\n\t\tsf.wg.Wait()\n\t\tfile := sf.cPath\n\t\tinput := tele.InputSticker{\n\t\t\tEmojis:   sf.emojis,\n\t\t\tKeywords: sf.keywords,\n\t\t}\n\t\tif sf.fileID != \"\" {\n\t\t\tinput.Sticker = sf.fileID\n\t\t\tinput.Format = sf.format\n\t\t} else {\n\t\t\tinput.Sticker = \"file://\" + file\n\t\t\tinput.Format = guessInputStickerFormat(file)\n\t\t}\n\t\tinputs = append(inputs, input)\n\n\t\t//Up to 50 stickers.\n\t\tif i == 49 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn c.Bot().CreateStickerSet(c.Recipient(), inputs, name, title, ssType)\n}\n\n// Commit single sticker, retry happens inside this function.\n// If all retries failed, return err.\n//\n// flCount counts the total flood limit for entire sticker set.\n// pos is for logging only.\nfunc commitSingleticker(pos int, flCount *int, safeMode bool, sf *StickerFile, c tele.Context, name string, ssType string) error {\n\tvar err error\n\tvar floodErr tele.FloodError\n\tvar file string\n\tvar isCustomEmoji bool\n\tif ssType == tele.StickerCustomEmoji {\n\t\tisCustomEmoji = true\n\t}\n\tsf.wg.Wait()\n\n\tif safeMode {\n\t\tfile, _ = msbimport.FFToWebmSafe(sf.oPath, isCustomEmoji)\n\t} else {\n\t\tfile = sf.cPath\n\t}\n\n\tlog.Debugln(\"commitSingleticker: attempting, sticker file path:\", sf.cPath)\n\t// Retry loop.\n\t// For each sticker, retry at most 2 times, means 3 commit attempts in total.\n\tfor i := 0; i < 3; i++ {\n\t\tinput := tele.InputSticker{\n\t\t\tEmojis:   sf.emojis,\n\t\t\tKeywords: sf.keywords,\n\t\t}\n\t\tif sf.fileID != \"\" {\n\t\t\tinput.Sticker = sf.fileID\n\t\t\tinput.Format = sf.format\n\t\t} else {\n\t\t\tinput.Sticker = \"file://\" + file\n\t\t\tinput.Format = guessInputStickerFormat(file)\n\t\t}\n\n\t\terr = c.Bot().AddSticker(c.Recipient(), input, name)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tlog.Errorf(\"commit sticker error:%s for set:%s.\", err, name)\n\t\t// This flood limit error only happens to a specific user at a specific time.\n\t\t// It is \"fake\" most of time, since TDLib in API Server will automatically retry.\n\t\t// However, API always return 429.\n\t\t// Since API side will always do retry at TDLib level, message_id was also being kept so\n\t\t// no position shift will happen.\n\t\t// Flood limit error could be probably ignored.\n\t\tif errors.As(err, &floodErr) {\n\t\t\t// This reflects the retry count for entire SS.\n\t\t\t*flCount += 1\n\t\t\tlog.Warnf(\"commitSticker: Flood limit encountered for user:%d, set:%s, count:%d, pos:%d\", c.Sender().ID, name, *flCount, pos)\n\t\t\tlog.Warnln(\"commitSticker: commit sticker retry after: \", floodErr.RetryAfter)\n\t\t\tif *flCount == 2 {\n\t\t\t\tsendFLWarning(c)\n\t\t\t}\n\n\t\t\t//Sleep\n\t\t\tif floodErr.RetryAfter > 60 {\n\t\t\t\tlog.Error(\"RA too long! Telegram's bug? Attempt to sleep for 120 seconds.\")\n\t\t\t\ttime.Sleep(120 * time.Second)\n\t\t\t} else {\n\t\t\t\textraRA := *flCount * 15\n\t\t\t\tlog.Warnf(\"Sleeping for %d seconds due to FL.\", floodErr.RetryAfter+extraRA)\n\t\t\t\ttime.Sleep(time.Duration(floodErr.RetryAfter+extraRA) * time.Second)\n\t\t\t}\n\n\t\t\tlog.Warnf(\"Woken up from RA sleep. ignoring this error. user:%d, set:%s, count:%d, pos:%d\", c.Sender().ID, name, *flCount, pos)\n\n\t\t\t//According to collected logs, exceeding 2 flood counts will sometimes cause api server to stop auto retrying.\n\t\t\t//Hence, we do retry here, else, break retry loop.\n\t\t\tif *flCount > 2 {\n\t\t\t\tcontinue\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t} else if strings.Contains(strings.ToLower(err.Error()), \"video_long\") {\n\t\t\t// Redo with safe mode on.\n\t\t\t// This should happen only one time.\n\t\t\t// So if safe mode is on and this error still occurs, return err.\n\t\t\tif safeMode {\n\t\t\t\tlog.Error(\"safe mode DID NOT resolve video_long problem.\")\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\tlog.Warnln(\"returned video_long, attempting safe mode.\")\n\t\t\t\treturn commitSingleticker(pos, flCount, true, sf, c, name, ssType)\n\t\t\t}\n\t\t} else if strings.Contains(err.Error(), \"400\") {\n\t\t\t// return remaining 400 BAD REQUEST immediately to parent without retry.\n\t\t\treturn err\n\t\t} else if strings.Contains(err.Error(), \"invalid sticker emojis\") {\n\t\t\tlog.Warn(\"commitSticker: invalid emoji, resetting to a star emoji and retrying...\")\n\t\t\tinput.Emojis = []string{\"⭐️\"}\n\t\t} else {\n\t\t\t// Handle unknown error here.\n\t\t\t// We simply retry for 2 more times with 5 sec interval.\n\t\t\tlog.Warnln(\"commitSticker: retrying... cause:\", err)\n\t\t\ttime.Sleep(5 * time.Second)\n\t\t}\n\t}\n\n\tlog.Warn(\"commitSticker: too many retries\")\n\tif errors.As(err, &floodErr) {\n\t\tlog.Warn(\"commitSticker: reached max retry for flood limit, assume success.\")\n\t\treturn nil\n\t}\n\treturn err\n}\n\nfunc editStickerEmoji(newEmojis []string, fid string, ud *UserData) error {\n\treturn b.SetStickerEmojiList(ud.lastContext.Recipient(), fid, newEmojis)\n}\n\n// Receive and process user uploaded media file and convert to Telegram compliant format.\n// Accept telebot Media and Sticker only.\nfunc appendMedia(c tele.Context) error {\n\tlog.Debugf(\"appendMedia: Received file, MType:%s, FileID:%s\", c.Message().Media().MediaType(), c.Message().Media().MediaFile().FileID)\n\tvar files []string\n\tvar sfs []*StickerFile\n\tvar err error\n\tvar workDir string\n\tvar savePath string\n\n\tud := users.data[c.Sender().ID]\n\tud.wg.Add(1)\n\tdefer ud.wg.Done()\n\n\tif ud.stickerData.cAmount+len(ud.stickerData.stickers) > 120 {\n\t\treturn errors.New(\"sticker set already full 此貼圖包已滿\")\n\t}\n\n\t//Incoming media is a sticker.\n\tif c.Message().Sticker != nil && ((c.Message().Sticker.Type == tele.StickerCustomEmoji) == ud.stickerData.isCustomEmoji) {\n\t\tvar format string\n\t\tif c.Message().Sticker.Video {\n\t\t\tformat = \"video\"\n\t\t} else {\n\t\t\tformat = \"static\"\n\t\t}\n\t\tsfs = append(sfs, &StickerFile{\n\t\t\tfileID: c.Message().Sticker.FileID,\n\t\t\tformat: format,\n\t\t})\n\t\tlog.Debugf(\"One received sticker file OK. ID:%s\", c.Message().Sticker.FileID)\n\t\tgoto CONTINUE\n\t}\n\n\tworkDir = users.data[c.Sender().ID].workDir\n\tsavePath = filepath.Join(workDir, secHex(4))\n\n\tif c.Message().Media().MediaType() == \"document\" {\n\t\tsavePath += filepath.Ext(c.Message().Document.FileName)\n\t} else if c.Message().Media().MediaType() == \"animation\" {\n\t\tsavePath += filepath.Ext(c.Message().Animation.FileName)\n\t}\n\n\terr = c.Bot().Download(c.Message().Media().MediaFile(), savePath)\n\tif err != nil {\n\t\treturn errors.New(\"error downloading media\")\n\t}\n\n\tif guessIsArchive(savePath) {\n\t\tfiles = append(files, msbimport.ArchiveExtract(savePath)...)\n\t} else {\n\t\tfiles = append(files, savePath)\n\t}\n\n\tlog.Debugln(\"appendMedia: Media downloaded to savepath:\", savePath)\n\tfor _, f := range files {\n\t\tvar cf string\n\t\tvar err error\n\t\t//If incoming media is already a sticker, use the file as is.\n\t\tif c.Message().Sticker != nil && ((c.Message().Sticker.Type == \"custom_emoji\") == ud.stickerData.isCustomEmoji) {\n\t\t\tcf = f\n\t\t} else {\n\t\t\tcf, err = msbimport.ConverMediaToTGStickerSmart(f, ud.stickerData.isCustomEmoji)\n\t\t}\n\n\t\tif err != nil {\n\t\t\tlog.Warnln(\"Failed converting one user sticker\", err)\n\t\t\tc.Send(\"Failed converting one user sticker:\" + err.Error())\n\t\t\tcontinue\n\t\t}\n\t\tsfs = append(sfs, &StickerFile{\n\t\t\toPath: f,\n\t\t\tcPath: cf,\n\t\t})\n\t\tlog.Debugf(\"One received file OK. oPath:%s | cPath:%s\", f, cf)\n\t}\n\nCONTINUE:\n\tif len(sfs) == 0 {\n\t\treturn errors.New(\"download or convert error\")\n\t}\n\n\tud.stickerData.stickers = append(ud.stickerData.stickers, sfs...)\n\tud.stickerData.lAmount = len(ud.stickerData.stickers)\n\treplySFileOK(c, len(ud.stickerData.stickers))\n\treturn nil\n}\n\nfunc guessIsArchive(f string) bool {\n\tf = strings.ToLower(f)\n\tarchiveExts := []string{\".rar\", \".7z\", \".zip\", \".tar\", \".gz\", \".bz2\", \".zst\", \".rar5\"}\n\tfor _, ext := range archiveExts {\n\t\tif strings.HasSuffix(f, ext) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc verifyFloodedStickerSet(c tele.Context, fc int, ec int, desiredAmount int, ssn string) {\n\ttime.Sleep(31 * time.Second)\n\tss, err := b.StickerSet(ssn)\n\tif err != nil {\n\t\treturn\n\t}\n\tif desiredAmount < len(ss.Stickers) {\n\t\tlog.Warnf(\"A flooded sticker set duplicated! floodCount:%d, errorCount:%d, ssn:%s, desired:%d, got:%d\", fc, ec, ssn, desiredAmount, len(ss.Stickers))\n\t\tlog.Warnf(\"Attempting dedup!\")\n\t\tworkdir := filepath.Join(dataDir, secHex(8))\n\t\tos.MkdirAll(workdir, 0755)\n\t\tfor si, s := range ss.Stickers {\n\t\t\tif si > 0 {\n\t\t\t\tfp := filepath.Join(workdir, strconv.Itoa(si-1)+\".webp\")\n\t\t\t\tf := filepath.Join(workdir, strconv.Itoa(si)+\".webp\")\n\t\t\t\tc.Bot().Download(&s.File, f)\n\n\t\t\t\tif compCRC32(f, fp) {\n\t\t\t\t\tb.DeleteSticker(s.FileID)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tos.RemoveAll(workdir)\n\t} else if desiredAmount > len(ss.Stickers) {\n\t\tlog.Warnf(\"A flooded sticker set missing sticker! floodCount:%d, errorCount:%d, ssn:%s, desired:%d, got:%d\", fc, ec, ssn, desiredAmount, len(ss.Stickers))\n\t\tc.Reply(\"Sorry, this sticker set seems corrupted, please check.\\n抱歉, 這個貼圖包似乎有缺失貼圖, 請檢查一下.\")\n\t} else {\n\t\tlog.Infof(\"A flooded sticker set seems ok. floodCount:%d, errorCount:%d, ssn:%s, desired:%d, got:%d\", fc, ec, ssn, desiredAmount, len(ss.Stickers))\n\t}\n}\n"
  },
  {
    "path": "core/sticker_download.go",
    "content": "package core\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/panjf2000/ants/v2\"\n\t\"github.com/star-39/moe-sticker-bot/pkg/msbimport\"\n\ttele \"gopkg.in/telebot.v3\"\n)\n\n// When s is not nil, download single sticker,\n// otherwise, download whole set from setID.\nfunc downloadStickersAndSend(s *tele.Sticker, setID string, c tele.Context) error {\n\tvar id string\n\tif s != nil {\n\t\tid = s.SetName\n\t} else {\n\t\tid = setID\n\t}\n\n\tsID := secHex(8)\n\tud := &UserData{\n\t\tworkDir:     filepath.Join(dataDir, sID),\n\t\tstickerData: &StickerData{},\n\t\tlineData:    &msbimport.LineData{},\n\t}\n\n\tworkDir := filepath.Join(ud.workDir, id)\n\tos.MkdirAll(workDir, 0755)\n\tvar flist []string\n\tvar cflist []string\n\tvar err error\n\n\t//Single sticker\n\tif s != nil {\n\t\tobj := &StickerDownloadObject{\n\t\t\twg:          sync.WaitGroup{},\n\t\t\tsticker:     *s,\n\t\t\tdest:        filepath.Join(workDir, s.SetName+\"_\"+s.Emoji),\n\t\t\tneedConvert: true,\n\t\t\tshrinkGif:   false,\n\t\t\tforWebApp:   false,\n\t\t}\n\t\tobj.wg.Add(1)\n\t\twDownloadStickerObject(obj)\n\t\tif obj.err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif s.Video || s.Animated {\n\t\t\tzip := filepath.Join(workDir, secHex(4)+\".zip\")\n\t\t\tmsbimport.FCompress(zip, []string{obj.cf})\n\t\t\tc.Bot().Send(c.Recipient(), &tele.Document{FileName: filepath.Base(zip), File: tele.FromDisk(zip)})\n\t\t} else {\n\t\t\tc.Bot().Send(c.Recipient(), &tele.Document{FileName: filepath.Base(obj.cf), File: tele.FromDisk(obj.cf)})\n\t\t}\n\t\treturn err\n\t}\n\n\t//Sticker set\n\tss, err := c.Bot().StickerSet(setID)\n\tif err != nil {\n\t\treturn sendBadSNWarn(c)\n\t}\n\tud.stickerData.id = ss.Name\n\tud.stickerData.title = ss.Title\n\tpText, pMsg, _ := sendProcessStarted(ud, c, \"\")\n\tcleanUserData(c.Sender().ID)\n\tdefer os.RemoveAll(workDir)\n\tvar wpDownloadSticker *ants.PoolWithFunc\n\twpDownloadSticker, _ = ants.NewPoolWithFunc(4, wDownloadStickerObject)\n\n\tdefer wpDownloadSticker.Release()\n\timageTime := time.Now()\n\tvar objs []*StickerDownloadObject\n\tfor index, s := range ss.Stickers {\n\t\tobj := &StickerDownloadObject{\n\t\t\twg:          sync.WaitGroup{},\n\t\t\tsticker:     s,\n\t\t\tdest:        filepath.Join(workDir, fmt.Sprintf(\"%s_%d_%s\", setID, index+1, s.Emoji)),\n\t\t\tneedConvert: true,\n\t\t\tshrinkGif:   false,\n\t\t\tforWebApp:   false,\n\t\t}\n\t\tobj.wg.Add(1)\n\t\tobjs = append(objs, obj)\n\t\tgo wpDownloadSticker.Invoke(obj)\n\t}\n\tfor i, obj := range objs {\n\t\tgo editProgressMsg(i, len(ss.Stickers), \"\", pText, pMsg, c)\n\t\tobj.wg.Wait()\n\t\timageTime = imageTime.Add(time.Duration(i+1) * time.Second)\n\t\tmsbimport.SetImageTime(obj.of, imageTime)\n\t\tflist = append(flist, obj.of)\n\t\tcflist = append(cflist, obj.cf)\n\t}\n\tgo editProgressMsg(0, 0, \"Uploading...\", pText, pMsg, c)\n\n\toriginalZipPath := filepath.Join(workDir, setID+\"_original.zip\")\n\tconvertedZipPath := filepath.Join(workDir, setID+\"_converted.zip\")\n\n\tvar zipPaths []string\n\n\tzipPaths = append(zipPaths, msbimport.FCompressVol(originalZipPath, flist)...)\n\tzipPaths = append(zipPaths, msbimport.FCompressVol(convertedZipPath, cflist)...)\n\n\tfor _, zipPath := range zipPaths {\n\t\t_, err := c.Bot().Send(c.Recipient(), &tele.Document{FileName: filepath.Base(zipPath), File: tele.FromDisk(zipPath)})\n\t\ttime.Sleep(1 * time.Second)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\teditProgressMsg(0, 0, \"success! /start\", pText, pMsg, c)\n\treturn nil\n}\n\nfunc downloadGifToZip(c tele.Context) error {\n\tc.Reply(\"Downloading, please wait...\\n正在下載, 請稍等...\")\n\tworkDir := filepath.Join(dataDir, secHex(4))\n\tos.MkdirAll(workDir, 0755)\n\tdefer os.RemoveAll(workDir)\n\n\tf := filepath.Join(workDir, \"animation_MP4.mp4\")\n\terr := c.Bot().Download(&c.Message().Animation.File, f)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcf, _ := msbimport.FFToGif(f)\n\tcf2 := strings.ReplaceAll(cf, \"animation_MP4.mp4\", \"animation_GIF.gif\")\n\tos.Rename(cf, cf2)\n\tzip := filepath.Join(workDir, secHex(4)+\".zip\")\n\tmsbimport.FCompress(zip, []string{cf2})\n\n\t_, err = c.Bot().Reply(c.Message(), &tele.Document{FileName: filepath.Base(zip), File: tele.FromDisk(zip)})\n\treturn err\n}\n\nfunc downloadLineSToZip(c tele.Context, ud *UserData) error {\n\tworkDir := filepath.Join(ud.workDir, ud.lineData.Id)\n\terr := msbimport.PrepareImportStickers(ud.ctx, ud.lineData, workDir, false, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, f := range ud.lineData.Files {\n\t\tf.Wg.Wait()\n\t}\n\t// workDir := filepath.Dir(ud.lineData.files[0])\n\tzipName := ud.lineData.Id + \".zip\"\n\tzipPath := filepath.Join(workDir, zipName)\n\n\tvar files []string\n\tfor _, lf := range ud.lineData.Files {\n\t\tfiles = append(files, lf.OriginalFile)\n\t}\n\tmsbimport.FCompress(zipPath, files)\n\t_, err = c.Bot().Send(c.Recipient(), &tele.Document{FileName: zipName, File: tele.FromDisk(zipPath)})\n\tendSession(c)\n\treturn err\n}\n"
  },
  {
    "path": "core/userdata.go",
    "content": "package core\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/star-39/moe-sticker-bot/pkg/msbimport\"\n\ttele \"gopkg.in/telebot.v3\"\n)\n\nfunc cleanUserDataAndDir(uid int64) bool {\n\tlog.WithField(\"uid\", uid).Debugln(\"Purging userdata...\")\n\t_, exist := users.data[uid]\n\tif exist {\n\t\tos.RemoveAll(users.data[uid].workDir)\n\t\tusers.mu.Lock()\n\t\tdelete(users.data, uid)\n\t\tusers.mu.Unlock()\n\t\tlog.WithField(\"uid\", uid).Debugln(\"Userdata purged from map and disk.\")\n\t\treturn true\n\t} else {\n\t\tlog.WithField(\"uid\", uid).Debugln(\"Userdata does not exisst, do nothing.\")\n\t\treturn false\n\t}\n}\n\nfunc cleanUserData(uid int64) bool {\n\tlog.WithField(\"uid\", uid).Debugln(\"Purging userdata...\")\n\t_, exist := users.data[uid]\n\tif exist {\n\t\tusers.mu.Lock()\n\t\tdelete(users.data, uid)\n\t\tusers.mu.Unlock()\n\t\tlog.WithField(\"uid\", uid).Debugln(\"Userdata purged from map.\")\n\t\treturn true\n\t} else {\n\t\tlog.WithField(\"uid\", uid).Debugln(\"Userdata does not exist, do nothing.\")\n\t\treturn false\n\t}\n}\n\nfunc initUserData(c tele.Context, command string, state string) *UserData {\n\tuid := c.Sender().ID\n\tusers.mu.Lock()\n\tctx, cancel := context.WithCancel(context.Background())\n\tsID := secHex(6)\n\tusers.data[uid] = &UserData{\n\t\tstate:     state,\n\t\tsessionID: sID,\n\t\t// userDir:       filepath.Join(dataDir, strconv.FormatInt(uid, 10)),\n\t\tworkDir:     filepath.Join(dataDir, sID),\n\t\tcommand:     command,\n\t\tlineData:    &msbimport.LineData{},\n\t\tstickerData: &StickerData{},\n\t\t// stickerManage: &StickerManage{},\n\t\tctx:    ctx,\n\t\tcancel: cancel,\n\t}\n\tusers.mu.Unlock()\n\t// Do not anitize user work directory.\n\t// os.RemoveAll(users.data[uid].userDir)\n\tos.MkdirAll(users.data[uid].workDir, 0755)\n\treturn users.data[uid]\n}\n\nfunc getState(c tele.Context) (string, string) {\n\tud, exist := users.data[c.Sender().ID]\n\tif exist {\n\t\treturn ud.command, ud.state\n\t} else {\n\t\treturn \"\", \"\"\n\t}\n}\n\nfunc checkState(next tele.HandlerFunc) tele.HandlerFunc {\n\treturn func(c tele.Context) error {\n\t\t//If bot is summoned from group chat, check command.\n\t\tif c.Chat().Type == tele.ChatGroup || c.Chat().Type == tele.ChatSuperGroup {\n\t\t\tlog.Debugf(\"User %d attempted command from group chat.\", c.Sender().ID)\n\t\t\t//For group chat, support /search only.\n\t\t\tif strings.HasPrefix(c.Text(), \"/search@\"+botName) {\n\t\t\t\treturn next(c)\n\t\t\t} else if strings.Contains(c.Text(), \"@\"+botName) {\n\t\t\t\t//has metion\n\t\t\t\treturn sendUnsupportedCommandForGroup(c)\n\t\t\t} else {\n\t\t\t\t//do nothing\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tcommand, _ := getState(c)\n\t\tif command == \"\" {\n\t\t\tlog.Debugf(\"User %d entering command with message: %s\", c.Sender().ID, c.Message().Text)\n\t\t\treturn next(c)\n\t\t} else {\n\t\t\tlog.Debugf(\"User %d already in command: %v\", c.Sender().ID, command)\n\t\t\treturn sendInStateWarning(c)\n\t\t}\n\t}\n}\n\nfunc setState(c tele.Context, state string) {\n\tif c == nil {\n\t\treturn\n\t}\n\tud, ok := users.data[c.Sender().ID]\n\tif !ok {\n\t\treturn\n\t}\n\tud.state = state\n}\n\n// func setCommand(c tele.Context, command string) {\n// \tuid := c.Sender().ID\n// \tusers.data[uid].command = command\n// }\n"
  },
  {
    "path": "core/util.go",
    "content": "package core\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"hash/crc32\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/star-39/moe-sticker-bot/pkg/msbimport\"\n\ttele \"gopkg.in/telebot.v3\"\n\t\"mvdan.cc/xurls/v2\"\n)\n\nvar regexAlphanum = regexp.MustCompile(`[a-zA-Z0-9_]+`)\n\n// var httpClient = &http.Client{\n// \tTimeout: 5 * time.Second,\n// }\n\nfunc checkTitle(t string) bool {\n\tif len(t) > 128 || len(t) < 1 {\n\t\treturn false\n\t} else {\n\t\treturn true\n\t}\n}\n\nfunc checkID(s string) bool {\n\tmaxL := 64 - len(botName)\n\tif len(s) < 1 || len(s) > maxL {\n\t\treturn false\n\t}\n\tif _, err := strconv.Atoi(s[:1]); err == nil {\n\t\treturn false\n\t}\n\tif strings.Contains(s, \"__\") {\n\t\treturn false\n\t}\n\tif strings.Contains(s, \" \") {\n\t\treturn false\n\t}\n\t//Telegram does not allow sticker name having the word \"telegram\"\n\tif strings.Contains(s, \"telegram\") {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc secHex(n int) string {\n\tbytes := make([]byte, n)\n\trand.Read(bytes)\n\treturn hex.EncodeToString(bytes)\n}\n\n// func secNum(n int) string {\n// \tnumbers := \"\"\n// \tfor i := 0; i < n; i++ {\n// \t\trandInt, _ := rand.Int(rand.Reader, big.NewInt(10))\n// \t\tnumbers += randInt.String()\n// \t}\n// \treturn numbers\n// }\n\nfunc findLink(s string) string {\n\trx := xurls.Strict()\n\treturn rx.FindString(s)\n}\n\nfunc findLinkWithType(s string) (string, string) {\n\trx := xurls.Strict()\n\tlink := rx.FindString(s)\n\tif link == \"\" {\n\t\treturn \"\", \"\"\n\t}\n\n\tu, _ := url.Parse(link)\n\thost := u.Host\n\n\tif host == \"t.me\" {\n\t\thost = LINK_TG\n\t} else if strings.HasSuffix(host, \"line.me\") {\n\t\thost = LINK_IMPORT\n\t} else if strings.HasSuffix(host, \"kakao.com\") {\n\t\thost = LINK_IMPORT\n\t}\n\n\tlog.Debugf(\"link found within findLinkWithType: link=%s, host=%s\", link, host)\n\treturn link, host\n}\n\nfunc findEmojis(s string) string {\n\tout, err := exec.Command(\"msb_emoji.py\", \"string\", s).Output()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn string(out)\n}\n\nfunc findEmojiList(s string) []string {\n\tout, err := exec.Command(\"msb_emoji.py\", \"json\", s).Output()\n\tif err != nil {\n\t\treturn []string{}\n\t}\n\tlist := []string{}\n\tjson.Unmarshal(out, &list)\n\treturn list\n}\n\nfunc stripEmoji(s string) string {\n\tout, err := exec.Command(\"msb_emoji.py\", \"text\", s).Output()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn string(out)\n}\n\nfunc sanitizeCallback(next tele.HandlerFunc) tele.HandlerFunc {\n\treturn func(c tele.Context) error {\n\t\tlog.Debug(\"Sanitizing callback data...\")\n\t\tc.Callback().Data = regexAlphanum.FindString(c.Callback().Data)\n\n\t\tlog.Debugln(\"now:\", c.Callback().Data)\n\t\treturn next(c)\n\t}\n}\nfunc autoRespond(next tele.HandlerFunc) tele.HandlerFunc {\n\treturn func(c tele.Context) error {\n\t\tif c.Callback() != nil {\n\t\t\tdefer c.Respond()\n\t\t}\n\t\treturn next(c)\n\t}\n}\n\nfunc escapeTagMark(s string) string {\n\ts = strings.ReplaceAll(s, \"<\", \"＜\")\n\ts = strings.ReplaceAll(s, \">\", \"＞\")\n\treturn s\n}\n\nfunc getSIDFromMessage(m *tele.Message) string {\n\tif m.Sticker != nil {\n\t\treturn m.Sticker.SetName\n\t}\n\n\tlink := findLink(m.Text)\n\treturn path.Base(link)\n}\n\nfunc retrieveSSDetails(c tele.Context, id string, sd *StickerData) error {\n\tss, err := c.Bot().StickerSet(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsd.stickerSet = ss\n\tsd.title = ss.Title\n\tsd.id = ss.Name\n\tsd.cAmount = len(ss.Stickers)\n\tsd.stickerSetType = ss.Type\n\tif ss.Type == tele.StickerCustomEmoji {\n\t\tsd.isCustomEmoji = true\n\t}\n\treturn nil\n}\n\nfunc GetUd(uidS string) (*UserData, error) {\n\tuid, err := strconv.ParseInt(uidS, 10, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tud, ok := users.data[uid]\n\tif ok {\n\t\treturn ud, nil\n\t} else {\n\t\treturn nil, errors.New(\"no such user in state\")\n\t}\n}\n\nfunc sliceMove[T any](oldIndex int, newIndex int, slice []T) []T {\n\torig := slice\n\telement := slice[oldIndex]\n\n\tif oldIndex > newIndex {\n\t\tif len(slice)-1 == oldIndex {\n\t\t\tslice = slice[0 : len(slice)-1]\n\t\t} else {\n\t\t\tslice = append(slice[0:oldIndex], slice[oldIndex+1:]...)\n\t\t}\n\t\tslice = append(slice[:newIndex], append([]T{element}, slice[newIndex:]...)...)\n\t} else if oldIndex < newIndex {\n\t\tslice = append(slice[0:oldIndex], slice[oldIndex+1:]...)\n\t\tif newIndex != len(slice) {\n\t\t\tnewIndex = newIndex + 1\n\t\t}\n\t\tslice = append(slice[:newIndex], append([]T{element}, slice[newIndex:]...)...)\n\t} else {\n\t\treturn orig\n\t}\n\treturn slice\n}\n\nfunc chunkSlice(slice []string, chunkSize int) [][]string {\n\tvar chunks [][]string\n\tfor {\n\t\tif len(slice) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tif len(slice) < chunkSize {\n\t\t\tchunkSize = len(slice)\n\t\t}\n\n\t\tchunks = append(chunks, slice[0:chunkSize])\n\t\tslice = slice[chunkSize:]\n\t}\n\treturn chunks\n}\n\nfunc compCRC32(f1 string, f2 string) bool {\n\tfb1, err := os.ReadFile(f1)\n\tif err != nil {\n\t\treturn false\n\t}\n\tfb2, err := os.ReadFile(f2)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tc1 := crc32.ChecksumIEEE(fb1)\n\tc2 := crc32.ChecksumIEEE(fb2)\n\tlog.Debugf(\"File:%s, C:%v\", f1, c1)\n\tlog.Debugf(\"File:%s, C:%v\", f2, c2)\n\n\tif c1 == c2 {\n\t\treturn true\n\t} else {\n\t\treturn false\n\t}\n}\n\n// func hashCRC64(s string) string {\n// \th := crc64.New(crc64.MakeTable(crc64.ISO))\n// \th.Write([]byte(s))\n// \tcsum := fmt.Sprintf(\"%x\", h.Sum(nil))\n// \treturn csum\n// }\n\nfunc checkGnerateSIDFromLID(ld *msbimport.LineData) string {\n\tid := ld.Id\n\tid = strings.ReplaceAll(id, \"-\", \"_\")\n\tid = strings.ReplaceAll(id, \"__\", \"_\")\n\n\ts := ld.Store + id + secHex(2) + \"_by_\" + botName\n\n\tif len(s) > 64 {\n\t\tlog.Debugln(\"id too long:\", len(s))\n\t\textra := len(s) - 64\n\t\tid = id[:len(id)-extra]\n\t\ts = ld.Store + id + secHex(2) + \"_by_\" + botName\n\t\ts = strings.ReplaceAll(s, \"__\", \"_\")\n\t\tlog.Debugln(\"Shortend id to:\", s)\n\t}\n\n\treturn s\n}\n\n// // Local bot api returns a absolute path in FilePath.\n// // We need to separate \"real\" api server and local api server.\n// // We move the file from api server to target location.\n// // Be careful, this does not work when crossing mount points.\n// func teleDownload(tf *tele.File, f string) error {\n// \t// if msbconf.BotApiAddr != \"\" {\n// \t// \ttf2, err := b.FileByID(tf.FileID)\n// \t// \tif err != nil {\n// \t// \t\treturn err\n// \t// \t}\n// \t// \terr = os.Rename(tf2.FilePath, f)\n// \t// \tif err != nil {\n// \t// \t\texec.Command(\"cp\", tf2.FilePath, f).CombinedOutput()\n// \t// \t}\n// \t// \treturn os.Chmod(f, 0644)\n// \t// } else {\n// \treturn b.Download(tf, f)\n// \t// }\n// }\n\n// To comply with new InputSticker requirement on format,\n// guess format based on file extension.\nfunc guessInputStickerFormat(f string) string {\n\tif strings.HasSuffix(f, \".webm\") {\n\t\treturn \"video\"\n\t} else {\n\t\treturn \"static\"\n\t}\n}\n"
  },
  {
    "path": "core/webapp.go",
    "content": "package core\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/panjf2000/ants/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n\ttele \"gopkg.in/telebot.v3\"\n)\n\nfunc InitWebAppServer() {\n\tgin.SetMode(gin.ReleaseMode)\n\tr := gin.Default()\n\n\tu, err := url.Parse(msbconf.WebappUrl)\n\tif err != nil {\n\t\tlog.Error(\"Failed parsing WebApp URL! Consider disable --webapp ?\")\n\t\tlog.Fatalln(err.Error())\n\t}\n\tp := u.Path\n\n\twebappApi := r.Group(path.Join(p, \"api\"))\n\t{\n\t\t//Group: /webapp/api\n\t\twebappApi.POST(\"/initData\", apiInitData)\n\t\twebappApi.GET(\"/ss\", apiSS)\n\t\twebappApi.POST(\"/edit/result\", apiEditResult)\n\t\twebappApi.POST(\"/edit/move\", apiEditMove)\n\t\twebappApi.GET(\"/export\", apiExport)\n\t}\n\n\tgo func() {\n\t\terr := r.Run(msbconf.WebappApiListenAddr)\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"WebApp: Gin Run failed! Check your addr or disable webapp.\\n\", err)\n\t\t}\n\t\tlog.Infoln(\"WebApp: Listening on \", msbconf.WebappApiListenAddr)\n\t}()\n}\n\nfunc apiExport(c *gin.Context) {\n\tsn := c.Query(\"sn\")\n\tqid := c.Query(\"qid\")\n\thex := c.Query(\"hex\")\n\tdn := c.Query(\"dn\")\n\turl := fmt.Sprintf(\"msb://app/export/%s/?dn=%s&qid=%s&hex=%s\", sn, dn, qid, hex)\n\tc.Redirect(http.StatusFound, url)\n}\n\ntype webappStickerSet struct {\n\t//Sticker objects\n\tSS []webappStickerObject `json:\"ss\"`\n\t//StickerSet Name\n\tSSName string `json:\"ssname\"`\n\t//StickerSet Title\n\tSSTitle string `json:\"sstitle\"`\n\t//StickerSet PNG Thumbnail\n\tSSThumb string `json:\"ssthumb\"`\n\t//Is Animated WebP\n\tAnimated bool `json:\"animated\"`\n\tAmount   int  `json:\"amount\"`\n\t//Indicates that all sticker files are ready\n\tReady bool `json:\"ready\"`\n}\n\ntype webappStickerObject struct {\n\t//Sticker index with offset of +1\n\tId int `json:\"id\"`\n\t//Sticker emojis.\n\tEmoji string `json:\"emoji\"`\n\t//Sticker emoji changed on front-end.\n\tEmojiChanged bool `json:\"emoji_changed\"`\n\t//Sticker file path on server.\n\tFilePath string `json:\"file_path\"`\n\t//Sticker file ID\n\tFileID string `json:\"file_id\"`\n\t//Sticker unique ID\n\tUniqueID string `json:\"unique_id\"`\n\t//URL of sticker image.\n\tSurl string `json:\"surl\"`\n}\n\n// GET <- ?uid&qid&sn&cmd\n// -------------------------------------------\n// -> [webappStickerObject, ...]\n// -------------------------------------------\n// id starts from 1 !!!!\n// surl might be 404 when preparing stickers.\nfunc apiSS(c *gin.Context) {\n\tcmd := c.Query(\"cmd\")\n\tsn := c.Query(\"sn\")\n\tuid := c.Query(\"uid\")\n\tqid := c.Query(\"qid\")\n\thex := c.Query(\"hex\")\n\tvar ss *tele.StickerSet\n\tvar err error\n\n\tswitch cmd {\n\tcase \"edit\":\n\t\tud, err := checkGetUd(uid, qid)\n\t\tif err != nil {\n\t\t\tc.String(http.StatusBadRequest, err.Error())\n\t\t\treturn\n\t\t}\n\t\t// Refresh SS data since it might already changed.\n\t\tretrieveSSDetails(ud.lastContext, ud.stickerData.id, ud.stickerData)\n\t\tss = ud.stickerData.stickerSet\n\tcase \"export\":\n\t\tif sn == \"\" || qid == \"\" {\n\t\t\tc.String(http.StatusBadRequest, \"no_sn_or_qid\")\n\t\t\treturn\n\t\t}\n\t\tss, err = b.StickerSet(sn)\n\t\tif err != nil {\n\t\t\tc.String(http.StatusBadRequest, \"bad_sn\")\n\t\t\treturn\n\t\t}\n\tdefault:\n\t\tc.String(http.StatusBadRequest, \"no_cmd\")\n\t\treturn\n\t}\n\n\twss := webappStickerSet{\n\t\tSSTitle: ss.Title,\n\t\tSSName:  ss.Name,\n\t}\n\tsl := []webappStickerObject{}\n\tready := true\n\tfor i, s := range ss.Stickers {\n\t\tvar surl string\n\t\tvar fpath string\n\t\tif s.Video {\n\t\t\tfpath = filepath.Join(msbconf.WebappDataDir, hex, s.SetName, s.UniqueID+\".webm\")\n\t\t} else {\n\t\t\tfpath = filepath.Join(msbconf.WebappDataDir, hex, s.SetName, s.UniqueID+\".webp\")\n\t\t}\n\t\tsurl, _ = url.JoinPath(msbconf.WebappUrl, \"data\", hex, s.SetName, s.UniqueID+\".webp\")\n\t\tsl = append(sl, webappStickerObject{\n\t\t\tId:       i + 1,\n\t\t\tEmoji:    s.Emoji,\n\t\t\tSurl:     surl,\n\t\t\tUniqueID: s.UniqueID,\n\t\t\tFileID:   s.FileID,\n\t\t\tFilePath: fpath,\n\t\t})\n\t\tif i == 0 {\n\t\t\twss.SSThumb, _ = url.JoinPath(msbconf.WebappUrl, \"data\", hex, s.SetName, s.UniqueID+\".png\")\n\t\t}\n\t\tif st, _ := os.Stat(fpath); st == nil {\n\t\t\tready = false\n\t\t}\n\t}\n\twss.SS = sl\n\twss.Ready = ready\n\n\tjsonWSS, err := json.Marshal(wss)\n\tif err != nil {\n\t\tlog.Errorln(\"json marshal jsonWSS in apiSS error!\")\n\t\tc.String(http.StatusInternalServerError, \"json marshal jsonWSS in apiSS error!\")\n\t\treturn\n\t}\n\tc.String(http.StatusOK, string(jsonWSS))\n}\n\n// <- ?qid&qid&sha256sum  [{\"index\", \"emoji\", \"surl\"}, ...]\n// -------------------------------------------\n// -> STATUS\nfunc apiEditResult(c *gin.Context) {\n\tuid := c.Query(\"uid\")\n\tqid := c.Query(\"qid\")\n\tbody, _ := io.ReadAll(c.Request.Body)\n\t// if !validateSHA256(body, sum) {\n\t// \tc.String(http.StatusBadRequest, \"bad result csum!\")\n\t// \treturn\n\t// }\n\tif string(body) == \"\" {\n\t\t//user did nothing\n\t\treturn\n\t}\n\tso := []webappStickerObject{}\n\terr := json.Unmarshal(body, &so)\n\tif err != nil {\n\t\tc.String(http.StatusBadRequest, \"bad_json\")\n\t\treturn\n\t}\n\tud, err := checkGetUd(uid, qid)\n\tif err != nil {\n\t\tc.String(http.StatusBadRequest, err.Error())\n\t\treturn\n\t}\n\tif ud.state == ST_PROCESSING {\n\t\tc.String(http.StatusOK, \"already processing...\")\n\t\treturn\n\t}\n\n\tlog.Debugln(so)\n\n\tc.String(http.StatusOK, \"\")\n\tud.udSetState(ST_PROCESSING)\n\n\tgo func() {\n\t\terr := commitEmojiChange(ud, so)\n\t\tif err != nil {\n\t\t\tsendFatalError(err, ud.lastContext)\n\t\t\tendManageSession(ud.lastContext)\n\t\t\tendSession(ud.lastContext)\n\t\t}\n\t}()\n}\n\nfunc commitEmojiChange(ud *UserData, so []webappStickerObject) error {\n\t//Wait for previous jobs to be done.\n\twaitTime := 0\n\tfor ud.webAppWorkerPool.Waiting() > 0 {\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\twaitTime++\n\t\tif waitTime > 20 {\n\t\t\tbreak\n\t\t}\n\t}\n\tud.webAppWorkerPool.ReleaseTimeout(10 * time.Second)\n\n\t//copy slice\n\tss := ud.stickerData.stickerSet.Stickers\n\tfor i, s := range so {\n\t\tif s.EmojiChanged {\n\t\t\toldEmoji := findEmojis(ss[i].Emoji)\n\t\t\tnewEmoji := findEmojis(s.Emoji)\n\t\t\tnewEmojiList := findEmojiList(s.Emoji)\n\t\t\tif newEmoji == \"\" || newEmoji == oldEmoji {\n\t\t\t\tlog.Info(\"webapp: ignored one invalid emoji.\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Debugln(\"Old:\", i, s.Emoji, s.FileID)\n\t\t\tlog.Debugln(\"New\", i, newEmoji)\n\n\t\t\terr := editStickerEmoji(newEmojiList, s.FileID, ud)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tsendSEditOK(ud.lastContext)\n\treturn nil\n}\n\n// <- ?uid&qid POST_FORM:{\"oldIndex\", \"newIndex\"}\n// -------------------------------------------\n// -> STATUS\nfunc apiEditMove(c *gin.Context) {\n\tuid := c.Query(\"uid\")\n\tqid := c.Query(\"qid\")\n\toldIndex, _ := strconv.Atoi(c.PostForm(\"oldIndex\"))\n\tnewIndex, _ := strconv.Atoi(c.PostForm(\"newIndex\"))\n\tud, err := checkGetUd(uid, qid)\n\tif err != nil {\n\t\tc.String(http.StatusBadRequest, err.Error())\n\t\treturn\n\t}\n\tsmo := &StickerMoveObject{\n\t\twg:       sync.WaitGroup{},\n\t\tsd:       ud.stickerData,\n\t\toldIndex: oldIndex,\n\t\tnewIndex: newIndex,\n\t}\n\tsmo.wg.Add(1)\n\tud.webAppWorkerPool.Invoke(smo)\n\tsmo.wg.Wait()\n\tif smo.err != nil {\n\t\tc.String(http.StatusInternalServerError, smo.err.Error())\n\t\treturn\n\t}\n}\n\nfunc apiInitData(c *gin.Context) {\n\t//We must verify the initData before using it\n\tqueryID := c.PostForm(\"query_id\")\n\tauthDate := c.PostForm(\"auth_date\")\n\tuser := c.PostForm(\"user\")\n\thash := c.PostForm(\"hash\")\n\tdataCheckString := strings.Join([]string{\n\t\t\"auth_date=\" + authDate,\n\t\t\"query_id=\" + queryID,\n\t\t\"user=\" + user}, \"\\n\")\n\tif !validateHMAC(dataCheckString, hash) {\n\t\tlog.Warning(\"WebApp DCS HMAC failed, corrupt or attack?\")\n\t\tc.String(http.StatusBadRequest, \"data_check_string HMAC validation failed!!\")\n\t\treturn\n\t}\n\tlog.Debug(\"WebApp initData DCS HMAC OK.\")\n\n\tinitWebAppRequest(c)\n}\n\nfunc initWebAppRequest(c *gin.Context) {\n\tuser := c.PostForm(\"user\")\n\tqueryID := c.PostForm(\"query_id\")\n\tcmd := c.Query(\"cmd\")\n\tcmd = path.Base(cmd)\n\twebAppUser := &WebAppUser{}\n\terr := json.Unmarshal([]byte(user), webAppUser)\n\tif err != nil {\n\t\tlog.Error(\"json unmarshal webappuser error.\")\n\t\treturn\n\t}\n\n\tswitch cmd {\n\tcase \"edit\":\n\t\tud, err := GetUd(strconv.Itoa(webAppUser.Id))\n\t\tif err != nil {\n\t\t\tc.String(http.StatusBadRequest, \"bad_state\")\n\t\t\treturn\n\t\t}\n\t\tud.webAppWorkerPool, _ = ants.NewPoolWithFunc(1, wSubmitSMove)\n\t\tud.webAppQID = queryID\n\tcase \"export\":\n\t\tsn := c.Query(\"sn\")\n\t\thex := c.Query(\"hex\")\n\t\tif sn == \"\" || hex == \"\" {\n\t\t\tc.String(http.StatusBadRequest, \"no_sn_or_hex\")\n\t\t\treturn\n\t\t}\n\t\tss, err := b.StickerSet(sn)\n\t\tif err != nil {\n\t\t\tc.String(http.StatusBadRequest, \"bad_sn\")\n\t\t\treturn\n\t\t}\n\t\t// appendSStoQIDAuthList(sn, queryID)\n\t\tprepareWebAppExportStickers(ss, hex)\n\tdefault:\n\t\tc.String(http.StatusBadRequest, \"bad_or_no_cmd\")\n\t\treturn\n\t}\n\n\tc.String(http.StatusOK, \"webapp init ok\")\n}\n\n// Telegram WebApp Regulation.\nfunc validateHMAC(dataCheckString string, hash string) bool {\n\t// This calculated secret will be used to \"decrypt\" DCS\n\th := hmac.New(sha256.New, []byte(\"WebAppData\"))\n\th.Write([]byte(msbconf.BotToken))\n\tsecretByte := h.Sum(nil)\n\n\th = hmac.New(sha256.New, secretByte)\n\th.Write([]byte(dataCheckString))\n\tdcsHash := fmt.Sprintf(\"%x\", h.Sum(nil))\n\treturn hash == dcsHash\n}\n\n// func validateSHA256(dataToCheck []byte, hash string) bool {\n// \th := sha256.New()\n// \th.Write(dataToCheck)\n// \tcsum := fmt.Sprintf(\"%x\", h.Sum(nil))\n// \treturn hash == csum\n// }\n\nfunc checkGetUd(uid string, qid string) (*UserData, error) {\n\tud, err := GetUd(uid)\n\tif err != nil {\n\t\treturn nil, errors.New(\"no such user\")\n\t}\n\tif ud.webAppQID != qid {\n\t\treturn nil, errors.New(\"qid not valid\")\n\t}\n\treturn ud, nil\n}\n\nfunc prepareWebAppEditStickers(ud *UserData) error {\n\tdest := filepath.Join(msbconf.WebappDataDir, ud.stickerData.id)\n\tos.RemoveAll(dest)\n\tos.MkdirAll(dest, 0755)\n\n\tfor _, s := range ud.stickerData.stickerSet.Stickers {\n\t\tvar f string\n\t\tif s.Video {\n\t\t\tf = filepath.Join(dest, s.UniqueID+\".webm\")\n\t\t} else {\n\t\t\tf = filepath.Join(dest, s.UniqueID+\".webp\")\n\t\t}\n\t\tobj := &StickerDownloadObject{\n\t\t\tdest:      f,\n\t\t\tsticker:   s,\n\t\t\tforWebApp: true,\n\t\t}\n\t\tobj.wg.Add(1)\n\t\tud.stickerData.sDnObjects = append(ud.stickerData.sDnObjects, obj)\n\t\tgo wpDownloadStickerSet.Invoke(obj)\n\t}\n\treturn nil\n}\n\nfunc prepareWebAppExportStickers(ss *tele.StickerSet, hex string) error {\n\tdest := filepath.Join(msbconf.WebappDataDir, hex, ss.Name)\n\t// If the user is reusing the generated link to export.\n\t// Do not re-download for every initData.\n\tstat, _ := os.Stat(dest)\n\tif stat != nil {\n\t\tmtime := stat.ModTime().Unix()\n\t\t// Less than 5 minutes, do not re-download\n\t\tif time.Now().Unix()-mtime < 600 {\n\t\t\tlog.Debug(\"prepareWebAppExportStickers: dir still fresh, don't overwrite.\")\n\t\t\treturn nil\n\t\t}\n\t}\n\tos.RemoveAll(dest)\n\tos.MkdirAll(dest, 0755)\n\n\tfor i, s := range ss.Stickers {\n\t\tvar f string\n\t\tif s.Video {\n\t\t\tf = filepath.Join(dest, s.UniqueID+\".webm\")\n\t\t} else if s.Animated {\n\t\t\tf = filepath.Join(dest, s.UniqueID+\".tgs\")\n\t\t} else {\n\t\t\tf = filepath.Join(dest, s.UniqueID+\".webp\")\n\t\t}\n\t\tobj := &StickerDownloadObject{\n\t\t\tdest:    f,\n\t\t\tsticker: s,\n\t\t\t// forWebApp:   true,\n\t\t\tforWhatsApp: true,\n\t\t}\n\t\t//Use first image to create a thumbnail image\n\t\t//for WhatsApp.\n\t\tif i == 0 {\n\t\t\tobj.forWhatsAppThumb = true\n\t\t}\n\t\tobj.wg.Add(1)\n\t\tgo wpDownloadStickerSet.Invoke(obj)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "core/workers.go",
    "content": "package core\n\nimport (\n\t\"strings\"\n\n\t\"github.com/panjf2000/ants/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/star-39/moe-sticker-bot/pkg/msbimport\"\n)\n\n// Workers pool for converting webm\nvar wpDownloadStickerSet *ants.PoolWithFunc\n\nfunc initWorkersPool() {\n\t// wpConvertWebm, _ = ants.NewPoolWithFunc(4, wConvertWebm)\n\twpDownloadStickerSet, _ = ants.NewPoolWithFunc(\n\t\t8, wDownloadStickerObject)\n}\n\n// *StickerDownloadObject\nfunc wDownloadStickerObject(i interface{}) {\n\tobj := i.(*StickerDownloadObject)\n\tdefer obj.wg.Done()\n\tlog.Debugf(\"Downloading in pool: %s -> %s\", obj.sticker.FileID, obj.dest)\n\n\tif obj.forWebApp || obj.forWhatsApp {\n\t\terr := b.Download(&obj.sticker.File, obj.dest)\n\t\tif err != nil {\n\t\t\tlog.Warnln(\"download: error downloading sticker:\", err)\n\t\t\tobj.err = err\n\t\t\treturn\n\t\t}\n\t\tif obj.forWhatsApp {\n\t\t\tif obj.sticker.Video {\n\t\t\t\tobj.err = msbimport.FFToAnimatedWebpWA(obj.dest)\n\t\t\t} else {\n\t\t\t\tobj.err = msbimport.IMToWebpWA(obj.dest)\n\t\t\t}\n\n\t\t\tif obj.forWhatsAppThumb {\n\t\t\t\tif obj.sticker.Animated {\n\t\t\t\t\tf := strings.ReplaceAll(obj.dest, \".tgs\", \".webp\")\n\t\t\t\t\tobj.err = msbimport.IMToPNGThumb(f)\n\t\t\t\t} else {\n\t\t\t\t\tobj.err = msbimport.IMToPNGThumb(obj.dest)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t//TGS set is not managable, no need to convert.\n\t\t\tif obj.sticker.Video {\n\t\t\t\tobj.err = msbimport.FFToAnimatedWebpLQ(obj.dest)\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n\n\tvar f string\n\tvar cf string\n\tvar err error\n\tif obj.sticker.Video {\n\t\tf = obj.dest + \".webm\"\n\t\terr = b.Download(&obj.sticker.File, f)\n\t\tif obj.needConvert {\n\t\t\tcf, _ = msbimport.FFToGif(f)\n\t\t}\n\t} else if obj.sticker.Animated {\n\t\tf = obj.dest + \".tgs\"\n\t\terr = b.Download(&obj.sticker.File, f)\n\t\tif obj.needConvert {\n\t\t\tcf, _ = msbimport.RlottieToGIF(f)\n\t\t}\n\t} else {\n\t\tf = obj.dest + \".webp\"\n\t\terr = b.Download(&obj.sticker.File, f)\n\t\tif obj.needConvert {\n\t\t\tcf, _ = msbimport.IMToPng(f)\n\t\t}\n\t}\n\tif err != nil {\n\t\tlog.Warnln(\"download: error downloading sticker:\", err)\n\t\tobj.err = err\n\t\treturn\n\t}\n\n\tobj.of = f\n\tobj.cf = cf\n\n}\n\n// *StickerMoveObject\nfunc wSubmitSMove(i interface{}) {\n\tobj := i.(*StickerMoveObject)\n\tdefer obj.wg.Done()\n\tsid := obj.sd.stickerSet.Stickers[obj.oldIndex].FileID\n\tlog.Debugf(\"Moving in pool %d(%s) -> %d\", obj.oldIndex, sid, obj.newIndex)\n\terr := b.SetStickerPosition(sid, obj.newIndex)\n\tif err != nil {\n\t\tlog.Errorln(\"SMove failed!!\", err)\n\t\tobj.err = err\n\t} else {\n\t\tlog.Debugf(\"Sticker move OK for %s\", obj.sd.stickerSet.Name)\n\t\tobj.sd.stickerSet.Stickers =\n\t\t\tsliceMove(obj.oldIndex, obj.newIndex, obj.sd.stickerSet.Stickers)\n\t}\n}\n"
  },
  {
    "path": "deployments/kubernetes_msb.yaml",
    "content": "# Save the output of this file and use kubectl create -f to import\n# it into Kubernetes.\n#\n# Created with podman-4.2.0\napiVersion: v1\nkind: Pod\nmetadata:\n  creationTimestamp: \"2022-12-09T17:58:22Z\"\n  labels:\n    app: p-moe-sticker-bot\n  name: p-moe-sticker-bot\nspec:\n  containers:\n  - args:\n    - nginx\n    - -g\n    - daemon off;\n    env:\n    - name: NGINX_PORT\n      value: \"443\"\n    - name: NGINX_CERT\n      value: /certs/live/YOUR_WEBSITE/fullchain.pem\n    - name: WEBAPP_ROOT\n      value: /webapp\n    - name: WEBAPP_ADDR\n      value: 127.0.0.1:3921/webapp\n    - name: NGINX_KEY\n      value: /certs/live/YOUR_WEBSITE/privkey.pem\n    image: ghcr.io/star-39/moe-sticker-bot:msb_nginx_aarch64\n    name: msbnginx\n    ports:\n    - containerPort: 443\n      hostPort: 443\n    resources: {}\n    securityContext:\n      capabilities:\n        drop:\n        - CAP_MKNOD\n        - CAP_AUDIT_WRITE\n    tty: true\n    volumeMounts:\n    - mountPath: /certs\n      name: etc-letsencrypt-host-0\n    - mountPath: /webapp/data\n      name: moe-sticker-bot-webapp-data-ed\n\n  - command:\n    - /moe-sticker-bot\n    - --bot_token=BOT_TOKEN\n    - --webapp\n    - --webapp_url\n    - https://example.com/\n    - --webapp_data_dir\n    - /webapp/data/\n    - --webapp_listen_addr\n    - 127.0.0.1:3921\n    - --use_db\n    - --db_addr\n    - 10.88.0.1:3306\n    - --db_user\n    - root\n    - --db_pass\n    - DB_ROOT_PASS\n    image: ghcr.io/star-39/moe-sticker-bot:aarch64\n    name: msb\n    resources: {}\n    securityContext:\n      capabilities:\n        drop:\n        - CAP_MKNOD\n        - CAP_AUDIT_WRITE\n    volumeMounts:\n    - mountPath: /webapp/data\n      name: moe-sticker-bot-webapp-data-ed\n\n  - args:\n    - mariadbd\n    env:\n    - name: MARIADB_ROOT_PASSWORD\n      value: DB_ROOT_PASS\n    image: docker.io/library/mariadb:10.6\n    name: msbmariadb\n    resources: {}\n    securityContext:\n      capabilities:\n        drop:\n        - CAP_MKNOD\n        - CAP_AUDIT_WRITE\n    volumeMounts:\n    - mountPath: /var/lib/mysql\n      name: moe-sticker-bot-db-pvc\n\n  hostname: p-moe-sticker-bot\n\n  volumes:\n  - hostPath:\n      path: /etc/letsencrypt\n      type: Directory\n    name: etc-letsencrypt-host-0\n  - name: moe-sticker-bot-webapp-data-ed\n    emptyDir: {}\n  - name: moe-sticker-bot-db-pvc\n    persistentVolumeClaim:\n     claimName: moe-sticker-bot-db-pvc\nstatus: {}\n\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/star-39/moe-sticker-bot\n\ngo 1.19\n\nreplace gopkg.in/telebot.v3 => github.com/star-39/telebot/v3 v3.99.9\n\nreplace github.com/armon/go-metrics => github.com/hashicorp/go-metrics v0.5.3\n\nreplace github.com/circonus-labs/circonusllhist => github.com/openhistogram/circonusllhist v0.4.0\n\nreplace gopkg.in/alecthomas/kingpin.v2 => github.com/alecthomas/kingpin/v2 v2.4.0\n\n//replace gopkg.in/telebot.v3 => /media/ssd/repos/telebot\n\nrequire (\n\tgithub.com/PuerkitoBio/goquery v1.9.2\n\tgithub.com/gin-gonic/gin v1.10.0\n\tgithub.com/go-co-op/gocron v1.37.0\n\tgithub.com/go-sql-driver/mysql v1.8.1\n\tgithub.com/panjf2000/ants/v2 v2.9.1\n\tgithub.com/sirupsen/logrus v1.9.3\n\tgopkg.in/telebot.v3 v3.2.1\n\tmvdan.cc/xurls/v2 v2.5.0\n)\n\nrequire (\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/andybalholm/cascadia v1.3.2 // indirect\n\tgithub.com/bytedance/sonic v1.11.6 // indirect\n\tgithub.com/bytedance/sonic/loader v0.1.1 // indirect\n\tgithub.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect\n\tgithub.com/chenzhuoyu/iasm v0.9.1 // indirect\n\tgithub.com/cloudwego/base64x v0.1.4 // indirect\n\tgithub.com/cloudwego/iasm v0.2.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.3 // indirect\n\tgithub.com/gin-contrib/sse v0.1.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.20.0 // indirect\n\tgithub.com/goccy/go-json v0.10.2 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.7 // indirect\n\tgithub.com/knz/go-libedit v1.10.1 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.2 // indirect\n\tgithub.com/robfig/cron/v3 v3.0.1 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.2.12 // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgolang.org/x/arch v0.8.0 // indirect\n\tgolang.org/x/crypto v0.23.0 // indirect\n\tgolang.org/x/net v0.25.0 // indirect\n\tgolang.org/x/sys v0.20.0 // indirect\n\tgolang.org/x/text v0.15.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.1 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=\ncloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=\ncloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=\ncloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=\ncloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=\ncloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=\ncloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=\ncloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=\ncloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=\ncloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=\ncloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=\ncloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=\ncloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=\ngithub.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=\ngithub.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=\ngithub.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=\ngithub.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=\ngithub.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=\ngithub.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=\ngithub.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=\ngithub.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=\ngithub.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=\ngithub.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=\ngithub.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=\ngithub.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=\ngithub.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=\ngithub.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=\ngithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=\ngithub.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=\ngithub.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=\ngithub.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=\ngithub.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=\ngithub.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=\ngithub.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=\ngithub.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=\ngithub.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=\ngithub.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=\ngithub.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=\ngithub.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=\ngithub.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=\ngithub.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=\ngithub.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=\ngithub.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=\ngithub.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=\ngithub.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=\ngithub.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=\ngithub.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=\ngithub.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=\ngithub.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=\ngithub.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=\ngithub.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=\ngithub.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=\ngithub.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=\ngithub.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=\ngithub.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=\ngithub.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=\ngithub.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=\ngithub.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=\ngithub.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=\ngithub.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-metrics v0.5.3/go.mod h1:KEjodfebIOuBYSAe/bHTm+HChmKSxAOXPBieMLYozDE=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=\ngithub.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=\ngithub.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=\ngithub.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=\ngithub.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=\ngithub.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=\ngithub.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo=\ngithub.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/leodido/go-urn v1.3.0 h1:jX8FDLfW4ThVXctBNZ+3cIWnCSnrACDV73r76dy0aQQ=\ngithub.com/leodido/go-urn v1.3.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=\ngithub.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/openhistogram/circonusllhist v0.4.0/go.mod h1:PfeYJ/RW2+Jfv3wTz0upbY2TRour/LLqIm2K2Kw5zg0=\ngithub.com/panjf2000/ants/v2 v2.9.0 h1:SztCLkVxBRigbg+vt0S5QvF5vxAbxbKt09/YfAJ0tEo=\ngithub.com/panjf2000/ants/v2 v2.9.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I=\ngithub.com/panjf2000/ants/v2 v2.9.1 h1:Q5vh5xohbsZXGcD6hhszzGqB7jSSc2/CRr3QKIga8Kw=\ngithub.com/panjf2000/ants/v2 v2.9.1/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=\ngithub.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=\ngithub.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=\ngithub.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=\ngithub.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=\ngithub.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=\ngithub.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=\ngithub.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=\ngithub.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=\ngithub.com/star-39/telebot/v3 v3.99.7 h1:hkzOqAwksn7yMo7LxyhmOSqwBxo3PFk7vRbhGcArhcE=\ngithub.com/star-39/telebot/v3 v3.99.7/go.mod h1:Ao7cUAFKISN5qXdy7s32HTjE8yJfKSV0FDhaW3+Qw74=\ngithub.com/star-39/telebot/v3 v3.99.8 h1:/AOYaKvaUsDl7wVle5PIu/+HLjeapobiAhSF8c8RCyc=\ngithub.com/star-39/telebot/v3 v3.99.8/go.mod h1:Ao7cUAFKISN5qXdy7s32HTjE8yJfKSV0FDhaW3+Qw74=\ngithub.com/star-39/telebot/v3 v3.99.9 h1:0AzpYI796wpcEw4+aT0dHoFB/x0LStdqfpggVyZPoOE=\ngithub.com/star-39/telebot/v3 v3.99.9/go.mod h1:Ao7cUAFKISN5qXdy7s32HTjE8yJfKSV0FDhaW3+Qw74=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=\ngithub.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\ngithub.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=\ngo.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=\ngo.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=\ngolang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=\ngolang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=\ngolang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=\ngolang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=\ngolang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=\ngolang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=\ngolang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=\ngolang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=\ngolang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=\ngolang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=\ngolang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=\ngolang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=\ngoogle.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=\ngoogle.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=\ngoogle.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=\ngoogle.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=\ngoogle.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=\ngoogle.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=\ngoogle.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=\ngoogle.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=\ngoogle.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=\ngoogle.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=\ngoogle.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=\ngoogle.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=\ngoogle.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=\ngoogle.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=\ngoogle.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=\ngoogle.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=\ngoogle.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=\ngoogle.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=\ngoogle.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=\ngoogle.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngoogle.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=\ngoogle.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nmvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=\nmvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=\nnullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\n"
  },
  {
    "path": "pkg/msbimport/README.md",
    "content": "# Moe-Sticker-Bot Import Component (msbimport)\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/star-39/moe-sticker-bot/pkg/msbimport.svg)](https://pkg.go.dev/github.com/star-39/moe-sticker-bot/pkg/msbimport)\n\n## Description\nThis package is intended to fetch, parse, download and convert LINE and KakaoTalk Stickers from share link.\n\nIt is designed to be able to operate independentaly from moe-sticker-bot core so third party apps can also utilize this package.\n\n此套件或CLI用於解析LINE/Kakaotalk貼圖的分享連結並下載和轉換。\n\n此套件可獨立於moe-sticker-bot使用， 第三方App可以輕鬆利用此套件或CLI處理複雜貼圖。\n\n\n## CLI Usage/終端機程式使用\nSource code of CLI is on: [/moe-sticker-bot/cmd/msbimport](https://github.com/star-39/moe-sticker-bot/tree/master/cmd/msbimport)\n\nDownload `msbimport`： 下載`msbimport`： https://github.com/star-39/moe-sticker-bot/releases\n\nInstall dependencies:\n```bash\n# For Fedora / RHEL / CentOS etc. (Requires RPM Fusion)\ndnf install ImageMagick libwebp bsdtar curl ffmpeg gifsicle\n# For Debian / Ubuntu etc.\napt install imagemagick libarchive-tools curl ffmpeg gifsicle\n# For Arch\npacman -S install ffmpeg imagemagick curl libarchive gifsicle bsdtar\n# For macOS\nbrew install imagemagick ffmpeg curl bsdtar gifsicle\n\ninstall tools/msb_kakao_decrypt.py /usr/local/bin/\ninstall tools/msb_rlottie.py /usr/local/bin/\n```\n\n```bash\nmsbimport --help\nUsage of ./msbimport:\n  -convert\n    \tConvert to Telegram format(WEBP/WEBM) 轉換為Telegram格式。\n  -dir string\n    \tWhere to put sticker files. 指定存放貼圖檔的資料夾。\n  -json\n    \tOutput JSON serialized LineData, useful when integrating with other apps. 列印LineData為JSON。\n  -link string\n    \tImport link(LINE/kakao) 分享連結。\n  -log_level string\n    \tLog level (default \"debug\")\n        \nmsbimport --link https://store.line.me/stickershop/product/27286\n\nmsbimport --link https://store.line.me/stickershop/product/27286 --convert --json\n\n```\n\n\n\n## API Usage\n\nA typical workflow is to call `parseImportLink` then `prepareImportStickers`.\n\n```\ngo get -u https://github.com/star-39/moe-sticker-bot\n```\n\n\n```go\nimport \"github.com/star-39/moe-sticker-bot/pkg/msbimport\"\n\n//Create a context, which can be used to interrupt the process.\nctx, _ := context.WithCancel(context.Background())\n\n//Create a empty LineData struct pointer.\nld := &msbimport.LineData{}\n\n//LineData will be parsed to ld.\nwarn, err := msbimport.ParseImportLink(\"https://store.line.me/stickershop/product/27286\", ld)\nif err != nil {\n    //handle error here.\n}\nif warn != \"\" {\n    //handle warning message here.\n}\n\nerr = msbimport.PrepareImportStickers(ctx, ld, \"./\", false)\nif err != nil {\n    //handle error here.\n}\n\n//If I18n title is needed(LINE only), TitleWg must be waited.\nld.TitleWg.Wait()\nprintln(ld.I18nTitles)\n\nfor _, lf := range ld.Files {\n    //Each file has its own waitgroup and musted be waited.\n    lf.Wg.Wait()\n    if lf.CError != nil {\n        //hanlde sticker error here.\n    }\n    println(lf.OriginalFile)\n    println(lf.ConvertedFile)\n    //...\n}\n\n//Your stickers files will appear in the work dir you specified.\n```\n\n\n## License\nGPL v3 License.\n\nSource code of this package MUST ALWAYS be disclosed no matter what use case is, \n\nand source code referring to this package MUST ALSO be discolsed and share the same GPL v3 License.\n"
  },
  {
    "path": "pkg/msbimport/convert.go",
    "content": "package msbimport\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar FFMPEG_BIN = \"ffmpeg\"\nvar BSDTAR_BIN = \"bsdtar\"\nvar CONVERT_BIN = \"convert\"\nvar IDENTIFY_BIN = \"identify\"\nvar CONVERT_ARGS []string\nvar IDENTIFY_ARGS []string\n\nconst (\n\tFORMAT_TG_REGULAR_STATIC   = \"tg_reg_static\"\n\tFORMAT_TG_EMOJI_STATIC     = \"tg_emoji_static\"\n\tFORMAT_TG_REGULAR_ANIMATED = \"tg_reg_ani\"\n\tFORMAT_TG_EMOJI_ANIMATED   = \"tg_emoji_ani\"\n)\n\n// See: http://en.wikipedia.org/wiki/Binary_prefix\nconst (\n\t// Decimal\n\tKB = 1000\n\tMB = 1000 * KB\n\tGB = 1000 * MB\n\tTB = 1000 * GB\n\tPB = 1000 * TB\n\n\t// Binary\n\tKiB = 1024\n\tMiB = 1024 * KiB\n\tGiB = 1024 * MiB\n\tTiB = 1024 * GiB\n\tPiB = 1024 * TiB\n)\n\n// Should call before using functions in this package.\n// Otherwise, defaults to Linux environment.\n// This function also call CheckDeps to check if executables.\nfunc InitConvert() {\n\tswitch runtime.GOOS {\n\tcase \"linux\":\n\t\tCONVERT_BIN = \"convert\"\n\tdefault:\n\t\tCONVERT_BIN = \"magick\"\n\t\tIDENTIFY_BIN = \"magick\"\n\t\tCONVERT_ARGS = []string{\"convert\"}\n\t\tIDENTIFY_ARGS = []string{\"identify\"}\n\t}\n\tunfoundBins := CheckDeps()\n\tif len(unfoundBins) != 0 {\n\t\tlog.Warning(\"Following required executables not found!:\")\n\t\tlog.Warnln(strings.Join(unfoundBins, \"  \"))\n\t\tlog.Warning(\"Please install missing executables to your PATH, or some features will not work!\")\n\t}\n}\n\n// Check if required dependencies exist and return a string slice\n// containing binaries that are not found in PATH.\nfunc CheckDeps() []string {\n\tunfoundBins := []string{}\n\n\tif _, err := exec.LookPath(FFMPEG_BIN); err != nil {\n\t\tunfoundBins = append(unfoundBins, FFMPEG_BIN)\n\t}\n\tif _, err := exec.LookPath(BSDTAR_BIN); err != nil {\n\t\tunfoundBins = append(unfoundBins, BSDTAR_BIN)\n\t}\n\tif _, err := exec.LookPath(CONVERT_BIN); err != nil {\n\t\tunfoundBins = append(unfoundBins, CONVERT_BIN)\n\t}\n\tif _, err := exec.LookPath(\"exiv2\"); err != nil {\n\t\tunfoundBins = append(unfoundBins, \"exiv2\")\n\t}\n\tif _, err := exec.LookPath(\"gifsicle\"); err != nil {\n\t\tunfoundBins = append(unfoundBins, \"gifsicle\")\n\t}\n\treturn unfoundBins\n}\n\n// Convert any image to static WEBP image, for Telegram use.\n// `format` takes either FORMAT_TG_REGULAR_STATIC or FORMAT_TG_EMOJI_STATIC\nfunc IMToWebpTGStatic(f string, isCustomEmoji bool) (string, error) {\n\tpathOut := f + \".webp\"\n\tbin := CONVERT_BIN\n\targs := CONVERT_ARGS\n\tif isCustomEmoji {\n\t\targs = append(args, \"-resize\", \"100x100\", \"-gravity\", \"center\", \"-extent\", \"100x100\", \"-background\", \"none\")\n\t} else {\n\t\targs = append(args, \"-resize\", \"512x512\")\n\t}\n\targs = append(args, \"-filter\", \"Lanczos\", \"-define\", \"webp:lossless=true\", f+\"[0]\", pathOut)\n\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err != nil {\n\t\tlog.Warnln(\"IMToWebpTGRegular ERROR:\", string(out))\n\t\treturn \"\", err\n\t}\n\n\tst, err := os.Stat(pathOut)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// 100x100 should never exceed 255KIB, no need for extra check.\n\tif st.Size() > 255*KiB {\n\t\targs := CONVERT_ARGS\n\t\targs = append(args, \"-resize\", \"512x512\", \"-filter\", \"Lanczos\", f+\"[0]\", pathOut)\n\t\texec.Command(bin, args...).CombinedOutput()\n\t}\n\n\treturn pathOut, err\n}\n\n// Convert image to static Webp for Whatsapp, size limit is 100KiB.\nfunc IMToWebpWA(f string) error {\n\tpathOut := f\n\tbin := CONVERT_BIN\n\targs := CONVERT_ARGS\n\tqualities := []string{\"75\", \"50\"}\n\tfor _, q := range qualities {\n\t\targs = append(args, \"-define\", \"webp:quality=\"+q,\n\t\t\t\"-resize\", \"512x512\", \"-gravity\", \"center\", \"-extent\", \"512x512\",\n\t\t\t\"-background\", \"none\", f+\"[0]\", pathOut)\n\n\t\tout, err := exec.Command(bin, args...).CombinedOutput()\n\t\tif err != nil {\n\t\t\tlog.Warnln(\"imToWebp ERROR:\", string(out))\n\t\t\treturn err\n\t\t}\n\t\tst, err := os.Stat(pathOut)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif st.Size() > 100*KiB {\n\t\t\tcontinue\n\t\t} else {\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn errors.New(\"bad webp\")\n}\n\nfunc IMToPng(f string) (string, error) {\n\tpathOut := f + \".png\"\n\tbin := CONVERT_BIN\n\targs := CONVERT_ARGS\n\targs = append(args, f, pathOut)\n\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err != nil {\n\t\tlog.Warnln(\"imToPng ERROR:\", string(out))\n\t\treturn \"\", err\n\t}\n\treturn pathOut, err\n}\n\nfunc IMToApng(f string) (string, error) {\n\tpathOut := f + \".apng\"\n\tbin := CONVERT_BIN\n\targs := CONVERT_ARGS\n\targs = append(args, f, pathOut)\n\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err != nil {\n\t\tlog.Warnln(\"imToApng ERROR:\", string(out))\n\t\treturn \"\", err\n\t}\n\treturn pathOut, err\n}\n\n// If the source is IMAGE, convert to WEBP,\n// If the source is VIDEO, convert to WEBM\nfunc ConverMediaToTGStickerSmart(f string, isCustomEmoji bool) (string, error) {\n\tvar isVideo bool\n\t//Determine whether the media is Video or Image by counting frames.\n\tidentifyBin := IDENTIFY_BIN\n\tidentifyArgs := IDENTIFY_ARGS\n\tidentifyArgs = append(identifyArgs, \"-format\", \"%n\", f)\n\tidentifyOut, err := exec.Command(identifyBin, identifyArgs...).CombinedOutput()\n\tif err != nil {\n\t\tlog.Warnln(\"ConverMediaToTGStickerSmart ERROR:\", string(identifyOut))\n\t\treturn \"\", err\n\t}\n\t//IM might get buggy and return insane frame count causing overflow.\n\t//Trim it.\n\tidentifyOutString := string(identifyOut)\n\tif len(identifyOutString) > 5 {\n\t\tidentifyOutString = identifyOutString[:3]\n\t}\n\n\tframeCount, err := strconv.Atoi(identifyOutString)\n\tif err != nil {\n\t\tlog.Warnln(\"ConverMediaToTGStickerSmart Atoi ERROR:\", err)\n\t\treturn \"\", err\n\t}\n\n\tif frameCount > 1 {\n\t\tisVideo = true\n\t} else if frameCount == 0 {\n\t\tlog.Warnln(\"ConverMediaToTGStickerSmart ERROR: Frame count is zero.\")\n\t\treturn \"\", errors.New(\"frame count is zero\")\n\t}\n\n\tif isVideo {\n\t\treturn FFToWebmTGVideo(f, isCustomEmoji)\n\t} else {\n\t\treturn IMToWebpTGStatic(f, isCustomEmoji)\n\t}\n}\n\nfunc FFToWebmTGVideo(f string, isCustomEmoji bool) (string, error) {\n\tpathOut := f + \".webm\"\n\tbin := FFMPEG_BIN\n\tbaseargs := []string{}\n\tbaseargs = append(baseargs, \"-hide_banner\", \"-i\", f)\n\tif isCustomEmoji {\n\t\tbaseargs = append(baseargs, \"-vf\", \"scale=100:100:force_original_aspect_ratio=decrease\")\n\t} else {\n\t\tbaseargs = append(baseargs, \"-vf\", \"scale=512:512:force_original_aspect_ratio=decrease\")\n\t}\n\tbaseargs = append(baseargs, \"-pix_fmt\", \"yuva420p\", \"-c:v\", \"libvpx-vp9\", \"-cpu-used\", \"5\")\n\n\tfor rc := 0; rc < 4; rc++ {\n\t\trcargs := []string{}\n\t\tswitch rc {\n\t\tcase 0:\n\t\t\trcargs = []string{\"-minrate\", \"50k\", \"-b:v\", \"350k\", \"-maxrate\", \"450k\"}\n\t\tcase 1:\n\t\t\trcargs = []string{\"-minrate\", \"50k\", \"-b:v\", \"200k\", \"-maxrate\", \"300k\"}\n\t\tcase 2:\n\t\t\trcargs = []string{\"-minrate\", \"20k\", \"-b:v\", \"100k\", \"-maxrate\", \"200k\"}\n\t\tcase 3:\n\t\t\trcargs = []string{\"-minrate\", \"10k\", \"-b:v\", \"50k\", \"-maxrate\", \"100k\"}\n\t\t}\n\t\targs := append(baseargs, rcargs...)\n\t\targs = append(args, []string{\"-to\", \"00:00:03\", \"-an\", \"-y\", pathOut}...)\n\t\tout, err := exec.Command(bin, args...).CombinedOutput()\n\t\tif err != nil {\n\t\t\tlog.Warnln(\"ffToWebm ERROR:\", string(out))\n\t\t\t//FFMPEG does not support animated webp.\n\t\t\t//Convert to APNG first than WEBM.\n\t\t\tif strings.Contains(string(out), \"skipping unsupported chunk: ANIM\") {\n\t\t\t\tlog.Warnln(\"Trying to convert to APNG first.\")\n\t\t\t\tf2, _ := IMToApng(f)\n\t\t\t\treturn FFToWebmTGVideo(f2, isCustomEmoji)\n\t\t\t}\n\t\t\treturn pathOut, err\n\t\t}\n\t\tstat, err := os.Stat(pathOut)\n\t\tif err != nil {\n\t\t\treturn pathOut, err\n\t\t}\n\t\tif stat.Size() > 255*KiB {\n\t\t\tcontinue\n\t\t} else {\n\t\t\treturn pathOut, err\n\t\t}\n\t}\n\tlog.Errorln(\"FFToWebmTGVideo: unable to compress below 256KiB:\", pathOut)\n\treturn pathOut, errors.New(\"FFToWebmTGVideo: unable to compress below 256KiB\")\n}\n\n// This function will be called if Telegram's API rejected our webm.\n// It is normally due to overlength or bad FPS rate.\nfunc FFToWebmSafe(f string, isCustomEmoji bool) (string, error) {\n\tpathOut := f + \".webm\"\n\tbin := FFMPEG_BIN\n\targs := []string{}\n\targs = append(args, \"-hide_banner\", \"-i\", f)\n\tif isCustomEmoji {\n\t\targs = append(args, \"-vf\", \"scale=100:100:force_original_aspect_ratio=decrease\")\n\t} else {\n\t\targs = append(args, \"-vf\", \"scale=512:512:force_original_aspect_ratio=decrease:flags=lanczos\")\n\t}\n\targs = append(args, \"-pix_fmt\", \"yuva420p\",\n\t\t\"-c:v\", \"libvpx-vp9\", \"-cpu-used\", \"5\", \"-minrate\", \"50k\", \"-b:v\", \"200k\", \"-maxrate\", \"300k\",\n\t\t\"-to\", \"00:00:02.800\", \"-r\", \"30\", \"-an\", \"-y\", pathOut)\n\n\tcmd := exec.Command(bin, args...)\n\terr := cmd.Run()\n\treturn pathOut, err\n}\n\nfunc FFToGif(f string) (string, error) {\n\tvar decoder []string\n\tvar args []string\n\tif strings.HasSuffix(f, \".webm\") {\n\t\tdecoder = []string{\"-c:v\", \"libvpx-vp9\"}\n\t}\n\tpathOut := f + \".gif\"\n\tbin := FFMPEG_BIN\n\targs = append(args, decoder...)\n\targs = append(args, \"-i\", f, \"-hide_banner\",\n\t\t\"-lavfi\", \"split[a][b];[a]palettegen[p];[b][p]paletteuse=dither=atkinson\",\n\t\t\"-gifflags\", \"-transdiff\", \"-gifflags\", \"-offsetting\",\n\t\t\"-loglevel\", \"error\", \"-y\", pathOut)\n\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err != nil {\n\t\tlog.Warnf(\"ffToGif ERROR:\\n%s\", string(out))\n\t\treturn \"\", err\n\t}\n\t//Optimize GIF produced by ffmpeg\n\texec.Command(\"gifsicle\", \"--batch\", \"-O2\", \"--lossy=60\", pathOut).CombinedOutput()\n\n\treturn pathOut, err\n}\n\n// func FFToAPNG(f string) (string, error) {\n// \tvar decoder []string\n// \tvar args []string\n// \tif strings.HasSuffix(f, \".webm\") {\n// \t\tdecoder = []string{\"-c:v\", \"libvpx-vp9\"}\n// \t}\n// \tpathOut := f + \".apng\"\n// \tbin := FFMPEG_BIN\n// \targs = append(args, decoder...)\n// \targs = append(args, \"-i\", f, \"-hide_banner\",\n// \t\t\"-loglevel\", \"error\", \"-y\", pathOut)\n\n// \tout, err := exec.Command(bin, args...).CombinedOutput()\n// \tif err != nil {\n// \t\tlog.Warnf(\"ffToAPNG ERROR:\\n%s\", string(out))\n// \t\treturn \"\", err\n// \t}\n// \treturn pathOut, err\n// }\n\nfunc IMStackToWebp(base string, overlay string) (string, error) {\n\tbin := CONVERT_BIN\n\targs := CONVERT_ARGS\n\tfOut := base + \".composite.webp\"\n\n\targs = append(args, base, overlay, \"-background\", \"none\", \"-filter\", \"Lanczos\", \"-resize\", \"512x512\", \"-composite\",\n\t\t\"-define\", \"webp:lossless=true\", fOut)\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err != nil {\n\t\tlog.Errorln(\"IM stack ERROR!\", string(out))\n\t\treturn \"\", err\n\t} else {\n\t\treturn fOut, nil\n\t}\n}\n\n// Replaces tgs to gif.\nfunc RlottieToGIF(f string) (string, error) {\n\tbin := \"msb_rlottie.py\"\n\tfOut := strings.ReplaceAll(f, \".tgs\", \".gif\")\n\targs := []string{f, fOut}\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err != nil {\n\t\tlog.Errorln(\"lottieToGIF ERROR!\", string(out))\n\t\treturn \"\", err\n\t}\n\t//Optimize GIF\n\texec.Command(\"gifsicle\", \"--batch\", \"-O2\", \"--lossy=60\", fOut).CombinedOutput()\n\treturn fOut, nil\n}\n\n// Replaces tgs to webp.\n// The only purpose for this func is for WhatsApp export.\n// func RlottieToWebpWAAnimated(f string) (string, error) {\n// \tbin := \"msb_rlottie.py\"\n// \tpathOut := strings.ReplaceAll(f, \".tgs\", \".webp\")\n\n// \tqualities := []string{\"50\", \"20\", \"0\"}\n// \tfor _, q := range qualities {\n// \t\targs := []string{f, pathOut, q}\n// \t\tout, err := exec.Command(bin, args...).CombinedOutput()\n// \t\tif err != nil {\n// \t\t\tlog.Errorln(\"RlottieToWebp ERROR!\", string(out))\n// \t\t\treturn \"\", err\n// \t\t}\n// \t\t//WhatsApp uses KiB.\n// \t\tst, err := os.Stat(pathOut)\n// \t\tif err != nil {\n// \t\t\treturn pathOut, err\n// \t\t}\n// \t\tif st.Size() > 500*KiB {\n// \t\t\tlog.Warnf(\"convert: awebp exceeded 500KiB, q:%s z:%d s:%s\", q, st.Size(), pathOut)\n// \t\t\tcontinue\n// \t\t} else {\n// \t\t\treturn pathOut, nil\n// \t\t}\n// \t}\n// \tlog.Warnln(\"all quality failed! s:\", pathOut)\n// \treturn pathOut, errors.New(\"bad animated webp?\")\n// }\n\n// Replaces .webm ext to .webp\nfunc IMToAnimatedWebpLQ(f string) error {\n\tpathOut := strings.ReplaceAll(f, \".webm\", \".webp\")\n\tbin := CONVERT_BIN\n\targs := CONVERT_ARGS\n\targs = append(args, \"-resize\", \"128x128\", f, pathOut)\n\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err != nil {\n\t\tlog.Warnln(\"imToWebp ERROR:\", string(out))\n\t\treturn err\n\t}\n\treturn err\n}\n\n// Replaces .webm ext to .webp\nfunc FFToAnimatedWebpLQ(f string) error {\n\tpathOut := strings.ReplaceAll(f, \".webm\", \".webp\")\n\tbin := FFMPEG_BIN\n\n\targs := []string{\"-hide_banner\", \"-c:v\", \"libvpx-vp9\", \"-i\", f,\n\t\t\"-vf\", \"scale=128:128:force_original_aspect_ratio=decrease\",\n\t\t\"-loop\", \"0\", \"-pix_fmt\", \"yuva420p\",\n\t\t\"-an\", \"-y\", pathOut}\n\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err != nil {\n\t\tlog.Warnln(\"ffToAnimatedWebpWA ERROR:\", string(out))\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// // animated webp has a pretty bad compression ratio comparing to VP9,\n// // shrink down quality as much as possible.\nfunc FFToAnimatedWebpWA(f string) error {\n\tpathOut := strings.ReplaceAll(f, \".webm\", \".webp\")\n\tbin := FFMPEG_BIN\n\t//Try qualities from best to worst.\n\tqualities := []string{\"75\", \"50\", \"20\", \"0\", \"_DS256\", \"_DS256Q0\"}\n\n\tfor _, q := range qualities {\n\t\targs := []string{\"-hide_banner\", \"-c:v\", \"libvpx-vp9\", \"-i\", f,\n\t\t\t\"-vf\", \"scale=512:512:force_original_aspect_ratio=decrease,pad=512:512:-1:-1:color=black@0\",\n\t\t\t\"-quality\", q, \"-loop\", \"0\", \"-pix_fmt\", \"yuva420p\",\n\t\t\t\"-an\", \"-y\", pathOut}\n\n\t\tif q == \"_DS256\" {\n\t\t\targs = []string{\"-hide_banner\", \"-c:v\", \"libvpx-vp9\", \"-i\", f,\n\t\t\t\t\"-vf\", \"scale=256:256:force_original_aspect_ratio=decrease,pad=512:512:-1:-1:color=black@0\",\n\t\t\t\t\"-quality\", \"20\", \"-loop\", \"0\", \"-pix_fmt\", \"yuva420p\",\n\t\t\t\t\"-an\", \"-y\", pathOut}\n\t\t}\n\n\t\tif q == \"_DS256Q0\" {\n\t\t\targs = []string{\"-hide_banner\", \"-c:v\", \"libvpx-vp9\", \"-i\", f,\n\t\t\t\t\"-vf\", \"scale=256:256:force_original_aspect_ratio=decrease,pad=512:512:-1:-1:color=black@0\",\n\t\t\t\t\"-quality\", \"0\", \"-loop\", \"0\", \"-pix_fmt\", \"yuva420p\",\n\t\t\t\t\"-an\", \"-y\", pathOut}\n\t\t}\n\n\t\tout, err := exec.Command(bin, args...).CombinedOutput()\n\t\tif err != nil {\n\t\t\tlog.Warnln(\"ffToAnimatedWebpWA ERROR:\", string(out))\n\t\t\treturn err\n\t\t}\n\t\t//WhatsApp uses KiB.\n\t\tst, err := os.Stat(pathOut)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif st.Size() > 500*KiB {\n\t\t\tlog.Warnf(\"convert: awebp exceeded 500KiB, q:%s z:%d s:%s\", q, st.Size(), pathOut)\n\t\t\tcontinue\n\t\t} else {\n\t\t\treturn nil\n\t\t}\n\t}\n\tlog.Warnln(\"all quality failed! s:\", pathOut)\n\n\treturn errors.New(\"bad animated webp?\")\n}\n\n// appends png\nfunc FFtoPNG(f string, pathOut string) error {\n\tvar args []string\n\tbin := FFMPEG_BIN\n\targs = append(args, \"-c:v\", \"libvpx-vp9\", \"-i\", f, \"-hide_banner\",\n\t\t\"-loglevel\", \"error\", \"-frames\", \"1\", \"-y\", pathOut)\n\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err != nil {\n\t\tlog.Warnf(\"fftoPNG ERROR:\\n%s\", string(out))\n\t\treturn err\n\t}\n\treturn err\n}\n\n// Replaces .webm or .webp to .png\nfunc IMToPNGThumb(f string) error {\n\tpathOut := strings.ReplaceAll(f, \".webm\", \".png\")\n\tpathOut = strings.ReplaceAll(pathOut, \".webp\", \".png\")\n\n\tif strings.HasSuffix(f, \".webm\") {\n\t\ttempThumb := f + \".thumb.png\"\n\t\tFFtoPNG(f, tempThumb)\n\t\tf = tempThumb\n\t}\n\n\tbin := CONVERT_BIN\n\targs := CONVERT_ARGS\n\targs = append(args,\n\t\t\"-resize\", \"96x96\",\n\t\t\"-gravity\", \"center\", \"-extent\", \"96x96\", \"-background\", \"none\",\n\t\tf+\"[0]\", pathOut)\n\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err != nil {\n\t\tlog.Warnln(\"imToPng ERROR:\", string(out))\n\t\treturn err\n\t}\n\treturn err\n}\n\nfunc SetImageTime(f string, t time.Time) error {\n\treturn os.Chtimes(f, t, t)\n\t// asciiTime := t.Format(\"2006:01:02 15:04:05\")\n\t// _, err := exec.Command(\"exiv2\", \"-M\", \"set Exif.Image.DateTime \"+asciiTime, f).CombinedOutput()\n\t// if err != nil {\n\t// \treturn err\n\t// }\n\t// return nil\n}\n"
  },
  {
    "path": "pkg/msbimport/import.go",
    "content": "package msbimport\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/url\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// This function serves as an entrypoint for this package.\n// Parse a LINE or Kakao link and fetch metadata.\n// The metadata (which means the LineData struct) can be used to call prepareImportStickers.\n// Returns a string and an error. String act as a warning message, empty string means no warning yield.\n//\n// Attention: After this function returns, ld.Amount, ld.Files will NOT be available!\nfunc ParseImportLink(link string, ld *LineData) (string, error) {\n\tvar warn string\n\n\tu, err := url.Parse(link)\n\tif err != nil {\n\t\treturn warn, err\n\t}\n\tswitch {\n\tcase strings.HasSuffix(u.Host, \"line.me\"):\n\t\tld.Store = \"line\"\n\t\treturn parseLineLink(link, ld)\n\tcase strings.HasSuffix(u.Host, \"kakao.com\"):\n\t\tld.Store = \"kakao\"\n\t\treturn parseKakaoLink(link, ld)\n\tdefault:\n\t\treturn warn, errors.New(\"unknow import\")\n\t}\n}\n\n// Prepare stickers files.\n// Should be called after calling ParseImportLink().\n// A context is provided, which can be used to interrupt the process.\n// Even if this function returns, file preparation might still in progress.\n// LineData.Amount, LineData.Files will be produced after return.\n// wg.Wait() is required for individual LineData.Files\n//\n// convertToTGFormat: Convert original stickers to Telegram sticker format.\n// convertToTGEmoji: If present sticker set is Emoji(LINE), convert to 100x100 Telegram CustomEmoji.\nfunc PrepareImportStickers(ctx context.Context, ld *LineData, workDir string, convertToTGFormat bool, convertToTGEmoji bool) error {\n\tswitch ld.Store {\n\tcase \"line\":\n\t\treturn prepareLineStickers(ctx, ld, workDir, convertToTGFormat, convertToTGEmoji)\n\tcase \"kakao\":\n\t\treturn prepareKakaoStickers(ctx, ld, workDir, convertToTGFormat)\n\t}\n\treturn nil\n}\n\n// Convert imported sticker to Telegram format,\n// which means WEBM for animated and WEBP for static\n// with 512x512 dimension.\nfunc convertSToTGFormat(ctx context.Context, ld *LineData) {\n\tfor _, s := range ld.Files {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlog.Warn(\"convertSToTGFormat received ctxDone!\")\n\t\t\treturn\n\t\tdefault:\n\t\t}\n\t\tvar err error\n\t\t// If lineS is animated, commit to worker pool\n\t\t// since encoding vp9 is time and resource costy.\n\t\tif ld.IsAnimated {\n\t\t\twpConvertWebm.Invoke(s)\n\t\t} else {\n\t\t\ts.ConvertedFile, err = IMToWebpTGStatic(s.OriginalFile, s.ConvertToEmoji)\n\t\t\tif err != nil {\n\t\t\t\ts.CError = err\n\t\t\t}\n\t\t\ts.Wg.Done()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/msbimport/import_kakao.go",
    "content": "package msbimport\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc parseKakaoLink(link string, ld *LineData) (string, error) {\n\tvar kakaoID string\n\t// var eid string\n\tvar err error\n\tvar warn string\n\n\turl, _ := url.Parse(link)\n\n\tswitch url.Host {\n\t// Kakao web link.\n\tcase \"e.kakao.com\":\n\t\tkakaoID = path.Base(url.Path)\n\t// Kakao mobile app share link.\n\tcase \"emoticon.kakao.com\":\n\t\t_, kakaoID, err = fetchKakaoDetailsFromShareLink(link)\n\t\tif err != nil {\n\t\t\treturn warn, err\n\t\t}\n\t// unknown host\n\tdefault:\n\t\treturn warn, errors.New(\"unknown kakao link type\")\n\t}\n\n\tvar kakaoJson KakaoJson\n\terr = fetchKakaoMetadata(&kakaoJson, kakaoID)\n\tif err != nil {\n\t\tlog.Debugln(\"Failed fetchKakaoMetadata:\", err)\n\t\treturn warn, err\n\t}\n\n\tlog.Debugln(\"Parsed kakao link:\", link)\n\tlog.Debugln(kakaoJson.Result)\n\n\t// if url.Host == \"emoticon.kakao.com\" {\n\t// \tld.DLink = fmt.Sprintf(\"http://item.kakaocdn.net/dw/%s.file_pack.zip\", eid)\n\t// } else {\n\tld.DLinks = kakaoJson.Result.ThumbnailUrls\n\t// warn = WARN_KAKAO_PREFER_SHARE_LINK\n\t// }\n\n\tld.Title = kakaoJson.Result.Title\n\tld.Id = kakaoJson.Result.TitleUrl\n\tld.Link = link\n\tld.Amount = len(ld.DLinks)\n\tld.Category = KAKAO_EMOTICON\n\treturn warn, nil\n}\n\nfunc fetchKakaoMetadata(kakaoJson *KakaoJson, kakaoID string) error {\n\tapiUrl := \"https://e.kakao.com/api/v1/items/t/\" + kakaoID\n\tpage, err := httpGet(apiUrl)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = json.Unmarshal([]byte(page), &kakaoJson)\n\tif err != nil {\n\t\tlog.Errorln(\"Failed json parsing kakao link!\", err)\n\t\treturn err\n\t}\n\n\tlog.Debugln(\"fetchKakaoMetadata: api link metadata fetched:\", apiUrl)\n\treturn nil\n}\n\n// Download and convert(if needed) stickers to work directory.\n// *ld will be modified and loaded with local sticker information.\nfunc prepareKakaoStickers(ctx context.Context, ld *LineData, workDir string, needConvert bool) error {\n\t// If no dLink, continue importing static ones.\n\tif ld.DLink != \"\" {\n\t\treturn prepareKakaoZipStickers(ctx, ld, workDir, needConvert)\n\t}\n\n\tos.MkdirAll(workDir, 0755)\n\n\t//Initialize Files with wg added.\n\t//This is intended for async operation.\n\t//When user reached commitSticker state, sticker will be waited one by one.\n\tfor range ld.DLinks {\n\t\tlf := &LineFile{}\n\t\tlf.Wg.Add(1)\n\t\tld.Files = append(ld.Files, lf)\n\t}\n\n\t//Download stickers one by one.\n\tgo func() {\n\t\tfor i, l := range ld.DLinks {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tlog.Warn(\"prepareKakaoStickers received ctxDone!\")\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tf := filepath.Join(workDir, path.Base(l)+\".png\")\n\t\t\terr := httpDownload(l, f)\n\t\t\tif err != nil {\n\t\t\t\tld.Files[i].CError = err\n\t\t\t}\n\t\t\tcf, _ := IMToWebpTGStatic(f, false)\n\t\t\tld.Files[i].OriginalFile = f\n\t\t\tld.Files[i].ConvertedFile = cf\n\t\t\tld.Files[i].Wg.Done()\n\n\t\t\tlog.Debug(\"Done process one kakao emoticon\")\n\t\t\tlog.Debugf(\"f:%s, cf:%s\", f, cf)\n\t\t}\n\t\tlog.Debug(\"Done process ALL kakao emoticons\")\n\t}()\n\treturn nil\n}\n\nfunc prepareKakaoZipStickers(ctx context.Context, ld *LineData, workDir string, needConvert bool) error {\n\tzipPath := filepath.Join(workDir, \"kakao.zip\")\n\tos.MkdirAll(workDir, 0755)\n\n\tlog.Debugln(\"prepareKakaoZipStickers: downloading zip:\", ld.DLink)\n\terr := fDownload(ld.DLink, zipPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tkakaoFiles := kakaoZipExtract(zipPath, ld)\n\tif len(kakaoFiles) == 0 {\n\t\treturn errors.New(\"no kakao image in zip\")\n\t}\n\n\tif filepath.Ext(kakaoFiles[0]) != \".png\" {\n\t\tld.IsAnimated = true\n\t}\n\n\tfor _, wf := range kakaoFiles {\n\t\tlf := &LineFile{\n\t\t\tOriginalFile: wf,\n\t\t}\n\t\tif needConvert {\n\t\t\tlf.Wg.Add(1)\n\t\t}\n\t\tld.Files = append(ld.Files, lf)\n\t}\n\tld.Amount = len(kakaoFiles)\n\n\tif needConvert {\n\t\tgo convertSToTGFormat(ctx, ld)\n\t}\n\n\tlog.Debug(\"Done preparing kakao files:\")\n\tlog.Debugln(ld)\n\n\treturn nil\n}\n\n// Extract and decrypt kakao zip.\nfunc kakaoZipExtract(f string, ld *LineData) []string {\n\tvar files []string\n\tworkDir := fExtract(f)\n\tif workDir == \"\" {\n\t\treturn nil\n\t}\n\tlog.Debugln(\"scanning workdir:\", workDir)\n\tfiles = LsFiles(workDir, []string{}, []string{})\n\n\tfor _, f := range files {\n\t\t//PNG is not encrypted.\n\t\tif filepath.Ext(f) != \".png\" {\n\t\t\t//This script decrypts the file in-place.\n\t\t\texec.Command(\"msb_kakao_decrypt.py\", f).Run()\n\t\t}\n\t}\n\treturn files\n}\n\n// Return: kakao eid(code), kakao id, error\nfunc fetchKakaoDetailsFromShareLink(link string) (string, string, error) {\n\tlog.Debugln(\"fetchKakaoDetailsFromShareLink: Link is:\", link)\n\tres, err := httpGetAndroidUA(link)\n\tif err != nil {\n\t\tlog.Errorln(\"fetchKakaoDetailsFromShareLink: failed httpGetAndroidUA!\", err)\n\t\treturn \"\", \"\", err\n\t}\n\tdoc, err := goquery.NewDocumentFromReader(strings.NewReader(res))\n\tif err != nil {\n\t\tlog.Errorln(\"fetchKakaoDetailsFromShareLink failed gq parsing line link!\", err)\n\t\treturn \"\", \"\", err\n\t}\n\n\t//This eid seemed to be fake.\n\t//There will be no fix soon.\n\t//In the future we might use other package to complete\n\t//kakao download.\n\teid := \"\"\n\tdoc.Find(\"a\").Each(func(i int, s *goquery.Selection) {\n\t\tvalue, _ := s.Attr(\"id\")\n\t\tif value == \"app_scheme_link\" {\n\t\t\teid, _ = s.Attr(\"data-i\")\n\t\t}\n\t})\n\tlog.Debugln(\"kakao eid is:\", eid)\n\tredirLink, _, err := httpGetWithRedirLink(link)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tkakaoID := path.Base(redirLink)\n\treturn eid, kakaoID, nil\n}\n"
  },
  {
    "path": "pkg/msbimport/import_line.go",
    "content": "package msbimport\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc parseLineLink(link string, ld *LineData) (string, error) {\n\tvar warn string\n\tpage, err := httpGet(link)\n\tif err != nil {\n\t\treturn warn, err\n\t}\n\tdoc, err := goquery.NewDocumentFromReader(strings.NewReader(page))\n\tif err != nil {\n\t\tlog.Errorln(\"Failed gq parsing line link!\", err)\n\t\treturn warn, err\n\t}\n\n\tvar lineJson LineJson\n\terr = parseLineDetails(doc, &lineJson)\n\tif err != nil {\n\t\tlog.Errorln(\"parseLineLink: \", err)\n\t\treturn warn, err\n\t}\n\n\tt := lineJson.Name\n\ti := lineJson.Sku\n\tu := lineJson.Url\n\tls := fetchLineI18nLinks(doc)\n\ta := false\n\te := false\n\tc := \"\"\n\td := \"https://stickershop.line-scdn.net/stickershop/v1/product/\" + i + \"/iphone/\"\n\n\tif strings.Contains(u, \"stickershop\") || strings.Contains(u, \"officialaccount/event/sticker\") {\n\t\tif strings.Contains(page, \"MdIcoPlay_b\") || strings.Contains(page, \"MdIcoAni_b\") {\n\t\t\tc = LINE_STICKER_ANIMATION\n\t\t\td += \"stickerpack@2x.zip\"\n\t\t\ta = true\n\t\t} else if strings.Contains(page, \"MdIcoMessageSticker_b\") {\n\t\t\td = u\n\t\t\tc = LINE_STICKER_MESSAGE\n\t\t} else if strings.Contains(page, \"MdIcoNameSticker_b\") {\n\t\t\td += \"sticker_name_base@2x.zip\"\n\t\t\tc = LINE_STICKER_NAME\n\t\t} else if strings.Contains(page, \"MdIcoFlash_b\") || strings.Contains(page, \"MdIcoFlashAni_b\") {\n\t\t\tc = LINE_STICKER_POPUP\n\t\t\td += \"stickerpack@2x.zip\"\n\t\t\ta = true\n\t\t} else if strings.Contains(page, \"MdIcoEffectSoundSticker_b\") || strings.Contains(page, \"MdIcoEffectSticker_b\") {\n\t\t\tc = LINE_STICKER_POPUP_EFFECT\n\t\t\td += \"stickerpack@2x.zip\"\n\t\t\ta = true\n\t\t} else {\n\t\t\tc = LINE_STICKER_STATIC\n\t\t\t// According to collected logs, LINE ID befre exact 775 have special PNG encodings,\n\t\t\t// which are not parsable with libpng.\n\t\t\t// You will get -> CgBI: unhandled critical chunk <- from IM.\n\t\t\t// Workaround is to take the lower resolution \"android\" ones.\n\t\t\tif id, _ := strconv.Atoi(i); id < 775 && id != 0 {\n\t\t\t\td = \"https://stickershop.line-scdn.net/stickershop/v1/product/\" + i + \"/android/\" +\n\t\t\t\t\t\"stickers.zip\"\n\t\t\t} else {\n\t\t\t\td += \"stickers@2x.zip\"\n\t\t\t}\n\t\t}\n\t} else if strings.Contains(u, \"emojishop\") {\n\t\tif strings.Contains(page, \"MdIcoPlay_b\") {\n\t\t\tc = LINE_EMOJI_ANIMATION\n\t\t\td = \"https://stickershop.line-scdn.net/sticonshop/v1/sticon/\" + i + \"/iphone/package_animation.zip\"\n\t\t\ta = true\n\t\t\te = true\n\t\t} else {\n\t\t\tc = LINE_EMOJI_STATIC\n\t\t\te = true\n\t\t\td = \"https://stickershop.line-scdn.net/sticonshop/v1/sticon/\" + i + \"/iphone/package.zip\"\n\t\t}\n\t} else {\n\t\treturn warn, errors.New(\"unknown line store category\")\n\t}\n\tif ld == nil {\n\t\treturn warn, nil\n\t}\n\n\tld.Link = u\n\tld.I18nLinks = ls\n\tld.Category = c\n\tld.DLink = d\n\tld.Id = i\n\tld.Title = t\n\tld.IsAnimated = a\n\tld.IsEmoji = e\n\n\tlog.Debugln(\"line data parsed:\", ld)\n\n\tld.TitleWg.Add(1)\n\tgo fetchLineI18nTitles(ld)\n\treturn warn, nil\n}\n\nfunc fetchLineI18nLinks(doc *goquery.Document) []string {\n\tvar i18nLinks []string\n\tdoc.Find(\"link\").Each(func(i int, s *goquery.Selection) {\n\t\threflang, exist := s.Attr(\"hreflang\")\n\t\tif !exist {\n\t\t\treturn\n\t\t}\n\t\thref, exist2 := s.Attr(\"href\")\n\t\tif !exist2 {\n\t\t\treturn\n\t\t}\n\t\tswitch hreflang {\n\t\tcase \"zh-Hant\":\n\t\t\tfallthrough\n\t\tcase \"ja\":\n\t\t\tfallthrough\n\t\tcase \"en\":\n\t\t\ti18nLinks = append(i18nLinks, href)\n\t\t}\n\t})\n\tlog.Debugln(\"Fetched LINE I18n Links: \", i18nLinks)\n\treturn i18nLinks\n}\n\nfunc fetchLineI18nTitles(ld *LineData) {\n\tdefer ld.TitleWg.Done()\n\tlog.Debugln(\"Fetching LINE i18n titles...\")\n\tvar i18nTitles []string\n\n\tfor _, l := range ld.I18nLinks {\n\t\tpage, err := httpGet(l)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tlineJson := &LineJson{}\n\t\tdoc, err := goquery.NewDocumentFromReader(strings.NewReader(page))\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tparseLineDetails(doc, lineJson)\n\n\t\tfor _, t := range i18nTitles {\n\t\t\t// if title duplicates, skip loop\n\t\t\tif t == lineJson.Name {\n\t\t\t\tgoto CONTINUE\n\t\t\t}\n\t\t}\n\t\ti18nTitles = append(i18nTitles, lineJson.Name)\n\n\tCONTINUE:\n\t\tcontinue\n\t}\n\n\tld.I18nTitles = i18nTitles\n\tlog.Debugln(\"Fetched I18N titles are:\")\n\tlog.Debugln(ld.I18nTitles)\n}\n\n// This function goes after parseLineLink\n// Receives a gq document of LINE Store page and parse the details to *LineJson.\nfunc parseLineDetails(doc *goquery.Document, lj *LineJson) error {\n\t// For typical line store page, the first script is sticker's metadata in JSON.\n\t// Parse to json, if OK, return nil here.\n\terr := json.Unmarshal([]byte(doc.Find(\"script\").First().Text()), lj)\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\t// Some new line store page does not have a json metadata <script>\n\tlog.Warnln(\"Failed json parsing line link!\", err)\n\tlog.Warnln(\"Special LINE type? Trying to guess info.\")\n\n\t// Find URL.\n\tdoc.Find(\"meta\").Each(func(i int, s *goquery.Selection) {\n\t\tvalue, _ := s.Attr(\"property\")\n\t\tif value == \"og:url\" {\n\t\t\tlj.Url, _ = s.Attr(\"content\")\n\t\t}\n\t})\n\n\t// Find title.\n\tdoc.Find(\"h3\").Each(func(i int, s *goquery.Selection) {\n\t\toa, _ := s.Attr(\"data-test\")\n\t\tif oa == \"oa-sticker-title\" {\n\t\t\tlj.Name = s.Text()\n\t\t}\n\t})\n\t// Find title again.\n\tif lj.Name == \"\" {\n\t\tdoc.Find(\"p\").Each(func(i int, s *goquery.Selection) {\n\t\t\toa, _ := s.Attr(\"data-test\")\n\t\t\tif oa == \"sticker-name-title\" {\n\t\t\t\tlj.Name = s.Text()\n\t\t\t}\n\t\t})\n\t}\n\tif lj.Name == \"\" {\n\t\treturn errors.New(\"guess line: no title\")\n\t}\n\n\t// Find ID.\n\tvar defaultLink string\n\tdoc.Find(\"link\").Each(func(i int, s *goquery.Selection) {\n\t\threflang, _ := s.Attr(\"hreflang\")\n\t\tif hreflang == \"x-default\" {\n\t\t\tdefaultLink, _ = s.Attr(\"href\")\n\t\t}\n\t})\n\tlj.Sku = path.Base(defaultLink)\n\tif lj.Sku == \"\" {\n\t\treturn errors.New(\"guess line: no id(no link base)\")\n\t}\n\n\tlog.Debugln(\"parsed line detail:\", lj)\n\treturn nil\n}\n\n// Download and convert sticker files after parseLineLink.\nfunc prepareLineStickers(ctx context.Context, ld *LineData, workDir string, convertToTGFormat bool, convertToTGEmoji bool) error {\n\tif ld.Category == LINE_STICKER_MESSAGE {\n\t\treturn prepareLineMessageS(ctx, ld, workDir, convertToTGFormat)\n\t}\n\n\tsavePath := filepath.Join(workDir, \"line.zip\")\n\tos.MkdirAll(workDir, 0755)\n\n\terr := fDownload(ld.DLink, savePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpngFiles := lineZipExtract(savePath, ld)\n\tif len(pngFiles) == 0 {\n\t\treturn errors.New(\"no line image\")\n\t}\n\n\tfor _, pf := range pngFiles {\n\t\tlf := &LineFile{\n\t\t\tConvertToEmoji: convertToTGEmoji,\n\t\t\tOriginalFile:   pf,\n\t\t}\n\t\tif convertToTGFormat {\n\t\t\tlf.Wg.Add(1)\n\t\t}\n\t\tld.Files = append(ld.Files, lf)\n\t}\n\tld.Amount = len(pngFiles)\n\n\tif convertToTGFormat {\n\t\tlog.Debugln(\"start converting...\")\n\t\tgo convertSToTGFormat(ctx, ld)\n\t}\n\n\tlog.Debug(\"Done preparing line files:\")\n\tlog.Debugln(ld)\n\n\treturn nil\n}\n\nfunc lineZipExtract(f string, ld *LineData) []string {\n\tvar files []string\n\tworkDir := fExtract(f)\n\tif workDir == \"\" {\n\t\treturn nil\n\t}\n\tlog.Debugln(\"scanning workdir:\", workDir)\n\n\tswitch ld.Category {\n\tcase LINE_STICKER_ANIMATION:\n\t\tfiles, _ = filepath.Glob(filepath.Join(workDir, \"animation@2x\", \"*.png\"))\n\tcase LINE_STICKER_POPUP:\n\t\tfiles, _ = filepath.Glob(filepath.Join(workDir, \"popup\", \"*.png\"))\n\tcase LINE_STICKER_POPUP_EFFECT:\n\t\tpfs, _ := filepath.Glob(filepath.Join(workDir, \"popup\", \"*.png\"))\n\t\tfor _, pf := range pfs {\n\t\t\tos.Rename(pf, filepath.Join(workDir, strings.TrimSuffix(filepath.Base(pf), \".png\")+\"@99.png\"))\n\t\t}\n\t\tfiles = LsFiles(workDir, []string{\".png\"}, []string{\"tab\", \"key\", \"json\"})\n\tdefault:\n\t\tfiles = LsFiles(workDir, []string{\".png\"}, []string{\"tab\", \"key\", \"json\"})\n\t}\n\tif ld.IsAnimated {\n\t\tsanitizeLinePNGs(files)\n\t}\n\treturn files\n}\n\n// Some LINE png contains a 'tEXt' textual information after `fcTL`\n// FFMpeg's APNG demuxer could not parse it properly.\n// I have patched ffmpeg, however, compiling it for AArch64 is not easy.\n// Therefore, we are going to tackle the source itself by removing tEXt chunk.\nfunc sanitizeLinePNGs(files []string) bool {\n\tfor _, f := range files {\n\t\tret := removeAPNGtEXtChunk(f)\n\t\tif !ret {\n\t\t\tlog.Debugln(\"one file sanitization ignored:\", f)\n\t\t\t//do nothing.\n\t\t}\n\t}\n\treturn true\n}\n\nfunc removeAPNGtEXtChunk(f string) bool {\n\tbytes, err := os.ReadFile(f)\n\tif err != nil {\n\t\treturn false\n\t}\n\tl := len(bytes)\n\tif l < 42 {\n\t\treturn false\n\t}\n\t// byte index 37-40 must be acTL Animation Control Chunk.\n\tif string(bytes[37:41]) != \"acTL\" {\n\t\treturn false\n\t}\n\ttextStart := 0\n\ttextEnd := 0\n\tfor i := range bytes {\n\t\tif i > l-10 {\n\t\t\tbreak\n\t\t}\n\t\ttag := bytes[i : i+4]\n\t\t// only probe the first appearence of 'tEXt' tag.\n\t\tif string(tag) == \"tEXt\" && textStart == 0 {\n\t\t\t// 4 bytes before tag represents chunk length.\n\t\t\ttextStart = i - 4\n\t\t\t// first IDAT after tEXt should be what we want.\n\t\t} else if string(tag) == \"IDAT\" && textStart != 0 {\n\t\t\ttextEnd = i - 4\n\t\t\tbreak\n\t\t}\n\t}\n\tif textStart == 0 || textEnd == 0 {\n\t\treturn false\n\t}\n\tnewBytes := bytes[:textStart]\n\tnewBytes = append(newBytes, bytes[textEnd:]...)\n\n\tos.Remove(f)\n\tfo, err := os.Create(f)\n\tif err != nil {\n\t\treturn false\n\t}\n\tdefer fo.Close()\n\tfo.Write(newBytes)\n\tlog.Infoln(\"Sanitized one APNG file, path:\", f)\n\tlog.Infof(\"Length from %d to %d.\", l, len(newBytes))\n\treturn true\n}\n\n// Line message sticker is a composition of two stickers.\n// One represents the backgroud and one represents the foreground text.\n// We need to composite them together.\nfunc prepareLineMessageS(ctx context.Context, ld *LineData, workDir string, needConvert bool) error {\n\tos.MkdirAll(workDir, 0755)\n\tvar baseImages []string\n\tvar overlayImages []string\n\n\terr := parseLineProductInfo(ld.Id, ld)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, l := range ld.DLinks {\n\t\tbaseImages = append(baseImages,\n\t\t\t\"https://stickershop.line-scdn.net/stickershop/v1/sticker/\"+l+\"/iPhone/base/plus/sticker@2x.png\")\n\t\toverlayImages = append(overlayImages,\n\t\t\t\"https://stickershop.line-scdn.net/stickershop/v1/product/\"+ld.Id+\"/sticker/\"+l+\"/iPhone/overlay/plus/default/sticker@2x.png\")\n\t}\n\n\tlog.Debugln(\"base images:\", baseImages)\n\tlog.Debugln(\"overlay images:\", overlayImages)\n\n\tfor range baseImages {\n\t\tlf := &LineFile{}\n\t\tlf.Wg.Add(1)\n\t\tld.Files = append(ld.Files, lf)\n\t}\n\tld.Amount = len(baseImages)\n\n\tgo func() {\n\t\tfor i, b := range baseImages {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tlog.Warn(\"prepLineMessageS received ctxDone!\")\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\t\t\tlog.Debugln(\"Preparing one message sticker... index:\", i)\n\t\t\tbPath := filepath.Join(workDir, strconv.Itoa(i)+\".base.png\")\n\t\t\toPath := filepath.Join(workDir, strconv.Itoa(i)+\".overlay.png\")\n\t\t\thttpDownloadCurlUA(b, bPath)\n\t\t\thttpDownloadCurlUA(overlayImages[i], oPath)\n\t\t\tf, err := IMStackToWebp(bPath, oPath)\n\t\t\tif err != nil {\n\t\t\t\tld.Files[i].CError = err\n\t\t\t}\n\t\t\tld.Files[i].ConvertedFile = f\n\t\t\tld.Files[i].OriginalFile = f\n\t\t\tld.Files[i].Wg.Done()\n\t\t\tlog.Debugln(\"one line message sticker OK:\", f)\n\t\t}\n\t}()\n\n\treturn nil\n}\n\n// Experimental\n// Currently only process LINE Message stickers, aka LINE_SRC_PER_STICKER_TEXT\nfunc parseLineProductInfo(id string, ld *LineData) error {\n\tpage, err := httpGet(fmt.Sprintf(\"https://stickershop.line-scdn.net/stickershop/v1/product/%s/iphone/productInfo.meta\", id))\n\tif err != nil {\n\t\treturn err\n\t}\n\tlpi := LineProductInfo{}\n\terr = json.Unmarshal([]byte(page), &lpi)\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch lpi.StickerResourceType {\n\tcase LINE_SRC_PER_STICKER_TEXT:\n\t\tfor _, s := range lpi.Stickers {\n\t\t\tld.DLinks = append(ld.DLinks, strconv.FormatInt(s.ID, 10))\n\t\t}\n\t\t// case LINE_SRC_ANIMATION:\n\t\t// \tld.DLink = \"https://stickershop.line-scdn.net/stickershop/v1/product/\" + id + \"/iphone/stickerpack@2x.zip\"\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/msbimport/typedefs.go",
    "content": "package msbimport\n\nimport (\n\t\"sync\"\n)\n\n// Line sticker types\nconst (\n\tLINE_STICKER_STATIC       = \"line_s\"  //普通貼圖\n\tLINE_STICKER_ANIMATION    = \"line_a\"  //動態貼圖\n\tLINE_STICKER_POPUP        = \"line_p\"  //全螢幕\n\tLINE_STICKER_POPUP_EFFECT = \"line_f\"  //特效\n\tLINE_EMOJI_STATIC         = \"line_e\"  //表情貼\n\tLINE_EMOJI_ANIMATION      = \"line_i\"  //動態表情貼\n\tLINE_STICKER_MESSAGE      = \"line_m\"  //訊息\n\tLINE_STICKER_NAME         = \"line_n\"  //隨你填\n\tKAKAO_EMOTICON            = \"kakao_e\" //KAKAOTALK普通貼圖\n\n\tLINE_SRC_PER_STICKER_TEXT = \"PER_STICKER_TEXT\"\n\tLINE_SRC_ANIMATION        = \"ANIMATION\"\n\tLINE_SRC_STATIC           = \"STATIC\"\n\tLINE_SRC_POPUP            = \"POPUP\"\n\tLINE_SRC_NAME_TEXT        = \"NAME_TEXT\"\n\n\t//Indicates popup effect, bot popup APNG and static PNG should be kept.\n\tLINE_POPUP_LAYER_BACKGROUND = \"BACKGROUND\"\n\t//Indicates popup, only popup APNG needed.\n\tLINE_POPUP_LAYER_FOREGROUND = \"FOREGROUND\"\n\n\tStoreLine  = \"line\"\n\tStoreKakao = \"kakao\"\n\n\tWARN_KAKAO_PREFER_SHARE_LINK = \"prefer share link for kakao\"\n)\n\ntype LineFile struct {\n\t//Waitgroup for conversion.\n\tWg sync.WaitGroup\n\t//Convert to 100x100 emoji.\n\tConvertToEmoji bool\n\t// path of original file\n\tOriginalFile string\n\t// path of converted filea\n\tConvertedFile string\n\t// conversion error\n\tCError error\n}\n\n// This is called linedata due to historical reason,\n// instead, it handles \"import\" data, which includes kakao and line by far.\ntype LineData struct {\n\t//Waitgroup for when linedata become available.\n\tWg sync.WaitGroup\n\t//Store type, defined in const.\n\tStore string\n\t//Store link\n\tLink string\n\t//Store links for different langs\n\tI18nLinks []string\n\t//Sticker download link, typically ZIP.\n\tDLink string\n\t//Sticker download links.\n\tDLinks []string\n\t//Sticker file paths.\n\tFiles []*LineFile\n\t//Sticker category, defined in const.\n\tCategory string\n\t//Sticker pack ID.\n\tId string\n\t//Sticker title appeared in store.\n\tTitle string\n\t//I18n titles for LINE sticker packs, TitleWg must be waited before using this field.\n\tI18nTitles []string\n\t//WaitGroup for I18nTitles\n\tTitleWg sync.WaitGroup\n\t//Is Animated line sticker.\n\tIsAnimated bool\n\t//Is line emoji(emoticon).\n\tIsEmoji bool\n\tAmount  int\n}\n\ntype LineJson struct {\n\tName string\n\tSku  string\n\tUrl  string\n}\n\ntype KakaoJsonResult struct {\n\t//Korean title\n\tTitle string\n\t//kakao ID\n\tTitleUrl string\n\t//PNG urls\n\tThumbnailUrls []string\n\t//??\n\tTitleImageUrl string\n\t//Cover image\n\tTitleDetailUrl string\n}\n\ntype KakaoJson struct {\n\tResult KakaoJsonResult\n}\n\ntype LineProductInfo struct {\n\tPackageID           int64     `json:\"packageId\"`\n\tOnSale              bool      `json:\"onSale\"`\n\tValidDays           int64     `json:\"validDays\"`\n\tTitle               Title     `json:\"title\"`\n\tAuthor              Author    `json:\"author\"`\n\tPrice               []Price   `json:\"price\"`\n\tStickers            []Sticker `json:\"stickers\"`\n\tHasAnimation        bool      `json:\"hasAnimation\"`\n\tHasSound            bool      `json:\"hasSound\"`\n\tStickerResourceType string    `json:\"stickerResourceType\"`\n}\n\ntype Author struct {\n\tEn     string `json:\"en\"`\n\tEs     string `json:\"es\"`\n\tIn     string `json:\"in\"`\n\tJa     string `json:\"ja\"`\n\tKo     string `json:\"ko\"`\n\tTh     string `json:\"th\"`\n\tZhHans string `json:\"zh-Hans\"`\n\tZhHant string `json:\"zh-Hant\"`\n}\n\ntype Price struct {\n\tCountry  string  `json:\"country\"`\n\tCurrency string  `json:\"currency\"`\n\tSymbol   string  `json:\"symbol\"`\n\tPrice    float64 `json:\"price\"`\n}\n\ntype Sticker struct {\n\tID     int64 `json:\"id\"`\n\tWidth  int64 `json:\"width\"`\n\tHeight int64 `json:\"height\"`\n}\n\ntype Title struct {\n\tEn     string `json:\"en\"`\n\tEs     string `json:\"es\"`\n\tIn     string `json:\"in\"`\n\tJa     string `json:\"ja\"`\n\tKo     string `json:\"ko\"`\n\tTh     string `json:\"th\"`\n\tZhHans string `json:\"zh-Hans\"`\n\tZhHant string `json:\"zh-Hant\"`\n}\n"
  },
  {
    "path": "pkg/msbimport/util.go",
    "content": "package msbimport\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar httpClient = &http.Client{\n\tTimeout: 10 * time.Second,\n}\n\nfunc httpDownload(link string, f string) error {\n\tres, err := http.Get(link)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\tfp, _ := os.Create(f)\n\tdefer fp.Close()\n\t_, err = io.Copy(fp, res.Body)\n\treturn err\n}\n\nfunc httpDownloadCurlUA(link string, f string) error {\n\treq, _ := http.NewRequest(\"GET\", link, nil)\n\treq.Header.Set(\"User-Agent\", \"curl/7.61.1\")\n\tres, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\tfp, _ := os.Create(f)\n\tdefer fp.Close()\n\t_, err = io.Copy(fp, res.Body)\n\treturn err\n}\n\nfunc httpGet(link string) (string, error) {\n\treq, _ := http.NewRequest(\"GET\", link, nil)\n\treq.Header.Set(\"User-Agent\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36\")\n\treq.Header.Set(\"Accept-Language\", \"zh-Hant;q=0.9, ja;q=0.8, en;q=0.7\")\n\tres, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer res.Body.Close()\n\tcontent, _ := io.ReadAll(res.Body)\n\treturn string(content), nil\n}\n\n// redirected link, body, error\nfunc httpGetWithRedirLink(link string) (string, string, error) {\n\tclient := &http.Client{}\n\treq, _ := http.NewRequest(\"GET\", link, nil)\n\treq.Header.Set(\"User-Agent\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36\")\n\treq.Header.Set(\"Accept-Language\", \"zh-Hant;q=0.9, ja;q=0.8, en;q=0.7\")\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tdefer res.Body.Close()\n\tcontent, _ := io.ReadAll(res.Body)\n\treturn res.Request.URL.String(), string(content), nil\n}\n\nfunc httpGetAndroidUA(link string) (string, error) {\n\treq, _ := http.NewRequest(\"GET\", link, nil)\n\treq.Header.Set(\"User-Agent\", \"Android\")\n\tres, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer res.Body.Close()\n\tcontent, _ := io.ReadAll(res.Body)\n\treturn string(content), nil\n}\n\nfunc fDownload(link string, savePath string) error {\n\tcmd := exec.Command(\"curl\", \"-o\", savePath, link)\n\t_, err := cmd.CombinedOutput()\n\treturn err\n}\n\nfunc fExtract(f string) string {\n\ttargetDir := filepath.Join(filepath.Dir(f), SecHex(4))\n\tos.MkdirAll(targetDir, 0755)\n\tlog.Debugln(\"Extracting to :\", targetDir)\n\n\tout, err := exec.Command(BSDTAR_BIN, \"-xvf\", f, \"-C\", targetDir).CombinedOutput()\n\tif err != nil {\n\t\tlog.Errorln(\"Error extracting:\", string(out))\n\t\treturn \"\"\n\t} else {\n\t\treturn targetDir\n\t}\n}\n\nfunc SecHex(n int) string {\n\tbytes := make([]byte, n)\n\trand.Read(bytes)\n\treturn hex.EncodeToString(bytes)\n}\n\nfunc ArchiveExtract(f string) []string {\n\ttargetDir := filepath.Join(path.Dir(f), SecHex(4))\n\tos.MkdirAll(targetDir, 0755)\n\n\terr := exec.Command(BSDTAR_BIN, \"-xvf\", f, \"-C\", targetDir).Run()\n\tif err != nil {\n\t\tlog.Warnln(\"ArchiveExtract error:\", err)\n\t\treturn []string{}\n\t}\n\treturn LsFilesR(targetDir, []string{}, []string{})\n}\n\nfunc LsFilesR(dir string, mustHave []string, mustNotHave []string) []string {\n\tvar files []string\n\terr := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {\n\t\taccept := true\n\t\tconfidence := 0\n\t\tfor _, kw := range mustHave {\n\t\t\tif !strings.Contains(strings.ToLower(path), strings.ToLower(kw)) {\n\t\t\t\tconfidence += 1\n\t\t\t}\n\t\t}\n\t\tif confidence < len(mustHave) {\n\t\t\taccept = false\n\t\t}\n\n\t\tfor _, kw := range mustNotHave {\n\t\t\tif strings.Contains(strings.ToLower(path), strings.ToLower(kw)) {\n\t\t\t\taccept = false\n\t\t\t}\n\t\t}\n\t\tif info.IsDir() {\n\t\t\taccept = false\n\t\t}\n\t\tlog.Debugf(\"accept?: %t path: %s\", accept, path)\n\t\tif accept {\n\t\t\tfiles = append(files, path)\n\t\t}\n\t\treturn err\n\t})\n\tlog.Debugln(\"listed following:\")\n\tlog.Debugln(files)\n\tif err != nil {\n\t\treturn []string{}\n\t} else {\n\t\treturn files\n\t}\n}\n\nfunc LsFiles(dir string, mustHave []string, mustNotHave []string) []string {\n\tvar files []string\n\tglob, _ := filepath.Glob(path.Join(dir, \"*\"))\n\n\tfor _, path := range glob {\n\t\tf, _ := os.Stat(path)\n\t\tif f.IsDir() {\n\t\t\tcontinue\n\t\t}\n\n\t\taccept := true\n\t\tfor _, kw := range mustHave {\n\t\t\tif !strings.Contains(strings.ToLower(path), strings.ToLower(kw)) {\n\t\t\t\taccept = false\n\t\t\t}\n\t\t}\n\t\tfor _, kw := range mustNotHave {\n\t\t\tif strings.Contains(strings.ToLower(path), strings.ToLower(kw)) {\n\t\t\t\taccept = false\n\t\t\t}\n\t\t}\n\t\tlog.Debugf(\"accept?: %t path: %s\", accept, path)\n\t\tif accept {\n\t\t\tfiles = append(files, path)\n\t\t}\n\t}\n\treturn files\n}\n\nfunc FCompress(f string, flist []string) error {\n\t// strip data dir in zip.\n\t// comps are 2\n\tcomps := \"2\"\n\n\targs := []string{\"--strip-components\", comps, \"-avcf\", f}\n\t// args := []string{\"-avcf\", f}\n\targs = append(args, flist...)\n\n\tlog.Debugf(\"Compressing strip-comps:%s to file:%s for these files:%v\", comps, f, flist)\n\tout, err := exec.Command(BSDTAR_BIN, args...).CombinedOutput()\n\tlog.Debugln(string(out))\n\tif err != nil {\n\t\tlog.Error(\"Compress error!\")\n\t\tlog.Errorln(string(out))\n\t}\n\treturn err\n}\n\nfunc FCompressVol(f string, flist []string) []string {\n\tbasename := filepath.Base(f)\n\tdir := filepath.Dir(f)\n\tzipIndex := 0\n\tvar zips [][]string\n\tvar zipPaths []string\n\tvar curSize int64 = 0\n\n\tfor _, f := range flist {\n\t\tst, err := os.Stat(f)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfSize := st.Size()\n\t\tif curSize == 0 {\n\t\t\tzips = append(zips, []string{})\n\t\t}\n\t\tif curSize+fSize < 50000000 {\n\t\t\tzips[zipIndex] = append(zips[zipIndex], f)\n\t\t} else {\n\t\t\tcurSize = 0\n\t\t\tzips = append(zips, []string{})\n\t\t\tzipIndex += 1\n\t\t\tzips[zipIndex] = append(zips[zipIndex], f)\n\t\t}\n\t\tcurSize += fSize\n\t}\n\n\tfor i, files := range zips {\n\t\tvar zipBN string\n\t\tif len(zips) == 1 {\n\t\t\tzipBN = basename\n\t\t} else {\n\t\t\tzipBN = strings.TrimSuffix(basename, \".zip\")\n\t\t\tzipBN += fmt.Sprintf(\"_00%d.zip\", i+1)\n\t\t}\n\n\t\tzipPath := filepath.Join(dir, zipBN)\n\t\terr := FCompress(zipPath, files)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tzipPaths = append(zipPaths, zipPath)\n\t}\n\treturn zipPaths\n}\n"
  },
  {
    "path": "pkg/msbimport/workers.go",
    "content": "package msbimport\n\nimport (\n\t\"strings\"\n\n\t\"github.com/panjf2000/ants/v2\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Workers pool for converting webm\nvar wpConvertWebm, _ = ants.NewPoolWithFunc(4, wConvertWebm)\n\n// Accepts *LineFile\nfunc wConvertWebm(i interface{}) {\n\tlf := i.(*LineFile)\n\tdefer lf.Wg.Done()\n\tlog.Debugln(\"Converting in pool for:\", lf)\n\n\tvar err error\n\t//FFMpeg doest not support animated webp.\n\t//IM convert it to apng then feed to webm.\n\tif strings.HasSuffix(lf.OriginalFile, \".webp\") {\n\t\tlf.OriginalFile, _ = IMToApng(lf.OriginalFile)\n\t}\n\n\tlf.ConvertedFile, err = FFToWebmTGVideo(lf.OriginalFile, lf.ConvertToEmoji)\n\tif err != nil {\n\t\tlf.CError = err\n\t}\n\tlog.Debugln(\"convert OK: \", lf.ConvertedFile)\n}\n"
  },
  {
    "path": "test/kakao_links.json",
    "content": "[\n    [\n        \"static kakao store\",\n        \"https://e.kakao.com/t/pretty-all-friends\"\n    ],\n    [\n        \"animated kakao store\",\n        \"https://e.kakao.com/t/lovey-dovey-healing-bear\"\n    ],\n    [\n        \"animated kakao share link\",\n        \"https://emoticon.kakao.com/items/lV6K2fWmU7CpXlHcP9-ysQJx9rg=?referer=share_link\"\n    ]\n]"
  },
  {
    "path": "test/line_links.json",
    "content": "[\n    [\n        \"LINE_STICKER_ANIMATED\",\n        \"https://store.line.me/stickershop/product/8831/\"\n    ],\n    [\n        \"LINE_STICKER_MESSAGE, no region lock \",\n        \"https://store.line.me/stickershop/product/26407/\"\n    ],\n    [\n        \"LINE_STICKER_MESSAGE, has region lock \",\n        \"https://store.line.me/stickershop/product/12320864/zh-Hant\"\n    ],\n    [\n        \"LINE_STICKER_STATIC, officialaccount\",\n        \"https://store.line.me/officialaccount/event/sticker/27404/zh-Hant\"\n    ],\n    [\n        \"LINE_STICKER_STATIC, special officialaccount, no <script>\",\n        \"https://store.line.me/officialaccount/event/sticker/27239/ja\"\n    ],\n    [\n        \"Line message, has region lock\",\n        \"https://store.line.me/stickershop/product/12188389/ja\"\n    ],\n    [\n        \"Line popup, no effect, use only popup APNG\",\n        \"https://store.line.me/stickershop/product/22229788/ja\"\n    ],\n    [\n        \"popup effect(both popup APNG and static PNG must be kept)\",\n        \"https://store.line.me/stickershop/product/14011294/ja\"\n    ],\n    [\n        \"Line emoji static\",\n        \"https://store.line.me/emojishop/product/5ac309f0031a6752fb806d8d/en\"\n    ]\n]\n"
  },
  {
    "path": "test/tg_links.json",
    "content": "[\n        [\n                \"Static\",\n                \"https://t.me/addstickers/sticker_6356d1bf_by_moe_sticker_bot\"\n        ],\n        [\n                \"Video\",\n                \"https://t.me/addstickers/Asoul_video_jiaran\"\n        ],\n        [\n                \"TGS\",\n                \"https://t.me/addstickers/prtyparrot\"\n        ]\n]"
  },
  {
    "path": "tools/msb_emoji.py",
    "content": "#!/usr/bin/python3\n\nimport emoji\nimport sys\nimport json\n\n# This simple python tool utilizes\n# 'emoji' package in PyPI which has great funcionality in\n# parsing and extracting complicated emojis from string.\n\n# Usage:\n# 1st cmdline arg: 'string', 'json', 'text'.\n# 2nd cmdline arg: string containing emoji(s).\n\n# Example:\n# ./msb_emoji.py string 🌸\n\ndef main():\n    if len(sys.argv) < 3:\n        print(\"wrong cmd line argument.\") \n        return -1\n\n    s = sys.argv[2]\n    e = emoji.distinct_emoji_list(s)\n\n    if sys.argv[1] == 'string':\n        sys.stdout.write(''.join(e))\n    elif sys.argv[1] == 'json':\n        sys.stdout.write(json.dumps(e, ensure_ascii=False))\n    elif sys.argv[1] == 'text':\n        sys.stdout.write(emoji.replace_emoji(s,''))\n\n    return 0\n\nmain()\n"
  },
  {
    "path": "tools/msb_kakao_decrypt.py",
    "content": "#!/usr/bin/python3\n\n# This tool is intended to decrypt kakao animated webp sticker.\n# Specify file path as 1st pos arg and decrypted data will overwrite the original one.\n\n# Credit:\n# https://github.com/blluv/KakaoTalkEmoticonDownloader MIT License Copyright @blluv\n\nimport sys\n\n\ndef generate_lfsr(key):\n    d = list(key*2)\n    seq=[0,0,0]\n\n    seq[0] = 301989938\n    seq[1] = 623357073\n    seq[2] = -2004086252\n\n    i = 0\n\n    for i in range(0, 4):\n        seq[0] = ord(d[i]) | (seq[0] << 8)\n        seq[1] = ord(d[4+i]) | (seq[1] << 8)\n        seq[2] = ord(d[8+i]) | (seq[2] << 8)\n\n    seq[0] = seq[0] & 0xffffffff\n    seq[1] = seq[1] & 0xffffffff\n    seq[2] = seq[2] & 0xffffffff\n\n    return seq\n\ndef xor_byte(b, seq):\n    flag1=1\n    flag2=0\n    result=0\n    for _ in range(0, 8):\n        v10 = (seq[0] >> 1)\n        if (seq[0] << 31) & 0xffffffff:\n            seq[0] = (v10 ^ 0xC0000031)\n            v12 = (seq[1] >> 1)\n            if (seq[1] << 31) & 0xffffffff:\n                seq[1] = ((v12 | 0xC0000000) ^ 0x20000010)\n                flag1 = 1\n            else:\n                seq[1] = (v12 & 0x3FFFFFFF)\n                flag1 = 0\n        else:\n            seq[0] = v10\n            v11 = (seq[2] >> 1)\n            if (seq[2] << 31) & 0xffffffff:\n                seq[2] = ((v11 | 0xF0000000) ^ 0x8000001)\n                flag2 = 1\n            else:\n                seq[2] = (v11 & 0xFFFFFFF)\n                flag2 = 0\n\n        result = (flag1 ^ flag2 | 2 * result)\n    return (result ^ b)\n\ndef xor_data(data):\n    dat=list(data)\n    s=generate_lfsr(\"a271730728cbe141e47fd9d677e9006d\")\n    for i in range(0,128):\n        dat[i]=xor_byte(dat[i], s)\n    return bytes(dat)\n\n\ndef main():\n    if len(sys.argv) < 2:\n        print('FATAL: no file specified on $1 arg.')\n        return -1\n    \n    f = sys.argv[1]\n    \n    fi =  open(f, 'rb')\n    data = fi.read()\n    fi.close()\n\n    dec_data = xor_data(data)\n    fo = open(f, 'wb')\n    fo.write(dec_data)\n    fo.close()\n\nmain()\n"
  },
  {
    "path": "tools/msb_rlottie.py",
    "content": "#!/usr/bin/python3\n\n# Utilize rlottie-python to convert TGS images.\n# Credit https://github.com/laggykiller/rlottie-python GPL-2.0 license  Copyright @laggykiller\n\n\n# Example:\n# msb_rlottie in.tgs out.gif\n\nfrom rlottie_python import LottieAnimation\nimport sys\n\ndef main():\n    f_in = sys.argv[1]\n    f_out = sys.argv[2]\n\n    anim = LottieAnimation.from_tgs(f_in)\n    anim.save_animation(f_out)\n\nmain()\n"
  },
  {
    "path": "web/nginx/assetlinks.json",
    "content": "[\n    {\n        \"relation\": [\n            \"delegate_permission/common.handle_all_urls\"\n        ],\n        \"target\": {\n            \"namespace\": \"android_app\",\n            \"package_name\": \"moe.star39.msb_app\",\n            \"sha256_cert_fingerprints\": [\n                \"93:BE:BA:D8:31:52:EB:1B:C0:E9:A7:E7:C9:9D:13:52:80:CD:05:41:9B:68:34:90:5A:D7:D3:23:2D:6D:23:C3\"\n            ]\n        }\n    }\n]"
  },
  {
    "path": "web/nginx/default.conf.template",
    "content": "# For more information on configuration, see:\n#   * Official English Documentation: http://nginx.org/en/docs/\n#   * Official Russian Documentation: http://nginx.org/ru/docs/\n\nlimit_req_zone $binary_remote_addr zone=one:10m rate=100r/s;\n\nserver {\n    listen ${NGINX_PORT} ssl http2;\n    server_name _;\n\n    ssl_certificate ${NGINX_CERT};\n    ssl_certificate_key ${NGINX_KEY};\n    ssl_session_cache shared:SSL:1m;\n    ssl_session_timeout 10m;\n    ssl_prefer_server_ciphers on;\n\n    root /www;\n\n    limit_req zone=one burst=200;\n\n    location /webapp/data {\n        alias ${WEBAPP_ROOT}/data;\n    }\n    location /webapp/api {\n        proxy_pass http://${WEBAPP_ADDR}/api;\n    }\n    location /webapp/edit {\n        alias ${WEBAPP_ROOT};\n    }\n    location /webapp/export {\n        alias ${WEBAPP_ROOT};\n    }\n    location /webapp/static {\n        alias ${WEBAPP_ROOT}/static;\n    }\n}\n"
  },
  {
    "path": "web/webapp3/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "web/webapp3/README.md",
    "content": "# WebApp for moe-sticker-bot\n\nSupports editing emoji and drag n drop to sort.\n\nSupports dark mode.\n\n## Build\n```\nREACT_APP_HOST=your_website npm run build\n```\n"
  },
  {
    "path": "web/webapp3/package.json",
    "content": "{\n  \"name\": \"webapp3\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@dnd-kit/core\": \"^6.1.0\",\n    \"@dnd-kit/sortable\": \"^8.0.0\",\n    \"@dnd-kit/utilities\": \"^3.2.2\",\n    \"axios\": \"1.6.7\",\n    \"react\": \"^18.2.0\",\n    \"react-cool-img\": \"^1.2.12\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.22.0\",\n    \"react-scripts\": \"5.0.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\"\n  },\n  \"eslintConfig\": {\n    \"extends\": [\n      \"react-app\",\n      \"react-app/jest\"\n    ]\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">5%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  }\n}\n"
  },
  {
    "path": "web/webapp3/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <script src=\"https://telegram.org/js/telegram-web-app.js\"></script>\n\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"Web site created using create-react-app\"\n    />\n    <!-- <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" /> -->\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <!-- <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" /> -->\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "web/webapp3/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "web/webapp3/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "web/webapp3/src/App.css",
    "content": "\n"
  },
  {
    "path": "web/webapp3/src/App.js",
    "content": "import { useEffect, useState } from 'react';\nimport Edit from './Edit'\nimport Export from './Export';\nimport axios from 'axios';\nimport { Route, Routes, useLocation } from \"react-router-dom\"\n\nfunction App() {\n  let [isInitDataValid, setIsInitDataValid] = useState(null)\n  const route = useLocation().pathname\n  const qstring = window.location.search ? window.location.search + \"&\" : \"?\"\n\n  useEffect(() => {\n    axios.post(`/webapp/api/initData${qstring}cmd=${route}`,\n      new URLSearchParams(window.Telegram.WebApp.initData))\n      .then(res => {\n        setIsInitDataValid(true)\n      })\n      .catch(err => {\n        setIsInitDataValid(false)\n      })\n  }, [])\n\n  if (isInitDataValid === null) {\n    // initData not generated yet.\n    return;\n  } else if (!isInitDataValid) {\n    // Bad initData\n    return (<div className=\"App\">\n      <h2>Invalid WebApp initData.</h2>\n      <br/>\n      <h2>Please launch WebApp using /manage command.</h2>\n      <br/>\n      <h2>請通過 /manage 指令打開WebApp.</h2>\n      </div>);\n  } else {\n    // initData OK\n    window.Telegram.WebApp.ready();\n    return (\n      <div className='App'>\n        <header className=\"App-header\">\n        </header>\n        <Routes>\n          <Route path=\"/webapp/edit\" element={<Edit />} />\n          <Route path=\"/webapp/export\" element={<Export />} />\n        </Routes>\n      </div>\n    );\n  }\n}\n\nexport default App;\n"
  },
  {
    "path": "web/webapp3/src/Edit.js",
    "content": "import React, { useEffect, useReducer, useState } from 'react';\nimport axios from 'axios';\n\nimport {\n  DndContext,\n  closestCenter,\n  DragOverlay,\n  MouseSensor,\n  TouchSensor,\n  useSensor,\n  useSensors,\n} from '@dnd-kit/core';\n\nimport {\n  arrayMove,\n  SortableContext,\n  rectSortingStrategy,\n} from '@dnd-kit/sortable';\n\nimport { StickerGrid } from './StickerGrid'\nimport { SortableSticker } from './SortableSticker'\nimport { Sticker } from './Sticker'\n\nlet resultArray;\n\nfunction Edit() {\n  const [items, setItems] = useState(null);\n  const [activeId, setActiveId] = useState(null);\n  // const [ss, setSS] = useState(null)\n\n  const [, forceUpdate] = useReducer(x => x + 1, 0);\n\n  const sensors = useSensors(\n    useSensor(MouseSensor, {\n      activationConstraint: {\n        delay: 250,\n      },\n    }),\n    useSensor(TouchSensor, {\n      activationConstraint: {\n        delay: 250,\n        tolerance: 5,\n      },\n    })\n  );\n\n  useEffect(() => {\n    const uid = window.Telegram.WebApp.initDataUnsafe.user.id\n    const queryId = window.Telegram.WebApp.initDataUnsafe.query_id\n    axios.get(`/webapp/api/ss?uid=${uid}&qid=${queryId}&cmd=edit`)\n      .then(res => {\n        setItems(res.data.ss)\n      })\n      .catch(() => { })\n\n    window.Telegram.WebApp.MainButton.setText('Done/完成').show()\n      .onClick(() => {\n        const uid = window.Telegram.WebApp.initDataUnsafe.user.id\n        const queryId = window.Telegram.WebApp.initDataUnsafe.query_id\n        axios.post(\n          `/webapp/api/edit/result?uid=${uid}&qid=${queryId}`,\n          JSON.stringify(resultArray))\n          .then(() => {\n            window.Telegram.WebApp.close();\n          })\n          .catch((error) => {\n            if (error.response) {\n              window.Telegram.WebApp.showAlert(error + \"\\n\" + error.response.data)\n            } else {\n              window.Telegram.WebApp.showAlert(error)\n            }\n          });\n      });\n    // This is to address Android specific bug.\n    // Expanding the webapp by swiping the content up\n    // might cause dnd-context to freeze and cannot be recovered.\n    // iOS is not affected.\n    if (window.Telegram.WebApp.platform === \"android\") {\n      window.Telegram.WebApp.expand()\n    }\n  }, []) //useEffect\n\n  if (items == null) {\n    return (\n      <p>\n        Please wait...\n      </p>\n    )\n  }\n  // Reject empty ss.\n  if (items === []) {\n    return (\n      <div className=\"App\"><h3>Please launch WebApp through /manage command.</h3>\n        <br />\n        <h3>請使用 /manage 指令後打開WebApp。</h3>\n      </div>\n    )\n  }\n\n  return (\n    <div>\n      <h3>Please hold and drag to reorder</h3>\n      <h3>請按住並拖拽來排序</h3>\n      <DndContext\n        sensors={sensors}\n        collisionDetection={closestCenter}\n        onDragStart={handleDragStart}\n        onDragEnd={handleDragEnd}\n        onDragCancel={handleDragCancel}\n      >\n        <SortableContext items={items} strategy={rectSortingStrategy}>\n          <StickerGrid columns={4}>\n            {\n              items.map((item) => (\n                <SortableSticker\n                  key={item.id}\n                  id={item.id}\n                  emoji={item.emoji}\n                  onEmojiChange={setEmoji}\n                  surl={item.surl} />\n              ))\n            }\n          </StickerGrid>\n        </SortableContext>\n\n        <DragOverlay adjustScale={true}>\n          {activeId ?\n            <Sticker\n              id={activeId}\n              surl={items[items.map(o => o.id).indexOf(activeId)].surl}\n              emoji={items[items.map(o => o.id).indexOf(activeId)].emoji}\n            />\n            : null}\n        </DragOverlay>\n      </DndContext>\n    </div>\n  );\n\n  function handleDragStart(event) {\n    setActiveId(event.active.id);\n    window.Telegram.WebApp.HapticFeedback.impactOccurred(\"heavy\");\n  }\n\n  function handleDragEnd(event) {\n    const { active, over } = event;\n    if (active.id !== over.id) {\n      setItems((items) => {\n        const oldIndex = items.map(o => o.id).indexOf(active.id)\n        const newIndex = items.map(o => o.id).indexOf(over.id)\n        apiSubmitIndexChange(oldIndex, newIndex, items) //This items is before change commited.\n        const newArray = arrayMove(items, oldIndex, newIndex);\n        resultArray = newArray;\n        return newArray\n      });\n    }\n    setActiveId(null);\n    window.Telegram.WebApp.HapticFeedback.impactOccurred(\"soft\");\n  }\n\n  function handleDragCancel() {\n    setActiveId(null);\n  }\n\n  function setEmoji(id, value) {\n    let newItems = items\n    let pos = newItems.map(o => o.id).indexOf(id)\n    newItems[pos].emoji = value\n    newItems[pos].emoji_changed = true\n    setItems(newItems)\n    resultArray = newItems\n    forceUpdate()\n  }\n\n  function apiSubmitIndexChange(oldIndex, newIndex, items) {\n    const uid = window.Telegram.WebApp.initDataUnsafe.user.id\n    const qid = window.Telegram.WebApp.initDataUnsafe.query_id\n    let form = new FormData()\n    form.append(\"oldIndex\", oldIndex)\n    form.append(\"newIndex\", newIndex)\n    axios.post(`/webapp/api/edit/move?uid=${uid}&qid=${qid}`, form)\n      .then((res) => {\n        console.log(\"pos mov ok.\")\n      })\n      .catch((err) => {\n        console.log(\"pos mov failed!\")\n        setItems(items) //Revert items.\n        window.Telegram.WebApp.showAlert(\n          `${err}\\n${err.response.data}\\nChange reverted, please try again`\n        )\n      })\n  }\n};\n\n\nexport default Edit;\n"
  },
  {
    "path": "web/webapp3/src/Export.js",
    "content": "import React, { useEffect, useReducer, useState } from 'react';\nimport axios, { all } from 'axios';\nimport { sha256sum } from './utils';\n\nimport { StickerGrid } from './StickerGrid'\nimport { SortableSticker } from './SortableSticker'\nimport { Sticker } from './Sticker'\n\nfunction Export() {\n  const [ss, setSS] = useState([])\n  const uid = window.Telegram.WebApp.initDataUnsafe.user.id\n  const queryId = window.Telegram.WebApp.initDataUnsafe.query_id\n  const params = new Proxy(new URLSearchParams(window.location.search), {\n    get: (searchParams, prop) => searchParams.get(prop),\n  });\n\n  const host = process.env.REACT_APP_HOST\n  const exportLinkHttps = `https://${host}/webapp/api/export?dn=${host}&sn=${params.sn}&qid=${queryId}&hex=${params.hex}`\n  const exportLinkMsb = `msb://app/export/${params.sn}/?dn=${host}&qid=${queryId}&hex=${params.hex}`\n\n  useEffect(() => {\n    axios.get(`/webapp/api/ss?sn=${params.sn}&uid=${uid}&qid=${queryId}&hex=${params.hex}&cmd=export`)\n      .then(res => {\n        setSS(res.data.ss)\n      })\n      .catch(() => { })\n\n    // Android specific bug.\n    // Android does not support opening custom scheme link\n    // as well as https link from MainButton.\n    // Hence, we need to put a button inside WebPage and do not\n    // generate MainButton.\n    if (window.Telegram.WebApp.platform !== \"android\") {\n      window.Telegram.WebApp.MainButton.setText('Export/匯出').show()\n        .onClick(() => {\n          window.open(exportLinkMsb)\n        })\n    }\n  }, [])\n  return (\n\n    <div>\n      {window.Telegram.WebApp.platform !== \"ios\" ? (\n        <button onClick={() => window.location.replace(exportLinkHttps)}>\n          Export/匯出\n        </button>) : null\n      }\n      <br/>\n      <h3>\n        Preview 預覽:\n      </h3>\n      <StickerGrid columns={4}>\n        {\n          ss.map((item) => (\n            <SortableSticker\n              key={item.id}\n              id={item.id}\n              emoji={item.emoji}\n              surl={item.surl} />\n          ))\n        }\n      </StickerGrid>\n      <br />\n      {window.Telegram.WebApp.platform !== \"ios\" ? (\n        <button onClick={() => window.location.replace(exportLinkHttps)}>\n          Export/匯出\n        </button>) : null\n      }\n    </div>\n  )\n}\n\nexport default Export\n"
  },
  {
    "path": "web/webapp3/src/SortableSticker.js",
    "content": "import {useSortable} from '@dnd-kit/sortable';\nimport {CSS} from '@dnd-kit/utilities';\n\nimport {Sticker} from './Sticker';\n\nexport function SortableSticker(props) {\n  const {\n    attributes,\n    listeners,\n    setNodeRef,\n    transform,\n    transition,\n  } = useSortable({id: props.id}); //id CANNOT be set to 0 !!!\n\n  const style = {\n    transform: CSS.Transform.toString(transform),\n    transition,\n  };\n\n  return (\n    <Sticker\n      ref={setNodeRef}\n      style={style}\n      {...props}\n      {...attributes}\n      {...listeners}\n    />\n  );\n};\n"
  },
  {
    "path": "web/webapp3/src/Sticker.js",
    "content": "import React, { forwardRef } from 'react';\n// import axios from 'axios';\nimport Img from \"react-cool-img\";\nimport './StickerStyle.css'\nimport loading_gif from './loading.gif'\n\n\n\nexport const Sticker = forwardRef(({ id, faded, style, emoji, surl, onEmojiChange, ...props }, ref) => {\n\n    return (\n      <div className='Sticker-Div' ref={ref} style={style} {...props}>\n          <Img src={surl} placeholder={loading_gif} alt=\"Loading...\"\n            retry={{ count: 10, delay: 2, acc: false }}\n          ></Img>\n        <br />\n        <div>\n          <label>{id}</label>\n          <input type=\"text\" value={emoji} size=\"6\"\n            onChange={(e) => onEmojiChange(id, e.target.value)}></input>\n        </div>\n      </div>\n    );\n});\n"
  },
  {
    "path": "web/webapp3/src/StickerGrid.js",
    "content": "import React from 'react';\n\nexport function StickerGrid({children, columns}) {\n  return (\n    <ul\n      style={{\n        maxWidth: '800px',\n        display: 'grid',\n        gridTemplateColumns: `repeat(${columns}, 1fr)`,\n        gridGap: 10,\n        padding: 10,\n        \n      }}\n    >\n      {children}\n    </ul>\n  );\n}\n"
  },
  {
    "path": "web/webapp3/src/StickerStyle.css",
    "content": "img {\n    display: block;\n    max-width: 64px;\n    max-height: 64px;\n    width: auto;\n    height: auto;\n}\n\n.Image-div {\n    width: 64px;\n    height: 64px;\n}\n\n.Sticker-Div {\n    touch-action: manipulation;\n}"
  },
  {
    "path": "web/webapp3/src/index.css",
    "content": ".div {\n  touch-action: auto;\n}\n\nbody {\n  font-family: sans-serif;\n  background-color: var(--tg-theme-bg-color, #ffffff);\n  color: var(--tg-theme-text-color, #222222);\n  font-size: 16px;\n  margin: 0;\n  padding: 0;\n  color-scheme: var(--tg-color-scheme);\n}\n\na {\n  color: var(--tg-theme-link-color, #2678b6);\n}\n\nbutton {\n  display: block;\n  width: 100%;\n  font-size: 14px;\n  margin: 15px 0;\n  padding: 12px 20px;\n  border: none;\n  border-radius: 4px;\n  background-color: var(--tg-theme-button-color, #50a8eb);\n  color: var(--tg-theme-button-text-color, #ffffff);\n  cursor: pointer;\n}\n\nbutton[disabled] {\n  opacity: 0.6;\n  cursor: auto;\n  pointer-events: none;\n}\n\nbutton.close_btn {\n  position: absolute;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  border-radius: 0;\n  margin: 0;\n  padding: 16px 20px;\n  text-transform: uppercase;\n}\n\nsection {\n  padding: 15px 15px 65px;\n  text-align: center;\n}\n\np {\n  margin: 40px 0 15px;\n}\n\nul {\n  text-align: left;\n}\n\nli {\n  color: var(--tg-theme-hint-color, #a8a8a8);\n}\n\ntextarea {\n  width: 100%;\n  box-sizing: border-box;\n  padding: 7px;\n}\n\npre {\n  background: rgba(0, 0, 0, .07);\n  border-radius: 4px;\n  padding: 4px;\n  margin: 7px 0;\n  word-break: break-all;\n  word-break: break-word;\n  white-space: pre-wrap;\n  text-align: left;\n}\n\n.dark pre {\n  background: rgba(255, 255, 255, .15);\n}\n\n.hint {\n  font-size: .8em;\n  color: var(--tg-theme-hint-color, #a8a8a8);\n}\n\n.ok {\n  color: green;\n}\n\n.err {\n  color: red;\n}\n\n#fixed_wrap {\n  position: fixed;\n  left: 0;\n  right: 0;\n  top: 0;\n  transform: translateY(100vh);\n}\n\n.viewport_border,\n.viewport_stable_border {\n  position: fixed;\n  left: 0;\n  right: 0;\n  top: 0;\n  height: var(--tg-viewport-height, 100vh);\n  pointer-events: none;\n}\n\n.viewport_stable_border {\n  height: var(--tg-viewport-stable-height, 100vh);\n}\n\n.viewport_border:before,\n.viewport_stable_border:before {\n  content: attr(text);\n  display: inline-block;\n  position: absolute;\n  background: gray;\n  right: 0;\n  top: 0;\n  font-size: 7px;\n  padding: 2px 4px;\n  vertical-align: top;\n}\n\n.viewport_stable_border:before {\n  background: green;\n  left: 0;\n  right: auto;\n}\n\n.viewport_border:after,\n.viewport_stable_border:after {\n  content: '';\n  display: block;\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  border: 2px dashed gray;\n}\n\n.viewport_stable_border:after {\n  border-color: green;\n}\n\nsmall {\n  font-size: 12px;\n}"
  },
  {
    "path": "web/webapp3/src/index.js",
    "content": "import React, { StrictMode } from 'react';\nimport ReactDOM from 'react-dom/client';\nimport './index.css';\nimport App from './App';\nimport { BrowserRouter } from 'react-router-dom';\n\nconst root = ReactDOM.createRoot(document.getElementById('root'));\n\nroot.render(\n  <StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </StrictMode>\n\n);\n"
  },
  {
    "path": "web/webapp3/src/utils.js",
    "content": "\nexport function sha256sum(string) {\n    const utf8 = new TextEncoder().encode(string);\n    return crypto.subtle.digest('SHA-256', utf8).then((hashBuffer) => {\n        const hashArray = Array.from(new Uint8Array(hashBuffer));\n        const hashHex = hashArray\n            .map((bytes) => bytes.toString(16).padStart(2, '0'))\n            .join('');\n        return hashHex;\n    });\n}"
  }
]